diff options
Diffstat (limited to 'src/com/android/email/service/AttachmentService.java')
-rw-r--r-- | src/com/android/email/service/AttachmentService.java | 1401 |
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; - } - } -} |