1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.service;
18
19import android.accounts.AccountManager;
20import android.app.AlarmManager;
21import android.app.PendingIntent;
22import android.app.Service;
23import android.content.BroadcastReceiver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.database.Cursor;
28import android.net.ConnectivityManager;
29import android.net.Uri;
30import android.os.IBinder;
31import android.os.RemoteException;
32import android.os.SystemClock;
33import android.text.format.DateUtils;
34
35import com.android.email.AttachmentInfo;
36import com.android.email.EmailConnectivityManager;
37import com.android.email.NotificationControllerCreatorHolder;
38import com.android.email.NotificationController;
39import com.android.emailcommon.provider.Account;
40import com.android.emailcommon.provider.EmailContent;
41import com.android.emailcommon.provider.EmailContent.Attachment;
42import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
43import com.android.emailcommon.provider.EmailContent.Message;
44import com.android.emailcommon.service.EmailServiceProxy;
45import com.android.emailcommon.service.EmailServiceStatus;
46import com.android.emailcommon.service.IEmailServiceCallback;
47import com.android.emailcommon.utility.AttachmentUtilities;
48import com.android.emailcommon.utility.Utility;
49import com.android.mail.providers.UIProvider.AttachmentState;
50import com.android.mail.utils.LogUtils;
51import com.google.common.annotations.VisibleForTesting;
52
53import java.io.File;
54import java.io.FileDescriptor;
55import java.io.PrintWriter;
56import java.util.Collection;
57import java.util.Comparator;
58import java.util.HashMap;
59import java.util.PriorityQueue;
60import java.util.Queue;
61import java.util.concurrent.ConcurrentHashMap;
62import java.util.concurrent.ConcurrentLinkedQueue;
63
64public class AttachmentService extends Service implements Runnable {
65    // For logging.
66    public static final String LOG_TAG = "AttachmentService";
67
68    // STOPSHIP Set this to 0 before shipping.
69    private static final int ENABLE_ATTACHMENT_SERVICE_DEBUG = 0;
70
71    // Minimum wait time before retrying a download that failed due to connection error
72    private static final long CONNECTION_ERROR_RETRY_MILLIS = 10 * DateUtils.SECOND_IN_MILLIS;
73    // Number of retries before we start delaying between
74    private static final long CONNECTION_ERROR_DELAY_THRESHOLD = 5;
75    // Maximum time to retry for connection errors.
76    private static final long CONNECTION_ERROR_MAX_RETRIES = 10;
77
78    // Our idle time, waiting for notifications; this is something of a failsafe
79    private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS);
80    // How long we'll wait for a callback before canceling a download and retrying
81    private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS);
82    // Try to download an attachment in the background this many times before giving up
83    private static final int MAX_DOWNLOAD_RETRIES = 5;
84
85    static final int PRIORITY_NONE = -1;
86    // High priority is for user requests
87    static final int PRIORITY_FOREGROUND = 0;
88    static final int PRIORITY_HIGHEST = PRIORITY_FOREGROUND;
89    // Normal priority is for forwarded downloads in outgoing mail
90    static final int PRIORITY_SEND_MAIL = 1;
91    // Low priority will be used for opportunistic downloads
92    static final int PRIORITY_BACKGROUND = 2;
93    static final int PRIORITY_LOWEST = PRIORITY_BACKGROUND;
94
95    // Minimum free storage in order to perform prefetch (25% of total memory)
96    private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F;
97    // Maximum prefetch storage (also 25% of total memory)
98    private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F;
99
100    // We can try various values here; I think 2 is completely reasonable as a first pass
101    private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2;
102    // Limit on the number of simultaneous downloads per account
103    // Note that a limit of 1 is currently enforced by both Services (MailService and Controller)
104    private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1;
105    // Limit on the number of attachments we'll check for background download
106    private static final int MAX_ATTACHMENTS_TO_CHECK = 25;
107
108    private static final String EXTRA_ATTACHMENT_ID =
109            "com.android.email.AttachmentService.attachment_id";
110    private static final String EXTRA_ATTACHMENT_FLAGS =
111            "com.android.email.AttachmentService.attachment_flags";
112
113    // This callback is invoked by the various service implementations to give us download progress
114    // since those modules are responsible for the actual download.
115    final ServiceCallback mServiceCallback = new ServiceCallback();
116
117    // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed
118    // by the use of "volatile"
119    static volatile AttachmentService sRunningService = null;
120
121    // Signify that we are being shut down & destroyed.
122    private volatile boolean mStop = false;
123
124    EmailConnectivityManager mConnectivityManager;
125
126    // Helper class that keeps track of in progress downloads to make sure that they
127    // are progressing well.
128    final AttachmentWatchdog mWatchdog = new AttachmentWatchdog();
129
130    private final Object mLock = new Object();
131
132    // A map of attachment storage used per account as we have account based maximums to follow.
133    // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated
134    // amount plus the size of any new attachments loaded).  If and when we reach the per-account
135    // limit, we recalculate the actual usage
136    final ConcurrentHashMap<Long, Long> mAttachmentStorageMap = new ConcurrentHashMap<Long, Long>();
137
138    // A map of attachment ids to the number of failed attempts to download the attachment
139    // NOTE: We do not want to persist this. This allows us to retry background downloading
140    // if any transient network errors are fixed and the app is restarted
141    final ConcurrentHashMap<Long, Integer> mAttachmentFailureMap =
142            new ConcurrentHashMap<Long, Integer>();
143
144    // Keeps tracks of downloads in progress based on an attachment ID to DownloadRequest mapping.
145    final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress =
146            new ConcurrentHashMap<Long, DownloadRequest>();
147
148    final DownloadQueue mDownloadQueue = new DownloadQueue();
149
150    // The queue entries here are entries of the form {id, flags}, with the values passed in to
151    // attachmentChanged(). Entries in the queue are picked off in processQueue().
152    private static final Queue<long[]> sAttachmentChangedQueue =
153            new ConcurrentLinkedQueue<long[]>();
154
155    // Extra layer of control over debug logging that should only be enabled when
156    // we need to take an extra deep dive at debugging the workflow in this class.
157    static private void debugTrace(final String format, final Object... args) {
158        if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
159            LogUtils.d(LOG_TAG, String.format(format, args));
160        }
161    }
162
163    /**
164     * This class is used to contain the details and state of a particular request to download
165     * an attachment. These objects are constructed and either placed in the {@link DownloadQueue}
166     * or in the in-progress map used to keep track of downloads that are currently happening
167     * in the system
168     */
169    static class DownloadRequest {
170        // Details of the request.
171        final int mPriority;
172        final long mCreatedTime;
173        final long mAttachmentId;
174        final long mMessageId;
175        final long mAccountId;
176
177        // Status of the request.
178        boolean mInProgress = false;
179        int mLastStatusCode;
180        int mLastProgress;
181        long mLastCallbackTime;
182        long mStartTime;
183        long mRetryCount;
184        long mRetryStartTime;
185
186        /**
187         * This constructor is mainly used for tests
188         * @param attPriority The priority of this attachment
189         * @param attId The id of the row in the attachment table.
190         */
191        @VisibleForTesting
192        DownloadRequest(final int attPriority, final long attId) {
193            // This constructor should only be used for unit tests.
194            mCreatedTime = SystemClock.elapsedRealtime();
195            mPriority = attPriority;
196            mAttachmentId = attId;
197            mAccountId = -1;
198            mMessageId = -1;
199        }
200
201        private DownloadRequest(final Context context, final Attachment attachment) {
202            mAttachmentId = attachment.mId;
203            final Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey);
204            if (msg != null) {
205                mAccountId = msg.mAccountKey;
206                mMessageId = msg.mId;
207            } else {
208                mAccountId = mMessageId = -1;
209            }
210            mPriority = getAttachmentPriority(attachment);
211            mCreatedTime = SystemClock.elapsedRealtime();
212        }
213
214        private DownloadRequest(final DownloadRequest orig, final long newTime) {
215            mPriority = orig.mPriority;
216            mAttachmentId = orig.mAttachmentId;
217            mMessageId = orig.mMessageId;
218            mAccountId = orig.mAccountId;
219            mCreatedTime = newTime;
220            mInProgress = orig.mInProgress;
221            mLastStatusCode = orig.mLastStatusCode;
222            mLastProgress = orig.mLastProgress;
223            mLastCallbackTime = orig.mLastCallbackTime;
224            mStartTime = orig.mStartTime;
225            mRetryCount = orig.mRetryCount;
226            mRetryStartTime = orig.mRetryStartTime;
227        }
228
229        @Override
230        public int hashCode() {
231            return (int)mAttachmentId;
232        }
233
234        /**
235         * Two download requests are equals if their attachment id's are equals
236         */
237        @Override
238        public boolean equals(final Object object) {
239            if (!(object instanceof DownloadRequest)) return false;
240            final DownloadRequest req = (DownloadRequest)object;
241            return req.mAttachmentId == mAttachmentId;
242        }
243    }
244
245    /**
246     * This class is used to organize the various download requests that are pending.
247     * We need a class that allows us to prioritize a collection of {@link DownloadRequest} objects
248     * while being able to pull off request with the highest priority but we also need
249     * to be able to find a particular {@link DownloadRequest} by id or by reference for retrieval.
250     * Bonus points for an implementation that does not require an iterator to accomplish its tasks
251     * as we can avoid pesky ConcurrentModificationException when one thread has the iterator
252     * and another thread modifies the collection.
253     */
254    static class DownloadQueue {
255        private final int DEFAULT_SIZE = 10;
256
257        // For synchronization
258        private final Object mLock = new Object();
259
260        /**
261         * Comparator class for the download set; we first compare by priority.  Requests with equal
262         * priority are compared by the time the request was created (older requests come first)
263         */
264        private static class DownloadComparator implements Comparator<DownloadRequest> {
265            @Override
266            public int compare(DownloadRequest req1, DownloadRequest req2) {
267                int res;
268                if (req1.mPriority != req2.mPriority) {
269                    res = (req1.mPriority < req2.mPriority) ? -1 : 1;
270                } else {
271                    if (req1.mCreatedTime == req2.mCreatedTime) {
272                        res = 0;
273                    } else {
274                        res = (req1.mCreatedTime < req2.mCreatedTime) ? -1 : 1;
275                    }
276                }
277                return res;
278            }
279        }
280
281        // For prioritization of DownloadRequests.
282        final PriorityQueue<DownloadRequest> mRequestQueue =
283                new PriorityQueue<DownloadRequest>(DEFAULT_SIZE, new DownloadComparator());
284
285        // Secondary collection to quickly find objects w/o the help of an iterator.
286        // This class should be kept in lock step with the priority queue.
287        final ConcurrentHashMap<Long, DownloadRequest> mRequestMap =
288                new ConcurrentHashMap<Long, DownloadRequest>();
289
290        /**
291         * This function will add the request to our collections if it does not already
292         * exist. If it does exist, the function will silently succeed.
293         * @param request The {@link DownloadRequest} that should be added to our queue
294         * @return true if it was added (or already exists), false otherwise
295         */
296        public boolean addRequest(final DownloadRequest request)
297                throws NullPointerException {
298            // It is key to keep the map and queue in lock step
299            if (request == null) {
300                // We can't add a null entry into the queue so let's throw what the underlying
301                // data structure would throw.
302                throw new NullPointerException();
303            }
304            final long requestId = request.mAttachmentId;
305            if (requestId < 0) {
306                // Invalid request
307                LogUtils.d(LOG_TAG, "Not adding a DownloadRequest with an invalid attachment id");
308                return false;
309            }
310            debugTrace("Queuing DownloadRequest #%d", requestId);
311            synchronized (mLock) {
312                // Check to see if this request is is already in the queue
313                final boolean exists = mRequestMap.containsKey(requestId);
314                if (!exists) {
315                    mRequestQueue.offer(request);
316                    mRequestMap.put(requestId, request);
317                } else {
318                    debugTrace("DownloadRequest #%d was already in the queue");
319                }
320            }
321            return true;
322        }
323
324        /**
325         * This function will remove the specified request from the internal collections.
326         * @param request The {@link DownloadRequest} that should be removed from our queue
327         * @return true if it was removed or the request was invalid (meaning that the request
328         * is not in our queue), false otherwise.
329         */
330        public boolean removeRequest(final DownloadRequest request) {
331            if (request == null) {
332                // If it is invalid, its not in the queue.
333                return true;
334            }
335            debugTrace("Removing DownloadRequest #%d", request.mAttachmentId);
336            final boolean result;
337            synchronized (mLock) {
338                // It is key to keep the map and queue in lock step
339                result = mRequestQueue.remove(request);
340                if (result) {
341                    mRequestMap.remove(request.mAttachmentId);
342                }
343                return result;
344            }
345        }
346
347        /**
348         * Return the next request from our queue.
349         * @return The next {@link DownloadRequest} object or null if the queue is empty
350         */
351        public DownloadRequest getNextRequest() {
352            // It is key to keep the map and queue in lock step
353            final DownloadRequest returnRequest;
354            synchronized (mLock) {
355                returnRequest = mRequestQueue.poll();
356                if (returnRequest != null) {
357                    final long requestId = returnRequest.mAttachmentId;
358                    mRequestMap.remove(requestId);
359                }
360            }
361            if (returnRequest != null) {
362                debugTrace("Retrieved DownloadRequest #%d", returnRequest.mAttachmentId);
363            }
364            return returnRequest;
365        }
366
367        /**
368         * Return the {@link DownloadRequest} with the given ID (attachment ID)
369         * @param requestId The ID of the request in question
370         * @return The associated {@link DownloadRequest} object or null if it does not exist
371         */
372        public DownloadRequest findRequestById(final long requestId) {
373            if (requestId < 0) {
374                return null;
375            }
376            synchronized (mLock) {
377                return mRequestMap.get(requestId);
378            }
379        }
380
381        public int getSize() {
382            synchronized (mLock) {
383                return mRequestMap.size();
384            }
385        }
386
387        public boolean isEmpty() {
388            synchronized (mLock) {
389                return mRequestMap.isEmpty();
390            }
391        }
392    }
393
394    /**
395     * Watchdog alarm receiver; responsible for making sure that downloads in progress are not
396     * stalled, as determined by the timing of the most recent service callback
397     */
398    public static class AttachmentWatchdog extends BroadcastReceiver {
399        // How often our watchdog checks for callback timeouts
400        private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS);
401        public static final String EXTRA_CALLBACK_TIMEOUT = "callback_timeout";
402        private PendingIntent mWatchdogPendingIntent;
403
404        public void setWatchdogAlarm(final Context context, final long delay,
405                final int callbackTimeout) {
406            // Lazily initialize the pending intent
407            if (mWatchdogPendingIntent == null) {
408                Intent intent = new Intent(context, AttachmentWatchdog.class);
409                intent.putExtra(EXTRA_CALLBACK_TIMEOUT, callbackTimeout);
410                mWatchdogPendingIntent =
411                        PendingIntent.getBroadcast(context, 0, intent, 0);
412            }
413            // Set the alarm
414            final AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
415            am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + delay,
416                    mWatchdogPendingIntent);
417            debugTrace("Set up a watchdog for %d millis in the future", delay);
418        }
419
420        public void setWatchdogAlarm(final Context context) {
421            // Call the real function with default values.
422            setWatchdogAlarm(context, WATCHDOG_CHECK_INTERVAL, CALLBACK_TIMEOUT);
423        }
424
425        @Override
426        public void onReceive(final Context context, final Intent intent) {
427            final int callbackTimeout = intent.getIntExtra(EXTRA_CALLBACK_TIMEOUT,
428                    CALLBACK_TIMEOUT);
429            new Thread(new Runnable() {
430                @Override
431                public void run() {
432                    // TODO: Really don't like hard coding the AttachmentService reference here
433                    // as it makes testing harder if we are trying to mock out the service
434                    // We should change this with some sort of getter that returns the
435                    // static (or test) AttachmentService instance to use.
436                    final AttachmentService service = AttachmentService.sRunningService;
437                    if (service != null) {
438                        // If our service instance is gone, just leave
439                        if (service.mStop) {
440                            return;
441                        }
442                        // Get the timeout time from the intent.
443                        watchdogAlarm(service, callbackTimeout);
444                    }
445                }
446            }, "AttachmentService AttachmentWatchdog").start();
447        }
448
449        boolean validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout,
450                final long now) {
451            // Check how long it's been since receiving a callback
452            final long timeSinceCallback = now - dr.mLastCallbackTime;
453            if (timeSinceCallback > callbackTimeout) {
454                LogUtils.d(LOG_TAG, "Timeout for DownloadRequest #%d ", dr.mAttachmentId);
455                return true;
456            }
457            return false;
458        }
459
460        /**
461         * Watchdog for downloads; we use this in case we are hanging on a download, which might
462         * have failed silently (the connection dropped, for example)
463         */
464        void watchdogAlarm(final AttachmentService service, final int callbackTimeout) {
465            debugTrace("Received a timer callback in the watchdog");
466
467            // We want to iterate on each of the downloads that are currently in progress and
468            // cancel the ones that seem to be taking too long.
469            final Collection<DownloadRequest> inProgressRequests =
470                    service.mDownloadsInProgress.values();
471            for (DownloadRequest req: inProgressRequests) {
472                debugTrace("Checking in-progress request with id: %d", req.mAttachmentId);
473                final boolean shouldCancelDownload = validateDownloadRequest(req, callbackTimeout,
474                        System.currentTimeMillis());
475                if (shouldCancelDownload) {
476                    LogUtils.w(LOG_TAG, "Cancelling DownloadRequest #%d", req.mAttachmentId);
477                    service.cancelDownload(req);
478                    // TODO: Should we also mark the attachment as failed at this point in time?
479                }
480            }
481            // Check whether we can start new downloads...
482            if (service.isConnected()) {
483                service.processQueue();
484            }
485            issueNextWatchdogAlarm(service);
486        }
487
488        void issueNextWatchdogAlarm(final AttachmentService service) {
489            if (!service.mDownloadsInProgress.isEmpty()) {
490                debugTrace("Rescheduling watchdog...");
491                setWatchdogAlarm(service);
492            }
493        }
494    }
495
496    /**
497     * We use an EmailServiceCallback to keep track of the progress of downloads.  These callbacks
498     * come from either Controller (IMAP/POP) or ExchangeService (EAS).  Note that we only
499     * implement the single callback that's defined by the EmailServiceCallback interface.
500     */
501    class ServiceCallback extends IEmailServiceCallback.Stub {
502
503        /**
504         * Simple routine to generate updated status values for the Attachment based on the
505         * service callback. Right now it is very simple but factoring out this code allows us
506         * to test easier and very easy to expand in the future.
507         */
508        ContentValues getAttachmentUpdateValues(final Attachment attachment,
509                final int statusCode, final int progress) {
510            final ContentValues values = new ContentValues();
511            if (attachment != null) {
512                if (statusCode == EmailServiceStatus.IN_PROGRESS) {
513                    // TODO: What else do we want to expose about this in-progress download through
514                    // the provider?  If there is more, make sure that the service implementation
515                    // reports it and make sure that we add it here.
516                    values.put(AttachmentColumns.UI_STATE, AttachmentState.DOWNLOADING);
517                    values.put(AttachmentColumns.UI_DOWNLOADED_SIZE,
518                            attachment.mSize * progress / 100);
519                }
520            }
521            return values;
522        }
523
524        @Override
525        public void loadAttachmentStatus(final long messageId, final long attachmentId,
526                final int statusCode, final int progress) {
527            debugTrace(LOG_TAG, "ServiceCallback for attachment #%d", attachmentId);
528
529            // Record status and progress
530            final DownloadRequest req = mDownloadsInProgress.get(attachmentId);
531            if (req != null) {
532                final long now = System.currentTimeMillis();
533                debugTrace("ServiceCallback: status code changing from %d to %d",
534                        req.mLastStatusCode, statusCode);
535                debugTrace("ServiceCallback: progress changing from %d to %d",
536                        req.mLastProgress,progress);
537                debugTrace("ServiceCallback: last callback time changing from %d to %d",
538                        req.mLastCallbackTime, now);
539
540                // Update some state to keep track of the progress of the download
541                req.mLastStatusCode = statusCode;
542                req.mLastProgress = progress;
543                req.mLastCallbackTime = now;
544
545                // Update the attachment status in the provider.
546                final Attachment attachment =
547                        Attachment.restoreAttachmentWithId(AttachmentService.this, attachmentId);
548                final ContentValues values = getAttachmentUpdateValues(attachment, statusCode,
549                        progress);
550                if (values.size() > 0) {
551                    attachment.update(AttachmentService.this, values);
552                }
553
554                switch (statusCode) {
555                    case EmailServiceStatus.IN_PROGRESS:
556                        break;
557                    default:
558                        // It is assumed that any other error is either a success or an error
559                        // Either way, the final updates to the DownloadRequest and attachment
560                        // objects will be handed there.
561                        LogUtils.d(LOG_TAG, "Attachment #%d is done", attachmentId);
562                        endDownload(attachmentId, statusCode);
563                        break;
564                }
565            } else {
566                // The only way that we can get a callback from the service implementation for
567                // an attachment that doesn't exist is if it was cancelled due to the
568                // AttachmentWatchdog. This is a valid scenario and the Watchdog should have already
569                // marked this attachment as failed/cancelled.
570            }
571        }
572    }
573
574    /**
575     * Called directly by EmailProvider whenever an attachment is inserted or changed. Since this
576     * call is being invoked on the UI thread, we need to make sure that the downloads are
577     * happening in the background.
578     * @param context the caller's context
579     * @param id the attachment's id
580     * @param flags the new flags for the attachment
581     */
582    public static void attachmentChanged(final Context context, final long id, final int flags) {
583        LogUtils.d(LOG_TAG, "Attachment with id: %d will potentially be queued for download", id);
584        // Throw this info into an intent and send it to the attachment service.
585        final Intent intent = new Intent(context, AttachmentService.class);
586        debugTrace("Calling startService with extras %d & %d", id, flags);
587        intent.putExtra(EXTRA_ATTACHMENT_ID, id);
588        intent.putExtra(EXTRA_ATTACHMENT_FLAGS, flags);
589        context.startService(intent);
590    }
591
592    /**
593     * The main entry point for this service, the attachment to download can be identified
594     * by the EXTRA_ATTACHMENT extra in the intent.
595     */
596    @Override
597    public int onStartCommand(final Intent intent, final int flags, final int startId) {
598        if (sRunningService == null) {
599            sRunningService = this;
600        }
601        if (intent != null) {
602            // Let's add this id/flags combo to the list of potential attachments to process.
603            final long attachment_id = intent.getLongExtra(EXTRA_ATTACHMENT_ID, -1);
604            final int attachment_flags = intent.getIntExtra(EXTRA_ATTACHMENT_FLAGS, -1);
605            if ((attachment_id >= 0) && (attachment_flags >= 0)) {
606                sAttachmentChangedQueue.add(new long[]{attachment_id, attachment_flags});
607                // Process the queue if we're in a wait
608                kick();
609            } else {
610                debugTrace("Received an invalid intent w/o the required extras %d & %d",
611                        attachment_id, attachment_flags);
612            }
613        } else {
614            debugTrace("Received a null intent in onStartCommand");
615        }
616        return Service.START_STICKY;
617    }
618
619    /**
620     * Most of the leg work is done by our service thread that is created when this
621     * service is created.
622     */
623    @Override
624    public void onCreate() {
625        // Start up our service thread.
626        new Thread(this, "AttachmentService").start();
627    }
628
629    @Override
630    public IBinder onBind(final Intent intent) {
631        return null;
632    }
633
634    @Override
635    public void onDestroy() {
636        debugTrace("Destroying AttachmentService object");
637        dumpInProgressDownloads();
638
639        // Mark this instance of the service as stopped. Our main loop for the AttachmentService
640        // checks for this flag along with the AttachmentWatchdog.
641        mStop = true;
642        if (sRunningService != null) {
643            // Kick it awake to get it to realize that we are stopping.
644            kick();
645            sRunningService = null;
646        }
647        if (mConnectivityManager != null) {
648            mConnectivityManager.unregister();
649            mConnectivityManager.stopWait();
650            mConnectivityManager = null;
651        }
652    }
653
654    /**
655     * The main routine for our AttachmentService service thread.
656     */
657    @Override
658    public void run() {
659        // These fields are only used within the service thread
660        mConnectivityManager = new EmailConnectivityManager(this, LOG_TAG);
661        mAccountManagerStub = new AccountManagerStub(this);
662
663        // Run through all attachments in the database that require download and add them to
664        // the queue. This is the case where a previous AttachmentService may have been notified
665        // to stop before processing everything in its queue.
666        final int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
667        final Cursor c = getContentResolver().query(Attachment.CONTENT_URI,
668                EmailContent.ID_PROJECTION, "(" + AttachmentColumns.FLAGS + " & ?) != 0",
669                new String[] {Integer.toString(mask)}, null);
670        try {
671            LogUtils.d(LOG_TAG,
672                    "Count of previous downloads to resume (from db): %d", c.getCount());
673            while (c.moveToNext()) {
674                final Attachment attachment = Attachment.restoreAttachmentWithId(
675                        this, c.getLong(EmailContent.ID_PROJECTION_COLUMN));
676                if (attachment != null) {
677                    debugTrace("Attempting to download attachment #%d again.", attachment.mId);
678                    onChange(this, attachment);
679                }
680            }
681        } catch (Exception e) {
682            e.printStackTrace();
683        } finally {
684            c.close();
685        }
686
687        // Loop until stopped, with a 30 minute wait loop
688        while (!mStop) {
689            // Here's where we run our attachment loading logic...
690            // Make a local copy of the variable so we don't null-crash on service shutdown
691            final EmailConnectivityManager ecm = mConnectivityManager;
692            if (ecm != null) {
693                ecm.waitForConnectivity();
694            }
695            if (mStop) {
696                // We might be bailing out here due to the service shutting down
697                LogUtils.d(LOG_TAG, "AttachmentService has been instructed to stop");
698                break;
699            }
700
701            // In advanced debug mode, let's look at the state of all in-progress downloads
702            // after processQueue() runs.
703            debugTrace("In progress downloads before processQueue");
704            dumpInProgressDownloads();
705            processQueue();
706            debugTrace("In progress downloads after processQueue");
707            dumpInProgressDownloads();
708
709            if (mDownloadQueue.isEmpty() && (mDownloadsInProgress.size() < 1)) {
710                LogUtils.d(LOG_TAG, "Shutting down service. No in-progress or pending downloads.");
711                stopSelf();
712                break;
713            }
714            debugTrace("Run() idle, wait for mLock (something to do)");
715            synchronized(mLock) {
716                try {
717                    mLock.wait(PROCESS_QUEUE_WAIT_TIME);
718                } catch (InterruptedException e) {
719                    // That's ok; we'll just keep looping
720                }
721            }
722            debugTrace("Run() got mLock (there is work to do or we timed out)");
723        }
724
725        // Unregister now that we're done
726        // Make a local copy of the variable so we don't null-crash on service shutdown
727        final EmailConnectivityManager ecm = mConnectivityManager;
728        if (ecm != null) {
729            ecm.unregister();
730        }
731    }
732
733    /*
734     * Function that kicks the service into action as it may be waiting for this object
735     * as it processed the last round of attachments.
736     */
737    private void kick() {
738        synchronized(mLock) {
739            mLock.notify();
740        }
741    }
742
743    /**
744     * onChange is called by the AttachmentReceiver upon receipt of a valid notification from
745     * EmailProvider that an attachment has been inserted or modified.  It's not strictly
746     * necessary that we detect a deleted attachment, as the code always checks for the
747     * existence of an attachment before acting on it.
748     */
749    public synchronized void onChange(final Context context, final Attachment att) {
750        debugTrace("onChange() for Attachment: #%d", att.mId);
751        DownloadRequest req = mDownloadQueue.findRequestById(att.mId);
752        final long priority = getAttachmentPriority(att);
753        if (priority == PRIORITY_NONE) {
754            LogUtils.d(LOG_TAG, "Attachment #%d has no priority and will not be downloaded",
755                    att.mId);
756            // In this case, there is no download priority for this attachment
757            if (req != null) {
758                // If it exists in the map, remove it
759                // NOTE: We don't yet support deleting downloads in progress
760                mDownloadQueue.removeRequest(req);
761            }
762        } else {
763            // Ignore changes that occur during download
764            if (mDownloadsInProgress.containsKey(att.mId)) {
765                debugTrace("Attachment #%d was already in the queue", att.mId);
766                return;
767            }
768            // If this is new, add the request to the queue
769            if (req == null) {
770                LogUtils.d(LOG_TAG, "Attachment #%d is a new download request", att.mId);
771                req = new DownloadRequest(context, att);
772                final AttachmentInfo attachInfo = new AttachmentInfo(context, att);
773                if (!attachInfo.isEligibleForDownload()) {
774                    LogUtils.w(LOG_TAG, "Attachment #%d is not eligible for download", att.mId);
775                    // We can't download this file due to policy, depending on what type
776                    // of request we received, we handle the response differently.
777                    if (((att.mFlags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) ||
778                            ((att.mFlags & Attachment.FLAG_POLICY_DISALLOWS_DOWNLOAD) != 0)) {
779                        LogUtils.w(LOG_TAG, "Attachment #%d cannot be downloaded ever", att.mId);
780                        // There are a couple of situations where we will not even allow this
781                        // request to go in the queue because we can already process it as a
782                        // failure.
783                        // 1. The user explicitly wants to download this attachment from the
784                        // email view but they should not be able to...either because there is
785                        // no app to view it or because its been marked as a policy violation.
786                        // 2. The user is forwarding an email and the attachment has been
787                        // marked as a policy violation. If the attachment is non viewable
788                        // that is OK for forwarding a message so we'll let it pass through
789                        markAttachmentAsFailed(att);
790                        return;
791                    }
792                    // If we get this far it a forward of an attachment that is only
793                    // ineligible because we can't view it or process it. Not because we
794                    // can't download it for policy reasons. Let's let this go through because
795                    // the final recipient of this forward email might be able to process it.
796                }
797                mDownloadQueue.addRequest(req);
798            }
799            // TODO: If the request already existed, we'll update the priority (so that the time is
800            // up-to-date); otherwise, create a new request
801            LogUtils.d(LOG_TAG,
802                    "Attachment #%d queued for download, priority: %d, created time: %d",
803                    att.mId, req.mPriority, req.mCreatedTime);
804        }
805        // Process the queue if we're in a wait
806        kick();
807    }
808
809    /**
810     * Set the bits in the provider to mark this download as failed.
811     * @param att The attachment that failed to download.
812     */
813    void markAttachmentAsFailed(final Attachment att) {
814        final ContentValues cv = new ContentValues();
815        final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
816        cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
817        cv.put(AttachmentColumns.UI_STATE, AttachmentState.FAILED);
818        att.update(this, cv);
819    }
820
821    /**
822     * Set the bits in the provider to mark this download as completed.
823     * @param att The attachment that was downloaded.
824     */
825    void markAttachmentAsCompleted(final Attachment att) {
826        final ContentValues cv = new ContentValues();
827        final int flags = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST;
828        cv.put(AttachmentColumns.FLAGS, att.mFlags &= ~flags);
829        cv.put(AttachmentColumns.UI_STATE, AttachmentState.SAVED);
830        att.update(this, cv);
831    }
832
833    /**
834     * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing
835     * the limit on maximum downloads
836     */
837    synchronized void processQueue() {
838        debugTrace("Processing changed queue, num entries: %d", sAttachmentChangedQueue.size());
839
840        // First thing we need to do is process the list of "potential downloads" that we
841        // added to sAttachmentChangedQueue
842        long[] change = sAttachmentChangedQueue.poll();
843        while (change != null) {
844            // Process this change
845            final long id = change[0];
846            final long flags = change[1];
847            final Attachment attachment = Attachment.restoreAttachmentWithId(this, id);
848            if (attachment == null) {
849                LogUtils.w(LOG_TAG, "Could not restore attachment #%d", id);
850                continue;
851            }
852            attachment.mFlags = (int) flags;
853            onChange(this, attachment);
854            change = sAttachmentChangedQueue.poll();
855        }
856
857        debugTrace("Processing download queue, num entries: %d", mDownloadQueue.getSize());
858
859        while (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS) {
860            final DownloadRequest req = mDownloadQueue.getNextRequest();
861            if (req == null) {
862                // No more queued requests?  We are done for now.
863                break;
864            }
865            // Enforce per-account limit here
866            if (getDownloadsForAccount(req.mAccountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) {
867                LogUtils.w(LOG_TAG, "Skipping #%d; maxed for acct %d",
868                        req.mAttachmentId, req.mAccountId);
869                continue;
870            }
871            if (Attachment.restoreAttachmentWithId(this, req.mAttachmentId) == null) {
872                LogUtils.e(LOG_TAG, "Could not load attachment: #%d", req.mAttachmentId);
873                continue;
874            }
875            if (!req.mInProgress) {
876                final long currentTime = SystemClock.elapsedRealtime();
877                if (req.mRetryCount > 0 && req.mRetryStartTime > currentTime) {
878                    debugTrace("Need to wait before retrying attachment #%d", req.mAttachmentId);
879                    mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
880                            CALLBACK_TIMEOUT);
881                    continue;
882                }
883                // TODO: We try to gate ineligible downloads from entering the queue but its
884                // always possible that they made it in here regardless in the future.  In a
885                // perfect world, we would make it bullet proof with a check for eligibility
886                // here instead/also.
887                tryStartDownload(req);
888            }
889        }
890
891        // Check our ability to be opportunistic regarding background downloads.
892        final EmailConnectivityManager ecm = mConnectivityManager;
893        if ((ecm == null) || !ecm.isAutoSyncAllowed() ||
894                (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI)) {
895            // Only prefetch if it if connectivity is available, prefetch is enabled
896            // and we are on WIFI
897            LogUtils.d(LOG_TAG, "Skipping opportunistic downloads since WIFI is not available");
898            return;
899        }
900
901        // Then, try opportunistic download of appropriate attachments
902        final int availableBackgroundThreads =
903                MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size();
904        if (availableBackgroundThreads < 1) {
905            // We want to leave one spot open for a user requested download that we haven't
906            // started processing yet.
907            LogUtils.d(LOG_TAG, "Skipping opportunistic downloads, %d threads available",
908                    availableBackgroundThreads);
909            dumpInProgressDownloads();
910            return;
911        }
912
913        debugTrace("Launching up to %d opportunistic downloads", availableBackgroundThreads);
914
915        // We'll load up the newest 25 attachments that aren't loaded or queued
916        // TODO: We are always looking for MAX_ATTACHMENTS_TO_CHECK, shouldn't this be
917        // backgroundDownloads instead?  We should fix and test this.
918        final Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI,
919                MAX_ATTACHMENTS_TO_CHECK);
920        final Cursor c = this.getContentResolver().query(lookupUri,
921                Attachment.CONTENT_PROJECTION,
922                EmailContent.Attachment.PRECACHE_INBOX_SELECTION,
923                null, AttachmentColumns._ID + " DESC");
924        File cacheDir = this.getCacheDir();
925        try {
926            while (c.moveToNext()) {
927                final Attachment att = new Attachment();
928                att.restore(c);
929                final Account account = Account.restoreAccountWithId(this, att.mAccountKey);
930                if (account == null) {
931                    // Clean up this orphaned attachment; there's no point in keeping it
932                    // around; then try to find another one
933                    debugTrace("Found orphaned attachment #%d", att.mId);
934                    EmailContent.delete(this, Attachment.CONTENT_URI, att.mId);
935                } else {
936                    // Check that the attachment meets system requirements for download
937                    // Note that there couple be policy that does not allow this attachment
938                    // to be downloaded.
939                    final AttachmentInfo info = new AttachmentInfo(this, att);
940                    if (info.isEligibleForDownload()) {
941                        // Either the account must be able to prefetch or this must be
942                        // an inline attachment.
943                        if (att.mContentId != null || canPrefetchForAccount(account, cacheDir)) {
944                            final Integer tryCount = mAttachmentFailureMap.get(att.mId);
945                            if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) {
946                                // move onto the next attachment
947                                LogUtils.w(LOG_TAG,
948                                        "Too many failed attempts for attachment #%d ", att.mId);
949                                continue;
950                            }
951                            // Start this download and we're done
952                            final DownloadRequest req = new DownloadRequest(this, att);
953                            tryStartDownload(req);
954                            break;
955                        }
956                    } else {
957                        // If this attachment was ineligible for download
958                        // because of policy related issues, its flags would be set to
959                        // FLAG_POLICY_DISALLOWS_DOWNLOAD and would not show up in the
960                        // query results. We are most likely here for other reasons such
961                        // as the inability to view the attachment. In that case, let's just
962                        // skip it for now.
963                        LogUtils.w(LOG_TAG, "Skipping attachment #%d, it is ineligible", att.mId);
964                    }
965                }
966            }
967        } finally {
968            c.close();
969        }
970    }
971
972    /**
973     * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account
974     * parameter
975     * @param req the DownloadRequest
976     * @return whether or not the download was started
977     */
978    synchronized boolean tryStartDownload(final DownloadRequest req) {
979        final EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
980                AttachmentService.this, req.mAccountId);
981
982        // Do not download the same attachment multiple times
983        boolean alreadyInProgress = mDownloadsInProgress.get(req.mAttachmentId) != null;
984        if (alreadyInProgress) {
985            debugTrace("This attachment #%d is already in progress", req.mAttachmentId);
986            return false;
987        }
988
989        try {
990            startDownload(service, req);
991        } catch (RemoteException e) {
992            // TODO: Consider whether we need to do more in this case...
993            // For now, fix up our data to reflect the failure
994            cancelDownload(req);
995        }
996        return true;
997    }
998
999    /**
1000     * Do the work of starting an attachment download using the EmailService interface, and
1001     * set our watchdog alarm
1002     *
1003     * @param service the service handling the download
1004     * @param req the DownloadRequest
1005     * @throws RemoteException
1006     */
1007    private void startDownload(final EmailServiceProxy service, final DownloadRequest req)
1008            throws RemoteException {
1009        LogUtils.d(LOG_TAG, "Starting download for Attachment #%d", req.mAttachmentId);
1010        req.mStartTime = System.currentTimeMillis();
1011        req.mInProgress = true;
1012        mDownloadsInProgress.put(req.mAttachmentId, req);
1013        service.loadAttachment(mServiceCallback, req.mAccountId, req.mAttachmentId,
1014                req.mPriority != PRIORITY_FOREGROUND);
1015        mWatchdog.setWatchdogAlarm(this);
1016    }
1017
1018    synchronized void cancelDownload(final DownloadRequest req) {
1019        LogUtils.d(LOG_TAG, "Cancelling download for Attachment #%d", req.mAttachmentId);
1020        req.mInProgress = false;
1021        mDownloadsInProgress.remove(req.mAttachmentId);
1022        // Remove the download from our queue, and then decide whether or not to add it back.
1023        mDownloadQueue.removeRequest(req);
1024        req.mRetryCount++;
1025        if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
1026            LogUtils.w(LOG_TAG, "Too many failures giving up on Attachment #%d", req.mAttachmentId);
1027        } else {
1028            debugTrace("Moving to end of queue, will retry #%d", req.mAttachmentId);
1029            // The time field of DownloadRequest is final, because it's unsafe to change it
1030            // as long as the DownloadRequest is in the DownloadSet. It's needed for the
1031            // comparator, so changing time would make the request unfindable.
1032            // Instead, we'll create a new DownloadRequest with an updated time.
1033            // This will sort at the end of the set.
1034            final DownloadRequest newReq = new DownloadRequest(req, SystemClock.elapsedRealtime());
1035            mDownloadQueue.addRequest(newReq);
1036        }
1037    }
1038
1039    /**
1040     * Called when a download is finished; we get notified of this via our EmailServiceCallback
1041     * @param attachmentId the id of the attachment whose download is finished
1042     * @param statusCode the EmailServiceStatus code returned by the Service
1043     */
1044    synchronized void endDownload(final long attachmentId, final int statusCode) {
1045        LogUtils.d(LOG_TAG, "Finishing download #%d", attachmentId);
1046
1047        // Say we're no longer downloading this
1048        mDownloadsInProgress.remove(attachmentId);
1049
1050        // TODO: This code is conservative and treats connection issues as failures.
1051        // Since we have no mechanism to throttle reconnection attempts, it makes
1052        // sense to be cautious here. Once logic is in place to prevent connecting
1053        // in a tight loop, we can exclude counting connection issues as "failures".
1054
1055        // Update the attachment failure list if needed
1056        Integer downloadCount;
1057        downloadCount = mAttachmentFailureMap.remove(attachmentId);
1058        if (statusCode != EmailServiceStatus.SUCCESS) {
1059            if (downloadCount == null) {
1060                downloadCount = 0;
1061            }
1062            downloadCount += 1;
1063            LogUtils.w(LOG_TAG, "This attachment failed, adding #%d to failure map", attachmentId);
1064            mAttachmentFailureMap.put(attachmentId, downloadCount);
1065        }
1066
1067        final DownloadRequest req = mDownloadQueue.findRequestById(attachmentId);
1068        if (statusCode == EmailServiceStatus.CONNECTION_ERROR) {
1069            // If this needs to be retried, just process the queue again
1070            if (req != null) {
1071                req.mRetryCount++;
1072                if (req.mRetryCount > CONNECTION_ERROR_MAX_RETRIES) {
1073                    // We are done, we maxed out our total number of tries.
1074                    // Not that we do not flag this attachment with any special flags so the
1075                    // AttachmentService will try to download this attachment again the next time
1076                    // that it starts up.
1077                    LogUtils.w(LOG_TAG, "Too many tried for connection errors, giving up #%d",
1078                            attachmentId);
1079                    mDownloadQueue.removeRequest(req);
1080                    // Note that we are not doing anything with the attachment right now
1081                    // We will annotate it later in this function if needed.
1082                } else if (req.mRetryCount > CONNECTION_ERROR_DELAY_THRESHOLD) {
1083                    // TODO: I'm not sure this is a great retry/backoff policy, but we're
1084                    // afraid of changing behavior too much in case something relies upon it.
1085                    // So now, for the first five errors, we'll retry immediately. For the next
1086                    // five tries, we'll add a ten second delay between each. After that, we'll
1087                    // give up.
1088                    LogUtils.w(LOG_TAG, "ConnectionError #%d, retried %d times, adding delay",
1089                            attachmentId, req.mRetryCount);
1090                    req.mInProgress = false;
1091                    req.mRetryStartTime = SystemClock.elapsedRealtime() +
1092                            CONNECTION_ERROR_RETRY_MILLIS;
1093                    mWatchdog.setWatchdogAlarm(this, CONNECTION_ERROR_RETRY_MILLIS,
1094                            CALLBACK_TIMEOUT);
1095                } else {
1096                    LogUtils.w(LOG_TAG, "ConnectionError for #%d, retried %d times, adding delay",
1097                            attachmentId, req.mRetryCount);
1098                    req.mInProgress = false;
1099                    req.mRetryStartTime = 0;
1100                    kick();
1101                }
1102            }
1103            return;
1104        }
1105
1106        // If the request is still in the queue, remove it
1107        if (req != null) {
1108            mDownloadQueue.removeRequest(req);
1109        }
1110
1111        if (ENABLE_ATTACHMENT_SERVICE_DEBUG > 0) {
1112            long secs = 0;
1113            if (req != null) {
1114                secs = (System.currentTimeMillis() - req.mCreatedTime) / 1000;
1115            }
1116            final String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" :
1117                "Error " + statusCode;
1118            debugTrace("Download finished for attachment #%d; %d seconds from request, status: %s",
1119                    attachmentId, secs, status);
1120        }
1121
1122        final Attachment attachment = Attachment.restoreAttachmentWithId(this, attachmentId);
1123        if (attachment != null) {
1124            final long accountId = attachment.mAccountKey;
1125            // Update our attachment storage for this account
1126            Long currentStorage = mAttachmentStorageMap.get(accountId);
1127            if (currentStorage == null) {
1128                currentStorage = 0L;
1129            }
1130            mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize);
1131            boolean deleted = false;
1132            if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
1133                if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) {
1134                    // If this is a forwarding download, and the attachment doesn't exist (or
1135                    // can't be downloaded) delete it from the outgoing message, lest that
1136                    // message never get sent
1137                    EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
1138                    // TODO: Talk to UX about whether this is even worth doing
1139                    final NotificationController nc =
1140                            NotificationControllerCreatorHolder.getInstance(this);
1141                    if (nc != null) {
1142                        nc.showDownloadForwardFailedNotificationSynchronous(attachment);
1143                    }
1144                    deleted = true;
1145                    LogUtils.w(LOG_TAG, "Deleting forwarded attachment #%d for message #%d",
1146                        attachmentId, attachment.mMessageKey);
1147                }
1148                // If we're an attachment on forwarded mail, and if we're not still blocked,
1149                // try to send pending mail now (as mediated by MailService)
1150                if ((req != null) &&
1151                        !Utility.hasUnloadedAttachments(this, attachment.mMessageKey)) {
1152                    debugTrace("Downloads finished for outgoing msg #%d", req.mMessageId);
1153                    EmailServiceProxy service = EmailServiceUtils.getServiceForAccount(
1154                            this, accountId);
1155                    try {
1156                        service.sendMail(accountId);
1157                    } catch (RemoteException e) {
1158                        LogUtils.e(LOG_TAG, "RemoteException while trying to send message: #%d, %s",
1159                                req.mMessageId, e.toString());
1160                    }
1161                }
1162            }
1163            if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) {
1164                Message msg = Message.restoreMessageWithId(this, attachment.mMessageKey);
1165                if (msg == null) {
1166                    LogUtils.w(LOG_TAG, "Deleting attachment #%d with no associated message #%d",
1167                            attachment.mId, attachment.mMessageKey);
1168                    // If there's no associated message, delete the attachment
1169                    EmailContent.delete(this, Attachment.CONTENT_URI, attachment.mId);
1170                } else {
1171                    // If there really is a message, retry
1172                    // TODO: How will this get retried? It's still marked as inProgress?
1173                    LogUtils.w(LOG_TAG, "Retrying attachment #%d with associated message #%d",
1174                            attachment.mId, attachment.mMessageKey);
1175                    kick();
1176                    return;
1177                }
1178            } else if (!deleted) {
1179                // Clear the download flags, since we're done for now.  Note that this happens
1180                // only for non-recoverable errors.  When these occur for forwarded mail, we can
1181                // ignore it and continue; otherwise, it was either 1) a user request, in which
1182                // case the user can retry manually or 2) an opportunistic download, in which
1183                // case the download wasn't critical
1184                LogUtils.d(LOG_TAG, "Attachment #%d successfully downloaded!", attachment.mId);
1185                markAttachmentAsCompleted(attachment);
1186            }
1187        }
1188        // Process the queue
1189        kick();
1190    }
1191
1192    /**
1193     * Count the number of running downloads in progress for this account
1194     * @param accountId the id of the account
1195     * @return the count of running downloads
1196     */
1197    synchronized int getDownloadsForAccount(final long accountId) {
1198        int count = 0;
1199        for (final DownloadRequest req: mDownloadsInProgress.values()) {
1200            if (req.mAccountId == accountId) {
1201                count++;
1202            }
1203        }
1204        return count;
1205    }
1206
1207    /**
1208     * Calculate the download priority of an Attachment.  A priority of zero means that the
1209     * attachment is not marked for download.
1210     * @param att the Attachment
1211     * @return the priority key of the Attachment
1212     */
1213    private static int getAttachmentPriority(final Attachment att) {
1214        int priorityClass = PRIORITY_NONE;
1215        final int flags = att.mFlags;
1216        if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) {
1217            priorityClass = PRIORITY_SEND_MAIL;
1218        } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) {
1219            priorityClass = PRIORITY_FOREGROUND;
1220        }
1221        return priorityClass;
1222    }
1223
1224    /**
1225     * Determine whether an attachment can be prefetched for the given account based on
1226     * total download size restrictions tied to the account.
1227     * @return true if download is allowed, false otherwise
1228     */
1229    public boolean canPrefetchForAccount(final Account account, final File dir) {
1230        // Check account, just in case
1231        if (account == null) return false;
1232
1233        // First, check preference and quickly return if prefetch isn't allowed
1234        if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) {
1235            debugTrace("Prefetch is not allowed for this account: %d", account.getId());
1236            return false;
1237        }
1238
1239        final long totalStorage = dir.getTotalSpace();
1240        final long usableStorage = dir.getUsableSpace();
1241        final long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE);
1242
1243        // If there's not enough overall storage available, stop now
1244        if (usableStorage < minAvailable) {
1245            debugTrace("Not enough physical storage for prefetch");
1246            return false;
1247        }
1248
1249        final int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts();
1250        // Calculate an even per-account storage although it would make a lot of sense to not
1251        // do this as you may assign more storage to your corporate account rather than a personal
1252        // account.
1253        final long perAccountMaxStorage =
1254                (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts);
1255
1256        // Retrieve our idea of currently used attachment storage; since we don't track deletions,
1257        // this number is the "worst case".  If the number is greater than what's allowed per
1258        // account, we walk the directory to determine the actual number.
1259        Long accountStorage = mAttachmentStorageMap.get(account.mId);
1260        if (accountStorage == null || (accountStorage > perAccountMaxStorage)) {
1261            // Calculate the exact figure for attachment storage for this account
1262            accountStorage = 0L;
1263            File[] files = dir.listFiles();
1264            if (files != null) {
1265                for (File file : files) {
1266                    accountStorage += file.length();
1267                }
1268            }
1269            // Cache the value. No locking here since this is a concurrent collection object.
1270            mAttachmentStorageMap.put(account.mId, accountStorage);
1271        }
1272
1273        // Return true if we're using less than the maximum per account
1274        if (accountStorage >= perAccountMaxStorage) {
1275            debugTrace("Prefetch not allowed for account %d; used: %d, limit %d",
1276                    account.mId, accountStorage, perAccountMaxStorage);
1277            return false;
1278        }
1279        return true;
1280    }
1281
1282    boolean isConnected() {
1283        if (mConnectivityManager != null) {
1284            return mConnectivityManager.hasConnectivity();
1285        }
1286        return false;
1287    }
1288
1289    // For Debugging.
1290    synchronized public void dumpInProgressDownloads() {
1291        if (ENABLE_ATTACHMENT_SERVICE_DEBUG < 1) {
1292            LogUtils.d(LOG_TAG, "Advanced logging not configured.");
1293        }
1294        LogUtils.d(LOG_TAG, "Here are the in-progress downloads...");
1295        for (final DownloadRequest req : mDownloadsInProgress.values()) {
1296            LogUtils.d(LOG_TAG, "--BEGIN DownloadRequest DUMP--");
1297            LogUtils.d(LOG_TAG, "Account: #%d", req.mAccountId);
1298            LogUtils.d(LOG_TAG, "Message: #%d", req.mMessageId);
1299            LogUtils.d(LOG_TAG, "Attachment: #%d", req.mAttachmentId);
1300            LogUtils.d(LOG_TAG, "Created Time: %d", req.mCreatedTime);
1301            LogUtils.d(LOG_TAG, "Priority: %d", req.mPriority);
1302            if (req.mInProgress == true) {
1303                LogUtils.d(LOG_TAG, "This download is in progress");
1304            } else {
1305                LogUtils.d(LOG_TAG, "This download is not in progress");
1306            }
1307            LogUtils.d(LOG_TAG, "Start Time: %d", req.mStartTime);
1308            LogUtils.d(LOG_TAG, "Retry Count: %d", req.mRetryCount);
1309            LogUtils.d(LOG_TAG, "Retry Start Tiome: %d", req.mRetryStartTime);
1310            LogUtils.d(LOG_TAG, "Last Status Code: %d", req.mLastStatusCode);
1311            LogUtils.d(LOG_TAG, "Last Progress: %d", req.mLastProgress);
1312            LogUtils.d(LOG_TAG, "Last Callback Time: %d", req.mLastCallbackTime);
1313            LogUtils.d(LOG_TAG, "------------------------------");
1314        }
1315        LogUtils.d(LOG_TAG, "Done reporting in-progress downloads...");
1316    }
1317
1318
1319    @Override
1320    public void dump(final FileDescriptor fd, final PrintWriter pw, final String[] args) {
1321        pw.println("AttachmentService");
1322        final long time = System.currentTimeMillis();
1323        synchronized(mDownloadQueue) {
1324            pw.println("  Queue, " + mDownloadQueue.getSize() + " entries");
1325            // If you iterate over the queue either via iterator or collection, they are not
1326            // returned in any particular order. With all things being equal its better to go with
1327            // a collection to avoid any potential ConcurrentModificationExceptions.
1328            // If we really want this sorted, we can sort it manually since performance isn't a big
1329            // concern with this debug method.
1330            for (final DownloadRequest req : mDownloadQueue.mRequestMap.values()) {
1331                pw.println("    Account: " + req.mAccountId + ", Attachment: " + req.mAttachmentId);
1332                pw.println("      Priority: " + req.mPriority + ", Time: " + req.mCreatedTime +
1333                        (req.mInProgress ? " [In progress]" : ""));
1334                final Attachment att = Attachment.restoreAttachmentWithId(this, req.mAttachmentId);
1335                if (att == null) {
1336                    pw.println("      Attachment not in database?");
1337                } else if (att.mFileName != null) {
1338                    final String fileName = att.mFileName;
1339                    final String suffix;
1340                    final int lastDot = fileName.lastIndexOf('.');
1341                    if (lastDot >= 0) {
1342                        suffix = fileName.substring(lastDot);
1343                    } else {
1344                        suffix = "[none]";
1345                    }
1346                    pw.print("      Suffix: " + suffix);
1347                    if (att.getContentUri() != null) {
1348                        pw.print(" ContentUri: " + att.getContentUri());
1349                    }
1350                    pw.print(" Mime: ");
1351                    if (att.mMimeType != null) {
1352                        pw.print(att.mMimeType);
1353                    } else {
1354                        pw.print(AttachmentUtilities.inferMimeType(fileName, null));
1355                        pw.print(" [inferred]");
1356                    }
1357                    pw.println(" Size: " + att.mSize);
1358                }
1359                if (req.mInProgress) {
1360                    pw.println("      Status: " + req.mLastStatusCode + ", Progress: " +
1361                            req.mLastProgress);
1362                    pw.println("      Started: " + req.mStartTime + ", Callback: " +
1363                            req.mLastCallbackTime);
1364                    pw.println("      Elapsed: " + ((time - req.mStartTime) / 1000L) + "s");
1365                    if (req.mLastCallbackTime > 0) {
1366                        pw.println("      CB: " + ((time - req.mLastCallbackTime) / 1000L) + "s");
1367                    }
1368                }
1369            }
1370        }
1371    }
1372
1373    // For Testing
1374    AccountManagerStub mAccountManagerStub;
1375    private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>();
1376
1377    void addServiceIntentForTest(final long accountId, final Intent intent) {
1378        mAccountServiceMap.put(accountId, intent);
1379    }
1380
1381    /**
1382     * We only use the getAccounts() call from AccountManager, so this class wraps that call and
1383     * allows us to build a mock account manager stub in the unit tests
1384     */
1385    static class AccountManagerStub {
1386        private int mNumberOfAccounts;
1387        private final AccountManager mAccountManager;
1388
1389        AccountManagerStub(final Context context) {
1390            if (context != null) {
1391                mAccountManager = AccountManager.get(context);
1392            } else {
1393                mAccountManager = null;
1394            }
1395        }
1396
1397        int getNumberOfAccounts() {
1398            if (mAccountManager != null) {
1399                return mAccountManager.getAccounts().length;
1400            } else {
1401                return mNumberOfAccounts;
1402            }
1403        }
1404
1405        void setNumberOfAccounts(final int numberOfAccounts) {
1406            mNumberOfAccounts = numberOfAccounts;
1407        }
1408    }
1409}
1410