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