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.NotificationController; 38 import com.android.emailcommon.provider.Account; 39 import com.android.emailcommon.provider.EmailContent; 40 import com.android.emailcommon.provider.EmailContent.Attachment; 41 import com.android.emailcommon.provider.EmailContent.AttachmentColumns; 42 import com.android.emailcommon.provider.EmailContent.Message; 43 import com.android.emailcommon.service.EmailServiceProxy; 44 import com.android.emailcommon.service.EmailServiceStatus; 45 import com.android.emailcommon.service.IEmailServiceCallback; 46 import com.android.emailcommon.utility.AttachmentUtilities; 47 import com.android.emailcommon.utility.Utility; 48 import com.android.mail.providers.UIProvider.AttachmentState; 49 import com.android.mail.utils.LogUtils; 50 import com.google.common.annotations.VisibleForTesting; 51 52 import java.io.File; 53 import java.io.FileDescriptor; 54 import java.io.PrintWriter; 55 import java.util.Collection; 56 import java.util.Comparator; 57 import java.util.HashMap; 58 import java.util.PriorityQueue; 59 import java.util.Queue; 60 import java.util.concurrent.ConcurrentHashMap; 61 import java.util.concurrent.ConcurrentLinkedQueue; 62 63 public 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