summaryrefslogtreecommitdiffstats
path: root/src/com/android/email/service/AttachmentService.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/email/service/AttachmentService.java')
-rw-r--r--src/com/android/email/service/AttachmentService.java1401
1 files changed, 0 insertions, 1401 deletions
diff --git a/src/com/android/email/service/AttachmentService.java b/src/com/android/email/service/AttachmentService.java
deleted file mode 100644
index f8871688a..000000000
--- a/src/com/android/email/service/AttachmentService.java
+++ /dev/null
@@ -1,1401 +0,0 @@
-/*
- * Copyright (C) 2014 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.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 com.google.common.annotations.VisibleForTesting;
-
-import java.io.File;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.PriorityQueue;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentLinkedQueue;
-
-public class AttachmentService extends Service implements Runnable {
- // For logging.
- public static final String LOG_TAG = "AttachmentService";
-
- // STOPSHIP Set this to 0 before shipping.
- private static final int ENABLE_ATTACHMENT_SERVICE_DEBUG = 0;
-
- // 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 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;
-
- static final int PRIORITY_NONE = -1;
- // High priority is for user requests
- static final int PRIORITY_FOREGROUND = 0;
- static final int PRIORITY_HIGHEST = PRIORITY_FOREGROUND;
- // Normal priority is for forwarded downloads in outgoing mail
- static final int PRIORITY_SEND_MAIL = 1;
- // Low priority will be used for opportunistic downloads
- static final int PRIORITY_BACKGROUND = 2;
- static final int PRIORITY_LOWEST = PRIORITY_BACKGROUND;
-
- // 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_ID =
- "com.android.email.AttachmentService.attachment_id";
- private static final String EXTRA_ATTACHMENT_FLAGS =
- "com.android.email.AttachmentService.attachment_flags";
-
- // This callback is invoked by the various service implementations to give us download progress
- // since those modules are responsible for the actual download.
- final ServiceCallback mServiceCallback = new ServiceCallback();
-
- // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
- // by the use of "volatile"
- static volatile AttachmentService sRunningService = null;
-
- // Signify that we are being shut down & destroyed.
- private volatile boolean mStop = false;
-
- EmailConnectivityManager mConnectivityManager;
-
- // Helper class that keeps track of in progress downloads to make sure that they
- // are progressing well.
- final AttachmentWatchdog mWatchdog = new AttachmentWatchdog();
-
- private final Object mLock = new Object();
-
- // A map of attachment storage used per account as we have account based maximums to follow.
- // 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 loaded). If and when we reach the per-account
- // limit, we recalculate the actual usage
- final ConcurrentHashMap<Long, Long> mAttachmentStorageMap = new ConcurrentHashMap<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
- final ConcurrentHashMap<Long, Integer> mAttachmentFailureMap = new ConcurrentHashMap<Long, Integer>();
-
- // Keeps tracks of downloads in progress based on an attachment ID to DownloadRequest mapping.
- final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
- new ConcurrentHashMap<Long, DownloadRequest>();
-
- final DownloadQueue mDownloadQueue = new DownloadQueue();
-
- // The queue entries here are entries of the form {id, flags}, with the values passed in to
- // attachmentChanged(). Entries in the queue are picked off in processQueue().
- private static final Queue<long[]> sAttachmentChangedQueue =
- new ConcurrentLinkedQueue<long[]>();
-
- // Extra layer of control over debug logging that should only be enabled when
- // we need to take an extra deep dive at debugging the workflow in this class.
- static private void debugTrace(final String format, final Object... args) {
- if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
- LogUtils.d(LOG_TAG, String.format(format, args));
- }
- }
-
- /**
- * This class is used to contain the details and state of a particular request to download
- * an attachment. These objects are constructed and either placed in the {@link DownloadQueue}
- * or in the in-progress map used to keep track of downloads that are currently happening
- * in the system
- */
- static class DownloadRequest {
- // Details of the request.
- final int mPriority;
- final long mCreatedTime;
- final long mAttachmentId;
- final long mMessageId;
- final long mAccountId;
-
- // Status of the request.
- boolean mInProgress = false;
- int mLastStatusCode;
- int mLastProgress;
- long mLastCallbackTime;
- long mStartTime;
- long mRetryCount;
- long mRetryStartTime;
-
- /**
- * This constructor is mainly used for tests
- * @param attPriority The priority of this attachment
- * @param attId The id of the row in the attachment table.
- */
- @VisibleForTesting
- DownloadRequest(final int attPriority, final long attId) {
- // This constructor should only be used for unit tests.
- mCreatedTime = SystemClock.elapsedRealtime();
- mPriority = attPriority;
- mAttachmentId = attId;
- mAccountId = -1;
- mMessageId = -1;
- }
-
- private DownloadRequest(final Context context, final Attachment attachment) {
- mAttachmentId = attachment.mId;
- final Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
- if (msg != null) {
- mAccountId = msg.mAccountKey;
- mMessageId = msg.mId;
- } else {
- mAccountId = mMessageId = -1;
- }
- mPriority = getAttachmentPriority(attachment);
- mCreatedTime = SystemClock.elapsedRealtime();
- }
-
- private DownloadRequest(final DownloadRequest orig, final long newTime) {
- mPriority = orig.mPriority;
- mAttachmentId = orig.mAttachmentId;
- mMessageId = orig.mMessageId;
- mAccountId = orig.mAccountId;
- mCreatedTime = newTime;
- mInProgress = orig.mInProgress;
- mLastStatusCode = orig.mLastStatusCode;
- mLastProgress = orig.mLastProgress;
- mLastCallbackTime = orig.mLastCallbackTime;
- mStartTime = orig.mStartTime;
- mRetryCount = orig.mRetryCount;
- mRetryStartTime = orig.mRetryStartTime;
- }
-
- @Override
- public int hashCode() {
- return (int)mAttachmentId;
- }
-
- /**
- * Two download requests are equals if their attachment id's are equals
- */
- @Override
- public boolean equals(final Object object) {
- if (!(object instanceof DownloadRequest)) return false;
- final DownloadRequest req = (DownloadRequest)object;
- return req.mAttachmentId == mAttachmentId;
- }
- }
-
- /**
- * This class is used to organize the various download requests that are pending.
- * We need a class that allows us to prioritize a collection of {@link DownloadRequest} objects
- * while being able to pull off request with the highest priority but we also need
- * to be able to find a particular {@link DownloadRequest} by id or by reference for retrieval.
- * Bonus points for an implementation that does not require an iterator to accomplish its tasks
- * as we can avoid pesky ConcurrentModificationException when one thread has the iterator
- * and another thread modifies the collection.
- */
- static class DownloadQueue {
- private final int DEFAULT_SIZE = 10;
-
- // For synchronization
- private final Object mLock = new Object();
-
- /**
- * 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)
- */
- private static class DownloadComparator implements Comparator<DownloadRequest> {
- @Override
- public int compare(DownloadRequest req1, DownloadRequest req2) {
- int res;
- if (req1.mPriority != req2.mPriority) {
- res = (req1.mPriority < req2.mPriority) ? -1 : 1;
- } else {
- if (req1.mCreatedTime == req2.mCreatedTime) {
- res = 0;
- } else {
- res = (req1.mCreatedTime < req2.mCreatedTime) ? -1 : 1;
- }
- }
- return res;
- }
- }
-
- // For prioritization of DownloadRequests.
- final PriorityQueue<DownloadRequest> mRequestQueue =
- new PriorityQueue<DownloadRequest>(DEFAULT_SIZE, new DownloadComparator());
-
- // Secondary collection to quickly find objects w/o the help of an iterator.
- // This class should be kept in lock step with the priority queue.
- final ConcurrentHashMap<Long, DownloadRequest> mRequestMap =
- new ConcurrentHashMap<Long, DownloadRequest>();
-
- /**
- * This function will add the request to our collections if it does not already
- * exist. If it does exist, the function will silently succeed.
- * @param request The {@link DownloadRequest} that should be added to our queue
- * @return true if it was added (or already exists), false otherwise
- */
- public boolean addRequest(final DownloadRequest request)
- throws NullPointerException {
- // It is key to keep the map and queue in lock step
- if (request == null) {
- // We can't add a null entry into the queue so let's throw what the underlying
- // data structure would throw.
- throw new NullPointerException();
- }
- final long requestId = request.mAttachmentId;
- if (requestId < 0) {
- // Invalid request
- LogUtils.d(LOG_TAG, "Not adding a DownloadRequest with an invalid attachment id");
- return false;
- }
- debugTrace("Queuing DownloadRequest #%d", requestId);
- synchronized (mLock) {
- // Check to see if this request is is already in the queue
- final boolean exists = mRequestMap.containsKey(requestId);
- if (!exists) {
- mRequestQueue.offer(request);
- mRequestMap.put(requestId, request);
- } else {
- debugTrace("DownloadRequest #%d was already in the queue");
- }
- }
- return true;
- }
-
- /**
- * This function will remove the specified request from the internal collections.
- * @param request The {@link DownloadRequest} that should be removed from our queue
- * @return true if it was removed or the request was invalid (meaning that the request
- * is not in our queue), false otherwise.
- */
- public boolean removeRequest(final DownloadRequest request) {
- if (request == null) {
- // If it is invalid, its not in the queue.
- return true;
- }
- debugTrace("Removing DownloadRequest #%d", request.mAttachmentId);
- final boolean result;
- synchronized (mLock) {
- // It is key to keep the map and queue in lock step
- result = mRequestQueue.remove(request);
- if (result) {
- mRequestMap.remove(request.mAttachmentId);
- }
- return result;
- }
- }
-
- /**
- * Return the next request from our queue.
- * @return The next {@link DownloadRequest} object or null if the queue is empty
- */
- public DownloadRequest getNextRequest() {
- // It is key to keep the map and queue in lock step
- final DownloadRequest returnRequest;
- synchronized (mLock) {
- returnRequest = mRequestQueue.poll();
- if (returnRequest != null) {
- final long requestId = returnRequest.mAttachmentId;
- mRequestMap.remove(requestId);
- }
- }
- if (returnRequest != null) {
- debugTrace("Retrieved DownloadRequest #%d", returnRequest.mAttachmentId);
- }
- return returnRequest;
- }
-
- /**
- * Return the {@link DownloadRequest} with the given ID (attachment ID)
- * @param requestId The ID of the request in question
- * @return The associated {@link DownloadRequest} object or null if it does not exist
- */
- public DownloadRequest findRequestById(final long requestId) {
- if (requestId < 0) {
- return null;
- }
- synchronized (mLock) {
- return mRequestMap.get(requestId);
- }
- }
-
- public int getSize() {
- synchronized (mLock) {
- return mRequestMap.size();
- }
- }
-
- public boolean isEmpty() {
- synchronized (mLock) {
- return mRequestMap.isEmpty();
- }
- }
- }
-
- /**
- * 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 AttachmentWatchdog extends BroadcastReceiver {
- // How often our watchdog checks for callback timeouts
- private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
- public static final String EXTRA_CALLBACK_TIMEOUT = "callback_timeout";
- private PendingIntent mWatchdogPendingIntent;
-
- public void setWatchdogAlarm(final Context context, final long delay,
- final int callbackTimeout) {
- // Lazily initialize the pending intent
- if (mWatchdogPendingIntent == null) {
- Intent intent = new Intent(context, AttachmentWatchdog.class);
- intent.putExtra(EXTRA_CALLBACK_TIMEOUT, callbackTimeout);
- mWatchdogPendingIntent =
- PendingIntent.getBroadcast(context, 0, intent, 0);
- }
- // Set the alarm
- final AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
- am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
- mWatchdogPendingIntent);
- debugTrace("Set up a watchdog for %d millis in the future", delay);
- }
-
- public void setWatchdogAlarm(final Context context) {
- // Call the real function with default values.
- setWatchdogAlarm(context, WATCHDOG_CHECK_INTERVAL, CALLBACK_TIMEOUT);
- }
-
- @Override
- public void onReceive(final Context context, final Intent intent) {
- final int callbackTimeout = intent.getIntExtra(EXTRA_CALLBACK_TIMEOUT,
- CALLBACK_TIMEOUT);
- new Thread(new Runnable() {
- @Override
- public void run() {
- // TODO: Really don't like hard coding the AttachmentService reference here
- // as it makes testing harder if we are trying to mock out the service
- // We should change this with some sort of getter that returns the
- // static (or test) AttachmentService instance to use.
- final AttachmentService service = AttachmentService.sRunningService;
- if (service != null) {
- // If our service instance is gone, just leave
- if (service.mStop) {
- return;
- }
- // Get the timeout time from the intent.
- watchdogAlarm(service, callbackTimeout);
- }
- }
- }, "AttachmentService AttachmentWatchdog").start();
- }
-
- boolean validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout,
- final long now) {
- // Check how long it's been since receiving a callback
- final long timeSinceCallback = now - dr.mLastCallbackTime;
- if (timeSinceCallback > callbackTimeout) {
- LogUtils.d(LOG_TAG, "Timeout for DownloadRequest #%d ", dr.mAttachmentId);
- return true;
- }
- return false;
- }
-
- /**
- * Watchdog for downloads; we use this in case we are hanging on a download, which might
- * have failed silently (the connection dropped, for example)
- */
- void watchdogAlarm(final AttachmentService service, final int callbackTimeout) {
- debugTrace("Received a timer callback in the watchdog");
-
- // We want to iterate on each of the downloads that are currently in progress and
- // cancel the ones that seem to be taking too long.
- final Collection<DownloadRequest> inProgressRequests =
- service.mDownloadsInProgress.values();
- for (DownloadRequest req: inProgressRequests) {
- debugTrace("Checking in-progress request with id: %d", req.mAttachmentId);
- final boolean shouldCancelDownload = validateDownloadRequest(req, callbackTimeout,
- System.currentTimeMillis());
- if (shouldCancelDownload) {
- LogUtils.w(LOG_TAG, "Cancelling DownloadRequest #%d", req.mAttachmentId);
- service.cancelDownload(req);
- // TODO: Should we also mark the attachment as failed at this point in time?
- }
- }
- // Check whether we can start new downloads...
- if (service.isConnected()) {
- service.processQueue();
- }
- issueNextWatchdogAlarm(service);
- }
-
- void issueNextWatchdogAlarm(final AttachmentService service) {
- if (!service.mDownloadsInProgress.isEmpty()) {
- debugTrace("Rescheduling watchdog...");
- setWatchdogAlarm(service);
- }
- }
- }
-
- /**
- * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks
- * come from either Controller (IMAP/POP) or ExchangeService (EAS). Note that we only
- * implement the single callback that's defined by the EmailServiceCallback interface.
- */
- class ServiceCallback extends IEmailServiceCallback.Stub {
-
- /**
- * Simple routine to generate updated status values for the Attachment based on the
- * service callback. Right now it is very simple but factoring out this code allows us
- * to test easier and very easy to expand in the future.
- */
- ContentValues getAttachmentUpdateValues(final Attachment attachment,
- final int statusCode, final int progress) {
- final ContentValues values = new ContentValues();
- if (attachment != null) {
- if (statusCode == EmailServiceStatus.IN_PROGRESS) {
- // TODO: What else do we want to expose about this in-progress download through
- // the provider? If there is more, make sure that the service implementation
- // reports it and make sure that we add it here.
- values.put(AttachmentColumns.UI_STATE, AttachmentState.DOWNLOADING);
- values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
- attachment.mSize * progress / 100);
- }
- }
- return values;
- }
-
- @Override
- public void loadAttachmentStatus(final long messageId, final long attachmentId,
- final int statusCode, final int progress) {
- debugTrace(LOG_TAG, "ServiceCallback for attachment #%d", attachmentId);
-
- // Record status and progress
- final DownloadRequest req = mDownloadsInProgress.get(attachmentId);
- if (req != null) {
- final long now = System.currentTimeMillis();
- debugTrace("ServiceCallback: status code changing from %d to %d",
- req.mLastStatusCode, statusCode);
- debugTrace("ServiceCallback: progress changing from %d to %d",
- req.mLastProgress,progress);
- debugTrace("ServiceCallback: last callback time changing from %d to %d",
- req.mLastCallbackTime, now);
-
- // Update some state to keep track of the progress of the download
- req.mLastStatusCode = statusCode;
- req.mLastProgress = progress;
- req.mLastCallbackTime = now;
-
- // Update the attachment status in the provider.
- final Attachment attachment =
- Attachment.restoreAttachmentWithId(AttachmentService.this, attachmentId);
- final ContentValues values = getAttachmentUpdateValues(attachment, statusCode,
- progress);
- if (values.size() > 0) {
- attachment.update(AttachmentService.this, values);
- }
-
- switch (statusCode) {
- case EmailServiceStatus.IN_PROGRESS:
- break;
- default:
- // It is assumed that any other error is either a success or an error
- // Either way, the final updates to the DownloadRequest and attachment
- // objects will be handed there.
- LogUtils.d(LOG_TAG, "Attachment #%d is done", attachmentId);
- endDownload(attachmentId, statusCode);
- break;
- }
- } else {
- // The only way that we can get a callback from the service implementation for
- // an attachment that doesn't exist is if it was cancelled due to the
- // AttachmentWatchdog. This is a valid scenario and the Watchdog should have already
- // marked this attachment as failed/cancelled.
- }
- }
- }
-
- /**
- * Called directly by EmailProvider whenever an attachment is inserted or changed. Since this
- * call is being invoked on the UI thread, we need to make sure that the downloads are
- * happening in the background.
- * @param context the caller's context
- * @param id the attachment's id
- * @param flags the new flags for the attachment
- */
- public static void attachmentChanged(final Context context, final long id, final int flags) {
- LogUtils.d(LOG_TAG, "Attachment with id: %d will potentially be queued for download", id);
- // Throw this info into an intent and send it to the attachment service.
- final Intent intent = new Intent(context, AttachmentService.class);
- debugTrace("Calling startService with extras %d & %d", id, flags);
- intent.putExtra(EXTRA_ATTACHMENT_ID, id);
- intent.putExtra(EXTRA_ATTACHMENT_FLAGS, flags);
- context.startService(intent);
- }
-
- /**
- * The main entry point for this service, the attachment to download can be identified
- * by the EXTRA_ATTACHMENT extra in the intent.
- */
- @Override
- public int onStartCommand(final Intent intent, final int flags, final int startId) {
- if (sRunningService == null) {
- sRunningService = this;
- }
- if (intent != null) {
- // Let's add this id/flags combo to the list of potential attachments to process.
- final long attachment_id = intent.getLongExtra(EXTRA_ATTACHMENT_ID, -1);
- final int attachment_flags = intent.getIntExtra(EXTRA_ATTACHMENT_FLAGS, -1);
- if ((attachment_id >= 0) && (attachment_flags >= 0)) {
- sAttachmentChangedQueue.add(new long[]{attachment_id, attachment_flags});
- // Process the queue if we're in a wait
- kick();
- } else {
- debugTrace("Received an invalid intent w/o the required extras %d & %d",
- attachment_id, attachment_flags);
- }
- } else {
- debugTrace("Received a null intent in onStartCommand");
- }
- return Service.START_STICKY;
- }
-
- /**
- * Most of the leg work is done by our service thread that is created when this
- * service is created.
- */
- @Override
- public void onCreate() {
- // Start up our service thread.
- new Thread(this, "AttachmentService").start();
- }
-
- @Override
- public IBinder onBind(final Intent intent) {
- return null;
- }
-
- @Override
- public void onDestroy() {
- debugTrace("Destroying AttachmentService object");
- dumpInProgressDownloads();
-
- // Mark this instance of the service as stopped. Our main loop for the AttachmentService
- // checks for this flag along with the AttachmentWatchdog.
- mStop = true;
- if (sRunningService != null) {
- // Kick it awake to get it to realize that we are stopping.
- kick();
- sRunningService = null;
- }
- if (mConnectivityManager != null) {
- mConnectivityManager.unregister();
- mConnectivityManager.stopWait();
- mConnectivityManager = null;
- }
- }
-
- /**
- * The main routine for our AttachmentService service thread.
- */
- @Override
- public void run() {
- // These fields are only used within the service thread
- mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG);
- mAccountManagerStub = new AccountManagerStub(this);
-
- // Run through all attachments in the database that require download and add them to
- // the queue. This is the case where a previous AttachmentService may have been notified
- // to stop before processing everything in its queue.
- final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
- final Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
- EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
- new String[] {Integer.toString(mask)}, null);
- try {
- LogUtils.d(LOG_TAG,
- "Count of previous downloads to resume (from db): %d", c.getCount());
- while (c.moveToNext()) {
- final Attachment attachment = Attachment.restoreAttachmentWithId(
- this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
- if (attachment != null) {
- debugTrace("Attempting to download attachment #%d again.", attachment.mId);
- onChange(this, attachment);
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- c.close();
- }
-
- // Loop until stopped, with a 30 minute wait loop
- while (!mStop) {
- // Here's where we run our attachment loading logic...
- // Make a local copy of the variable so we don't null-crash on service shutdown
- final EmailConnectivityManager ecm = mConnectivityManager;
- if (ecm != null) {
- ecm.waitForConnectivity();
- }
- if (mStop) {
- // We might be bailing out here due to the service shutting down
- LogUtils.d(LOG_TAG, "AttachmentService has been instructed to stop");
- break;
- }
-
- // In advanced debug mode, let's look at the state of all in-progress downloads
- // after processQueue() runs.
- debugTrace("Downloads Map before processQueue");
- dumpInProgressDownloads();
- processQueue();
- debugTrace("Downloads Map after processQueue");
- dumpInProgressDownloads();
-
- if (mDownloadQueue.isEmpty() && (mDownloadsInProgress.size() < 1)) {
- LogUtils.d(LOG_TAG, "Shutting down service. No in-progress or pending downloads.");
- stopSelf();
- break;
- }
- debugTrace("Run() wait for mLock");
- synchronized(mLock) {
- try {
- mLock.wait(PROCESS_QUEUE_WAIT_TIME);
- } catch (InterruptedException e) {
- // That's ok; we'll just keep looping
- }
- }
- debugTrace("Run() got mLock");
- }
-
- // Unregister now that we're done
- // Make a local copy of the variable so we don't null-crash on service shutdown
- final EmailConnectivityManager ecm = mConnectivityManager;
- if (ecm != null) {
- ecm.unregister();
- }
- }
-
- /*
- * Function that kicks the service into action as it may be waiting for this object
- * as it processed the last round of attachments.
- */
- private void kick() {
- synchronized(mLock) {
- mLock.notify();
- }
- }
-
- /**
- * 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(final Context context, final Attachment att) {
- debugTrace("onChange() for Attachment: #%d", att.mId);
- DownloadRequest req = mDownloadQueue.findRequestById(att.mId);
- final long priority = getAttachmentPriority(att);
- if (priority == PRIORITY_NONE) {
- LogUtils.d(LOG_TAG, "Attachment #%d has no priority and will not be downloaded",
- 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
- mDownloadQueue.removeRequest(req);
- }
- } else {
- // Ignore changes that occur during download
- if (mDownloadsInProgress.containsKey(att.mId)) {
- debugTrace("Attachment #%d was already in the queue", att.mId);
- return;
- }
- // If this is new, add the request to the queue
- if (req == null) {
- LogUtils.d(LOG_TAG, "Attachment #%d is a new download request", att.mId);
- req = new DownloadRequest(context, att);
- final AttachmentInfo attachInfo = new AttachmentInfo(context, att);
- if (!attachInfo.isEligibleForDownload()) {
- LogUtils.w(LOG_TAG, "Attachment #%d is not eligible for download", att.mId);
- // 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)) {
- LogUtils.w(LOG_TAG, "Attachment #%d cannot be downloaded ever", att.mId);
- // 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 explicitly 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.
- }
- mDownloadQueue.addRequest(req);
- }
- // TODO: If the request already existed, we'll update the priority (so that the time is
- // up-to-date); otherwise, create a new request
- LogUtils.d(LOG_TAG,
- "Attachment #%d queued for download, priority: %d, created time: %d",
- att.mId, req.mPriority, req.mCreatedTime);
- }
- // Process the queue if we're in a wait
- kick();
- }
-
- /**
- * Set the bits in the provider to mark this download as failed.
- * @param att The attachment that failed to download.
- */
- void markAttachmentAsFailed(final Attachment att) {
- final ContentValues cv = new ContentValues();
- final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
- cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
- cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
- att.update(this, cv);
- }
-
- /**
- * Set the bits in the provider to mark this download as completed.
- * @param att The attachment that was downloaded.
- */
- void markAttachmentAsCompleted(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.SAVED);
- att.update(this, cv);
- }
-
- /**
- * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
- * the limit on maximum downloads
- */
- synchronized void processQueue() {
- debugTrace("Processing changed queue, num entries: %d", sAttachmentChangedQueue.size());
-
- // First thing we need to do is process the list of "potential downloads" that we
- // added to sAttachmentChangedQueue
- long[] change = sAttachmentChangedQueue.poll();
- while (change != null) {
- // Process this change
- final long id = change[0];
- final long flags = change[1];
- final Attachment attachment = Attachment.restoreAttachmentWithId(this, id);
- if (attachment == null) {
- LogUtils.w(LOG_TAG, "Could not restore attachment #%d", id);
- continue;
- }
- attachment.mFlags = (int) flags;
- onChange(this, attachment);
- change = sAttachmentChangedQueue.poll();
- }
-
- debugTrace("Processing download queue, num entries: %d", mDownloadQueue.getSize());
-
- while (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS) {
- final DownloadRequest req = mDownloadQueue.getNextRequest();
- if (req == null) {
- // No more queued requests? We are done for now.
- break;
- }
- // Enforce per-account limit here
- if (getDownloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
- LogUtils.w(LOG_TAG, "Skipping #%d; maxed for acct %d",
- req.mAttachmentId, req.mAccountId);
- continue;
- }
- if (Attachment.restoreAttachmentWithId(this, req.mAttachmentId) == null) {
- LogUtils.e(LOG_TAG, "Could not load attachment: #%d", req.mAttachmentId);
- continue;
- }
- if (!req.mInProgress) {
- final long currentTime = SystemClock.elapsedRealtime();
- if (req.mRetryCount > 0 && req.mRetryStartTime > currentTime) {
- debugTrace("Need to wait before retrying attachment #%d", req.mAttachmentId);
- mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
- CALLBACK_TIMEOUT);
- 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.
- tryStartDownload(req);
- }
- }
-
- // Check our ability to be opportunistic regarding background downloads.
- final EmailConnectivityManager ecm = mConnectivityManager;
- if ((ecm == null) || !ecm.isAutoSyncAllowed() ||
- (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI)) {
- // Only prefetch if it if connectivity is available, prefetch is enabled
- // and we are on WIFI
- LogUtils.d(LOG_TAG, "Skipping opportunistic downloads since WIFI is not available");
- return;
- }
-
- // Then, try opportunistic download of appropriate attachments
- final int availableBackgroundThreads =
- MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size() - 1;
- if (availableBackgroundThreads < 1) {
- // We want to leave one spot open for a user requested download that we haven't
- // started processing yet.
- LogUtils.d(LOG_TAG, "Skipping opportunistic downloads, %d threads available",
- availableBackgroundThreads);
- return;
- }
-
- debugTrace("Launching up to %d opportunistic downloads", availableBackgroundThreads);
-
- // We'll load up the newest 25 attachments that aren't loaded or queued
- // TODO: We are always looking for MAX_ATTACHMENTS_TO_CHECK, shouldn't this be
- // backgroundDownloads instead? We should fix and test this.
- final Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
- MAX_ATTACHMENTS_TO_CHECK);
- final Cursor c = this.getContentResolver().query(lookupUri,
- Attachment.CONTENT_PROJECTION,
- EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
- null, AttachmentColumns._ID + " DESC");
- File cacheDir = this.getCacheDir();
- try {
- while (c.moveToNext()) {
- final Attachment att = new Attachment();
- att.restore(c);
- final Account account = Account.restoreAccountWithId(this, att.mAccountKey);
- if (account == null) {
- // Clean up this orphaned attachment; there's no point in keeping it
- // around; then try to find another one
- debugTrace("Found orphaned attachment #%d", att.mId);
- EmailContent.delete(this, Attachment.CONTENT_URI, att.mId);
- } else {
- // Check that the attachment meets system requirements for download
- // Note that there couple be policy that does not allow this attachment
- // to be downloaded.
- final AttachmentInfo info = new AttachmentInfo(this, 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)) {
- final Integer tryCount = mAttachmentFailureMap.get(att.mId);
- if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
- // move onto the next attachment
- LogUtils.w(LOG_TAG,
- "Too many failed attempts for attachment #%d ", att.mId);
- continue;
- }
- // Start this download and we're done
- final DownloadRequest req = new DownloadRequest(this, att);
- 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.w(LOG_TAG, "Skipping attachment #%d, it is ineligible", att.mId);
- }
- }
- }
- } finally {
- c.close();
- }
- }
-
- /**
- * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
- * parameter
- * @param req the DownloadRequest
- * @return whether or not the download was started
- */
- synchronized boolean tryStartDownload(final DownloadRequest req) {
- final EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
- AttachmentService.this, req.mAccountId);
-
- // Do not download the same attachment multiple times
- boolean alreadyInProgress = mDownloadsInProgress.get(req.mAttachmentId) != null;
- if (alreadyInProgress) {
- debugTrace("This attachment #%d is already in progress", req.mAttachmentId);
- return false;
- }
-
- try {
- 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;
- }
-
- /**
- * 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(final EmailServiceProxy service, final DownloadRequest req)
- throws RemoteException {
- LogUtils.d(LOG_TAG, "Starting download for Attachment #%d", req.mAttachmentId);
- req.mStartTime = System.currentTimeMillis();
- req.mInProgress = true;
- mDownloadsInProgress.put(req.mAttachmentId, req);
- service.loadAttachment(mServiceCallback, req.mAccountId, req.mAttachmentId,
- req.mPriority != PRIORITY_FOREGROUND);
- mWatchdog.setWatchdogAlarm(this);
- }
-
- synchronized void cancelDownload(final DownloadRequest req) {
- LogUtils.d(LOG_TAG, "Cancelling download for Attachment #%d", req.mAttachmentId);
- req.mInProgress = false;
- mDownloadsInProgress.remove(req.mAttachmentId);
- // Remove the download from our queue, and then decide whether or not to add it back.
- mDownloadQueue.removeRequest(req);
- req.mRetryCount++;
- if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
- LogUtils.w(LOG_TAG, "Too many failures giving up on Attachment #%d", req.mAttachmentId);
- } else {
- debugTrace("Moving to end of queue, will retry #%d", req.mAttachmentId);
- // 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.
- final DownloadRequest newReq = new DownloadRequest(req, SystemClock.elapsedRealtime());
- mDownloadQueue.addRequest(newReq);
- }
- }
-
- /**
- * 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
- */
- synchronized void endDownload(final long attachmentId, final int statusCode) {
- LogUtils.d(LOG_TAG, "Finishing download #%d", attachmentId);
-
- // 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;
- LogUtils.w(LOG_TAG, "This attachment failed, adding #%d to failure map", attachmentId);
- mAttachmentFailureMap.put(attachmentId, downloadCount);
- }
-
- final DownloadRequest req = mDownloadQueue.findRequestById(attachmentId);
- if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
- // If this needs to be retried, just process the queue again
- if (req != null) {
- req.mRetryCount++;
- if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
- // We are done, we maxed out our total number of tries.
- // Not that we do not flag this attachment with any special flags so the
- // AttachmentService will try to download this attachment again the next time
- // that it starts up.
- LogUtils.w(LOG_TAG, "Too many tried for connection errors, giving up #%d",
- attachmentId);
- mDownloadQueue.removeRequest(req);
- // Note that we are not doing anything with the attachment right now
- // We will annotate it later in this function if needed.
- } else if (req.mRetryCount > 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.w(LOG_TAG, "ConnectionError #%d, retried %d times, adding delay",
- attachmentId, req.mRetryCount);
- req.mInProgress = false;
- req.mRetryStartTime = SystemClock.elapsedRealtime() +
- CONNECTION_ERROR_RETRY_MILLIS;
- mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
- CALLBACK_TIMEOUT);
- } else {
- LogUtils.w(LOG_TAG, "ConnectionError for #%d, retried %d times, adding delay",
- attachmentId, req.mRetryCount);
- req.mInProgress = false;
- req.mRetryStartTime = 0;
- kick();
- }
- }
- return;
- }
-
- // If the request is still in the queue, remove it
- if (req != null) {
- mDownloadQueue.removeRequest(req);
- }
-
- if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
- long secs = 0;
- if (req != null) {
- secs = (System.currentTimeMillis() - req.mCreatedTime) / 1000;
- }
- final String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
- "Error " + statusCode;
- debugTrace("Download finished for attachment #%d; %d seconds from request, status: %s",
- attachmentId, secs, status);
- }
-
- final Attachment attachment = Attachment.restoreAttachmentWithId(this, attachmentId);
- if (attachment != null) {
- final 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(this, Attachment.CONTENT_URI, attachment.mId);
- // TODO: Talk to UX about whether this is even worth doing
- NotificationController nc = NotificationController.getInstance(this);
- nc.showDownloadForwardFailedNotificationSynchronous(attachment);
- deleted = true;
- LogUtils.w(LOG_TAG, "Deleting forwarded attachment #%d for message #%d",
- attachmentId, attachment.mMessageKey);
- }
- // 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(this, attachment.mMessageKey)) {
- debugTrace("Downloads finished for outgoing msg #%d", req.mMessageId);
- EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
- this, accountId);
- try {
- service.sendMail(accountId);
- } catch (RemoteException e) {
- LogUtils.e(LOG_TAG, "RemoteException while trying to send message: #%d, %s",
- req.mMessageId, e.toString());
- }
- }
- }
- if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
- Message msg = Message.restoreMessageWithId(this, attachment.mMessageKey);
- if (msg == null) {
- LogUtils.w(LOG_TAG, "Deleting attachment #%d with no associated message #%d",
- attachment.mId, attachment.mMessageKey);
- // If there's no associated message, delete the attachment
- EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
- } else {
- // If there really is a message, retry
- // TODO: How will this get retried? It's still marked as inProgress?
- LogUtils.w(LOG_TAG, "Retrying attachment #%d with associated message #%d",
- attachment.mId, attachment.mMessageKey);
- 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
- LogUtils.d(LOG_TAG, "Attachment #%d successfully downloaded!", attachment.mId);
- markAttachmentAsCompleted(attachment);
- }
- }
- // Process the queue
- kick();
- }
-
- /**
- * Count the number of running downloads in progress for this account
- * @param accountId the id of the account
- * @return the count of running downloads
- */
- synchronized int getDownloadsForAccount(final long accountId) {
- int count = 0;
- for (final DownloadRequest req: mDownloadsInProgress.values()) {
- if (req.mAccountId == accountId) {
- count++;
- }
- }
- return count;
- }
-
- /**
- * Calculate the download priority of an Attachment. A priority of zero means that the
- * attachment is not marked for download.
- * @param att the Attachment
- * @return the priority key of the Attachment
- */
- private static int getAttachmentPriority(final Attachment att) {
- int priorityClass = PRIORITY_NONE;
- final 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;
- }
-
- /**
- * Determine whether an attachment can be prefetched for the given account based on
- * total download size restrictions tied to the account.
- * @return true if download is allowed, false otherwise
- */
- public boolean canPrefetchForAccount(final Account account, final 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) {
- debugTrace("Prefetch is not allowed for this account: %d", account.getId());
- return false;
- }
-
- final long totalStorage = dir.getTotalSpace();
- final long usableStorage = dir.getUsableSpace();
- final long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
-
- // If there's not enough overall storage available, stop now
- if (usableStorage < minAvailable) {
- debugTrace("Not enough physical storage for prefetch");
- return false;
- }
-
- final int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
- // Calculate an even per-account storage although it would make a lot of sense to not
- // do this as you may assign more storage to your corporate account rather than a personal
- // account.
- final long perAccountMaxStorage =
- (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
-
- // 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. No locking here since this is a concurrent collection object.
- mAttachmentStorageMap.put(account.mId, accountStorage);
- }
-
- // Return true if we're using less than the maximum per account
- if (accountStorage >= perAccountMaxStorage) {
- debugTrace("Prefetch not allowed for account %d; used: %d, limit %d",
- account.mId, accountStorage, perAccountMaxStorage);
- return false;
- }
- return true;
- }
-
- boolean isConnected() {
- if (mConnectivityManager != null) {
- return mConnectivityManager.hasConnectivity();
- }
- return false;
- }
-
- // For Debugging.
- synchronized public void dumpInProgressDownloads() {
- if (ENABLE_ATTACHMENT_SERVICE_DEBUG < 1) {
- LogUtils.d(LOG_TAG, "Advanced logging not configured.");
- }
- for (final DownloadRequest req : mDownloadsInProgress.values()) {
- LogUtils.d(LOG_TAG, "--BEGIN DownloadRequest DUMP--");
- LogUtils.d(LOG_TAG, "Account: #%d", req.mAccountId);
- LogUtils.d(LOG_TAG, "Message: #%d", req.mMessageId);
- LogUtils.d(LOG_TAG, "Attachment: #%d", req.mAttachmentId);
- LogUtils.d(LOG_TAG, "Created Time: %d", req.mCreatedTime);
- LogUtils.d(LOG_TAG, "Priority: %d", req.mPriority);
- if (req.mInProgress == true) {
- LogUtils.d(LOG_TAG, "This download is in progress");
- } else {
- LogUtils.d(LOG_TAG, "This download is not in progress");
- }
- LogUtils.d(LOG_TAG, "Start Time: %d", req.mStartTime);
- LogUtils.d(LOG_TAG, "Retry Count: %d", req.mRetryCount);
- LogUtils.d(LOG_TAG, "Retry Start Tiome: %d", req.mRetryStartTime);
- LogUtils.d(LOG_TAG, "Last Status Code: %d", req.mLastStatusCode);
- LogUtils.d(LOG_TAG, "Last Progress: %d", req.mLastProgress);
- LogUtils.d(LOG_TAG, "Last Callback Time: %d", req.mLastCallbackTime);
- LogUtils.d(LOG_TAG, "------------------------------");
- }
- }
-
-
- @Override
- public void dump(final FileDescriptor fd, final PrintWriter pw, final String[] args) {
- pw.println("AttachmentService");
- final long time = System.currentTimeMillis();
- synchronized(mDownloadQueue) {
- pw.println(" Queue, " + mDownloadQueue.getSize() + " entries");
- // If you iterate over the queue either via iterator or collection, they are not
- // returned in any particular order. With all things being equal its better to go with
- // a collection to avoid any potential ConcurrentModificationExceptions.
- // If we really want this sorted, we can sort it manually since performance isn't a big
- // concern with this debug method.
- for (final DownloadRequest req : mDownloadQueue.mRequestMap.values()) {
- pw.println(" Account: " + req.mAccountId + ", Attachment: " + req.mAttachmentId);
- pw.println(" Priority: " + req.mPriority + ", Time: " + req.mCreatedTime +
- (req.mInProgress ? " [In progress]" : ""));
- final Attachment att = Attachment.restoreAttachmentWithId(this, req.mAttachmentId);
- if (att == null) {
- pw.println(" Attachment not in database?");
- } else if (att.mFileName != null) {
- final String fileName = att.mFileName;
- final String suffix;
- final int lastDot = fileName.lastIndexOf('.');
- if (lastDot >= 0) {
- suffix = fileName.substring(lastDot);
- } else {
- suffix = "[none]";
- }
- 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.mInProgress) {
- pw.println(" Status: " + req.mLastStatusCode + ", Progress: " +
- req.mLastProgress);
- pw.println(" Started: " + req.mStartTime + ", Callback: " +
- req.mLastCallbackTime);
- pw.println(" Elapsed: " + ((time - req.mStartTime) / 1000L) + "s");
- if (req.mLastCallbackTime > 0) {
- pw.println(" CB: " + ((time - req.mLastCallbackTime) / 1000L) + "s");
- }
- }
- }
- }
- }
-
- // For Testing
- AccountManagerStub mAccountManagerStub;
- private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
-
- void addServiceIntentForTest(final long accountId, final Intent intent) {
- mAccountServiceMap.put(accountId, intent);
- }
-
- /**
- * 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
- */
- static class AccountManagerStub {
- private int mNumberOfAccounts;
- private final AccountManager mAccountManager;
-
- AccountManagerStub(final Context context) {
- if (context != null) {
- mAccountManager = AccountManager.get(context);
- } else {
- mAccountManager = null;
- }
- }
-
- int getNumberOfAccounts() {
- if (mAccountManager != null) {
- return mAccountManager.getAccounts().length;
- } else {
- return mNumberOfAccounts;
- }
- }
-
- void setNumberOfAccounts(final int numberOfAccounts) {
- mNumberOfAccounts = numberOfAccounts;
- }
- }
-}