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 17 package com.android.email.service; 18 19 import android.accounts.AccountManager; 20 import android.app.AlarmManager; 21 import android.app.PendingIntent; 22 import android.app.Service; 23 import android.content.BroadcastReceiver; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.database.Cursor; 28 import android.net.ConnectivityManager; 29 import android.net.Uri; 30 import android.os.IBinder; 31 import android.os.RemoteException; 32 import android.os.SystemClock; 33 import android.text.format.DateUtils; 34 35 import com.android.email.AttachmentInfo; 36 import com.android.email.EmailConnectivityManager; 37 import com.android.email.NotificationControllerCreatorHolder; 38 import com.android.email.NotificationController; 39 import com.android.emailcommon.provider.Account; 40 import com.android.emailcommon.provider.EmailContent; 41 import com.android.emailcommon.provider.EmailContent.Attachment; 42 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 43 import com.android.emailcommon.provider.EmailContent.Message; 44 import com.android.emailcommon.service.EmailServiceProxy; 45 import com.android.emailcommon.service.EmailServiceStatus; 46 import com.android.emailcommon.service.IEmailServiceCallback; 47 import com.android.emailcommon.utility.AttachmentUtilities; 48 import com.android.emailcommon.utility.Utility; 49 import com.android.mail.providers.UIProvider.AttachmentState; 50 import com.android.mail.utils.LogUtils; 51 import com.google.common.annotations.VisibleForTesting; 52 53 import java.io.File; 54 import java.io.FileDescriptor; 55 import java.io.PrintWriter; 56 import java.util.Collection; 57 import java.util.Comparator; 58 import java.util.HashMap; 59 import java.util.PriorityQueue; 60 import java.util.Queue; 61 import java.util.concurrent.ConcurrentHashMap; 62 import java.util.concurrent.ConcurrentLinkedQueue; 63 64 public 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. debugTrace(final String format, final Object... args)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 DownloadRequest(final int attPriority, final long attId)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 DownloadRequest(final Context context, final Attachment attachment)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 DownloadRequest(final DownloadRequest orig, final long newTime)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 hashCode()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 equals(final Object object)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 compare(DownloadRequest req1, DownloadRequest req2)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 */ addRequest(final DownloadRequest request)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 */ removeRequest(final DownloadRequest request)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 */ getNextRequest()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 */ findRequestById(final long requestId)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 getSize()381 public int getSize() { 382 synchronized (mLock) { 383 return mRequestMap.size(); 384 } 385 } 386 isEmpty()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 setWatchdogAlarm(final Context context, final long delay, final int callbackTimeout)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 setWatchdogAlarm(final Context context)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 onReceive(final Context context, final Intent intent)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 validateDownloadRequest(final DownloadRequest dr, final int callbackTimeout, final long now)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 */ watchdogAlarm(final AttachmentService service, final int callbackTimeout)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 issueNextWatchdogAlarm(final AttachmentService service)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 */ getAttachmentUpdateValues(final Attachment attachment, final int statusCode, final int progress)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 loadAttachmentStatus(final long messageId, final long attachmentId, final int statusCode, final int progress)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 */ attachmentChanged(final Context context, final long id, final int flags)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 onStartCommand(final Intent intent, final int flags, final int startId)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 onCreate()624 public void onCreate() { 625 // Start up our service thread. 626 new Thread(this, "AttachmentService").start(); 627 } 628 629 @Override onBind(final Intent intent)630 public IBinder onBind(final Intent intent) { 631 return null; 632 } 633 634 @Override onDestroy()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 run()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 */ kick()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 */ onChange(final Context context, final Attachment att)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 */ markAttachmentAsFailed(final Attachment att)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 */ markAttachmentAsCompleted(final Attachment att)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 */ processQueue()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 } else { 851 attachment.mFlags = (int) flags; 852 onChange(this, attachment); 853 } 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 */ tryStartDownload(final DownloadRequest req)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 */ startDownload(final EmailServiceProxy service, final DownloadRequest req)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 cancelDownload(final DownloadRequest req)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 */ endDownload(final long attachmentId, final int statusCode)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 */ getDownloadsForAccount(final long accountId)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 */ getAttachmentPriority(final Attachment att)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 */ canPrefetchForAccount(final Account account, final File dir)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 isConnected()1282 boolean isConnected() { 1283 if (mConnectivityManager != null) { 1284 return mConnectivityManager.hasConnectivity(); 1285 } 1286 return false; 1287 } 1288 1289 // For Debugging. dumpInProgressDownloads()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 dump(final FileDescriptor fd, final PrintWriter pw, final String[] args)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 addServiceIntentForTest(final long accountId, final Intent intent)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 AccountManagerStub(final Context context)1389 AccountManagerStub(final Context context) { 1390 if (context != null) { 1391 mAccountManager = AccountManager.get(context); 1392 } else { 1393 mAccountManager = null; 1394 } 1395 } 1396 getNumberOfAccounts()1397 int getNumberOfAccounts() { 1398 if (mAccountManager != null) { 1399 return mAccountManager.getAccounts().length; 1400 } else { 1401 return mNumberOfAccounts; 1402 } 1403 } 1404 setNumberOfAccounts(final int numberOfAccounts)1405 void setNumberOfAccounts(final int numberOfAccounts) { 1406 mNumberOfAccounts = numberOfAccounts; 1407 } 1408 } 1409 } 1410