1 /* 2 * Copyright (C) 2010 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.text.format.DateUtils; 33 import android.util.Log; 34 35 import com.android.email.AttachmentInfo; 36 import com.android.email.Controller.ControllerService; 37 import com.android.email.Email; 38 import com.android.email.EmailConnectivityManager; 39 import com.android.email.NotificationController; 40 import com.android.emailcommon.provider.Account; 41 import com.android.emailcommon.provider.EmailContent; 42 import com.android.emailcommon.provider.EmailContent.Attachment; 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 50 import java.io.File; 51 import java.io.FileDescriptor; 52 import java.io.PrintWriter; 53 import java.util.Comparator; 54 import java.util.HashMap; 55 import java.util.Iterator; 56 import java.util.TreeSet; 57 import java.util.concurrent.ConcurrentHashMap; 58 59 public class AttachmentDownloadService extends Service implements Runnable { 60 public static final String TAG = "AttachmentService"; 61 62 // Our idle time, waiting for notifications; this is something of a failsafe 63 private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS); 64 // How often our watchdog checks for callback timeouts 65 private static final int WATCHDOG_CHECK_INTERVAL = 20 * ((int)DateUtils.SECOND_IN_MILLIS); 66 // How long we'll wait for a callback before canceling a download and retrying 67 private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS); 68 // Try to download an attachment in the background this many times before giving up 69 private static final int MAX_DOWNLOAD_RETRIES = 5; 70 private static final int PRIORITY_NONE = -1; 71 @SuppressWarnings("unused") 72 // Low priority will be used for opportunistic downloads 73 private static final int PRIORITY_BACKGROUND = 0; 74 // Normal priority is for forwarded downloads in outgoing mail 75 private static final int PRIORITY_SEND_MAIL = 1; 76 // High priority is for user requests 77 private static final int PRIORITY_FOREGROUND = 2; 78 79 // Minimum free storage in order to perform prefetch (25% of total memory) 80 private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F; 81 // Maximum prefetch storage (also 25% of total memory) 82 private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F; 83 84 // We can try various values here; I think 2 is completely reasonable as a first pass 85 private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; 86 // Limit on the number of simultaneous downloads per account 87 // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) 88 private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; 89 // Limit on the number of attachments we'll check for background download 90 private static final int MAX_ATTACHMENTS_TO_CHECK = 25; 91 92 private static final String EXTRA_ATTACHMENT = 93 "com.android.email.AttachmentDownloadService.attachment"; 94 95 // sRunningService is only set in the UI thread; it's visibility elsewhere is guaranteed 96 // by the use of "volatile" 97 /*package*/ static volatile AttachmentDownloadService sRunningService = null; 98 99 /*package*/ Context mContext; 100 /*package*/ EmailConnectivityManager mConnectivityManager; 101 102 /*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator()); 103 104 private final HashMap<Long, Intent> mAccountServiceMap = new HashMap<Long, Intent>(); 105 // A map of attachment storage used per account 106 // NOTE: This map is not kept current in terms of deletions (i.e. it stores the last calculated 107 // amount plus the size of any new attachments laoded). If and when we reach the per-account 108 // limit, we recalculate the actual usage 109 /*package*/ final HashMap<Long, Long> mAttachmentStorageMap = new HashMap<Long, Long>(); 110 // A map of attachment ids to the number of failed attempts to download the attachment 111 // NOTE: We do not want to persist this. This allows us to retry background downloading 112 // if any transient network errors are fixed & and the app is restarted 113 /* package */ final HashMap<Long, Integer> mAttachmentFailureMap = new HashMap<Long, Integer>(); 114 private final ServiceCallback mServiceCallback = new ServiceCallback(); 115 116 private final Object mLock = new Object(); 117 private volatile boolean mStop = false; 118 119 /*package*/ AccountManagerStub mAccountManagerStub; 120 121 /** 122 * We only use the getAccounts() call from AccountManager, so this class wraps that call and 123 * allows us to build a mock account manager stub in the unit tests 124 */ 125 /*package*/ static class AccountManagerStub { 126 private int mNumberOfAccounts; 127 private final AccountManager mAccountManager; 128 AccountManagerStub(Context context)129 AccountManagerStub(Context context) { 130 if (context != null) { 131 mAccountManager = AccountManager.get(context); 132 } else { 133 mAccountManager = null; 134 } 135 } 136 getNumberOfAccounts()137 /*package*/ int getNumberOfAccounts() { 138 if (mAccountManager != null) { 139 return mAccountManager.getAccounts().length; 140 } else { 141 return mNumberOfAccounts; 142 } 143 } 144 setNumberOfAccounts(int numberOfAccounts)145 /*package*/ void setNumberOfAccounts(int numberOfAccounts) { 146 mNumberOfAccounts = numberOfAccounts; 147 } 148 } 149 150 /** 151 * Watchdog alarm receiver; responsible for making sure that downloads in progress are not 152 * stalled, as determined by the timing of the most recent service callback 153 */ 154 public static class Watchdog extends BroadcastReceiver { 155 @Override onReceive(final Context context, Intent intent)156 public void onReceive(final Context context, Intent intent) { 157 new Thread(new Runnable() { 158 @Override 159 public void run() { 160 watchdogAlarm(); 161 } 162 }, "AttachmentDownloadService Watchdog").start(); 163 } 164 } 165 166 public static class DownloadRequest { 167 final int priority; 168 final long time; 169 final long attachmentId; 170 final long messageId; 171 final long accountId; 172 boolean inProgress = false; 173 int lastStatusCode; 174 int lastProgress; 175 long lastCallbackTime; 176 long startTime; 177 DownloadRequest(Context context, Attachment attachment)178 private DownloadRequest(Context context, Attachment attachment) { 179 attachmentId = attachment.mId; 180 Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey); 181 if (msg != null) { 182 accountId = msg.mAccountKey; 183 messageId = msg.mId; 184 } else { 185 accountId = messageId = -1; 186 } 187 priority = getPriority(attachment); 188 time = System.currentTimeMillis(); 189 } 190 191 @Override hashCode()192 public int hashCode() { 193 return (int)attachmentId; 194 } 195 196 /** 197 * Two download requests are equals if their attachment id's are equals 198 */ 199 @Override equals(Object object)200 public boolean equals(Object object) { 201 if (!(object instanceof DownloadRequest)) return false; 202 DownloadRequest req = (DownloadRequest)object; 203 return req.attachmentId == attachmentId; 204 } 205 } 206 207 /** 208 * Comparator class for the download set; we first compare by priority. Requests with equal 209 * priority are compared by the time the request was created (older requests come first) 210 */ 211 /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> { 212 @Override compare(DownloadRequest req1, DownloadRequest req2)213 public int compare(DownloadRequest req1, DownloadRequest req2) { 214 int res; 215 if (req1.priority != req2.priority) { 216 res = (req1.priority < req2.priority) ? -1 : 1; 217 } else { 218 if (req1.time == req2.time) { 219 res = 0; 220 } else { 221 res = (req1.time > req2.time) ? -1 : 1; 222 } 223 } 224 return res; 225 } 226 } 227 228 /** 229 * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the 230 * time of the request. Higher priority requests 231 * are always processed first; among equals, the oldest request is processed first. The 232 * priority key represents this ordering. Note: All methods that change the attachment map are 233 * synchronized on the map itself 234 */ 235 /*package*/ class DownloadSet extends TreeSet<DownloadRequest> { 236 private static final long serialVersionUID = 1L; 237 private PendingIntent mWatchdogPendingIntent; 238 DownloadSet(Comparator<? super DownloadRequest> comparator)239 /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) { 240 super(comparator); 241 } 242 243 /** 244 * Maps attachment id to DownloadRequest 245 */ 246 /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress = 247 new ConcurrentHashMap<Long, DownloadRequest>(); 248 249 /** 250 * onChange is called by the AttachmentReceiver upon receipt of a valid notification from 251 * EmailProvider that an attachment has been inserted or modified. It's not strictly 252 * necessary that we detect a deleted attachment, as the code always checks for the 253 * existence of an attachment before acting on it. 254 */ onChange(Context context, Attachment att)255 public synchronized void onChange(Context context, Attachment att) { 256 DownloadRequest req = findDownloadRequest(att.mId); 257 long priority = getPriority(att); 258 if (priority == PRIORITY_NONE) { 259 if (Email.DEBUG) { 260 Log.d(TAG, "== Attachment changed: " + att.mId); 261 } 262 // In this case, there is no download priority for this attachment 263 if (req != null) { 264 // If it exists in the map, remove it 265 // NOTE: We don't yet support deleting downloads in progress 266 if (Email.DEBUG) { 267 Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing"); 268 } 269 remove(req); 270 } 271 } else { 272 // Ignore changes that occur during download 273 if (mDownloadsInProgress.containsKey(att.mId)) return; 274 // If this is new, add the request to the queue 275 if (req == null) { 276 req = new DownloadRequest(context, att); 277 add(req); 278 } 279 // If the request already existed, we'll update the priority (so that the time is 280 // up-to-date); otherwise, we create a new request 281 if (Email.DEBUG) { 282 Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " + 283 req.priority + ", priority time " + req.time); 284 } 285 } 286 // Process the queue if we're in a wait 287 kick(); 288 } 289 290 /** 291 * Find a queued DownloadRequest, given the attachment's id 292 * @param id the id of the attachment 293 * @return the DownloadRequest for that attachment (or null, if none) 294 */ findDownloadRequest(long id)295 /*package*/ synchronized DownloadRequest findDownloadRequest(long id) { 296 Iterator<DownloadRequest> iterator = iterator(); 297 while(iterator.hasNext()) { 298 DownloadRequest req = iterator.next(); 299 if (req.attachmentId == id) { 300 return req; 301 } 302 } 303 return null; 304 } 305 306 @Override isEmpty()307 public synchronized boolean isEmpty() { 308 return super.isEmpty() && mDownloadsInProgress.isEmpty(); 309 } 310 311 /** 312 * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing 313 * the limit on maximum downloads 314 */ processQueue()315 /*package*/ synchronized void processQueue() { 316 if (Email.DEBUG) { 317 Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries"); 318 } 319 320 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 321 // First, start up any required downloads, in priority order 322 while (iterator.hasNext() && 323 (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) { 324 DownloadRequest req = iterator.next(); 325 // Enforce per-account limit here 326 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { 327 if (Email.DEBUG) { 328 Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + 329 req.accountId); 330 } 331 continue; 332 } 333 334 if (!req.inProgress) { 335 mDownloadSet.tryStartDownload(req); 336 } 337 } 338 339 // Don't prefetch if background downloading is disallowed 340 EmailConnectivityManager ecm = mConnectivityManager; 341 if (ecm == null) return; 342 if (!ecm.isAutoSyncAllowed()) return; 343 // Don't prefetch unless we're on a WiFi network 344 if (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI) { 345 return; 346 } 347 // Then, try opportunistic download of appropriate attachments 348 int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size(); 349 // Always leave one slot for user requested download 350 if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) { 351 // We'll load up the newest 25 attachments that aren't loaded or queued 352 Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI, 353 MAX_ATTACHMENTS_TO_CHECK); 354 Cursor c = mContext.getContentResolver().query(lookupUri, AttachmentInfo.PROJECTION, 355 EmailContent.Attachment.PRECACHE_INBOX_SELECTION, 356 null, Attachment.RECORD_ID + " DESC"); 357 File cacheDir = mContext.getCacheDir(); 358 try { 359 while (c.moveToNext()) { 360 long accountKey = c.getLong(AttachmentInfo.COLUMN_ACCOUNT_KEY); 361 long id = c.getLong(AttachmentInfo.COLUMN_ID); 362 Account account = Account.restoreAccountWithId(mContext, accountKey); 363 if (account == null) { 364 // Clean up this orphaned attachment; there's no point in keeping it 365 // around; then try to find another one 366 EmailContent.delete(mContext, Attachment.CONTENT_URI, id); 367 } else if (canPrefetchForAccount(account, cacheDir)) { 368 // Check that the attachment meets system requirements for download 369 AttachmentInfo info = new AttachmentInfo(mContext, c); 370 if (info.isEligibleForDownload()) { 371 Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 372 if (att != null) { 373 Integer tryCount; 374 tryCount = mAttachmentFailureMap.get(att.mId); 375 if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) { 376 // move onto the next attachment 377 continue; 378 } 379 // Start this download and we're done 380 DownloadRequest req = new DownloadRequest(mContext, att); 381 mDownloadSet.tryStartDownload(req); 382 break; 383 } 384 } 385 } 386 } 387 } finally { 388 c.close(); 389 } 390 } 391 } 392 393 /** 394 * Count the number of running downloads in progress for this account 395 * @param accountId the id of the account 396 * @return the count of running downloads 397 */ downloadsForAccount(long accountId)398 /*package*/ synchronized int downloadsForAccount(long accountId) { 399 int count = 0; 400 for (DownloadRequest req: mDownloadsInProgress.values()) { 401 if (req.accountId == accountId) { 402 count++; 403 } 404 } 405 return count; 406 } 407 408 /** 409 * Watchdog for downloads; we use this in case we are hanging on a download, which might 410 * have failed silently (the connection dropped, for example) 411 */ onWatchdogAlarm()412 private void onWatchdogAlarm() { 413 // If our service instance is gone, just leave 414 if (mStop) { 415 return; 416 } 417 long now = System.currentTimeMillis(); 418 for (DownloadRequest req: mDownloadsInProgress.values()) { 419 // Check how long it's been since receiving a callback 420 long timeSinceCallback = now - req.lastCallbackTime; 421 if (timeSinceCallback > CALLBACK_TIMEOUT) { 422 if (Email.DEBUG) { 423 Log.d(TAG, "== Download of " + req.attachmentId + " timed out"); 424 } 425 cancelDownload(req); 426 } 427 } 428 // Check whether we can start new downloads... 429 if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) { 430 processQueue(); 431 } 432 // If there are downloads in progress, reset alarm 433 if (!mDownloadsInProgress.isEmpty()) { 434 if (Email.DEBUG) { 435 Log.d(TAG, "Reschedule watchdog..."); 436 } 437 setWatchdogAlarm(); 438 } 439 } 440 441 /** 442 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 443 * parameter 444 * @param req the DownloadRequest 445 * @return whether or not the download was started 446 */ tryStartDownload(DownloadRequest req)447 /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { 448 Intent intent = getServiceIntentForAccount(req.accountId); 449 if (intent == null) return false; 450 451 // Do not download the same attachment multiple times 452 boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null; 453 if (alreadyInProgress) return false; 454 455 try { 456 if (Email.DEBUG) { 457 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 458 } 459 startDownload(intent, req); 460 } catch (RemoteException e) { 461 // TODO: Consider whether we need to do more in this case... 462 // For now, fix up our data to reflect the failure 463 cancelDownload(req); 464 } 465 return true; 466 } 467 getDownloadInProgress(long attachmentId)468 private synchronized DownloadRequest getDownloadInProgress(long attachmentId) { 469 return mDownloadsInProgress.get(attachmentId); 470 } 471 setWatchdogAlarm()472 private void setWatchdogAlarm() { 473 // Lazily initialize the pending intent 474 if (mWatchdogPendingIntent == null) { 475 Intent intent = new Intent(mContext, Watchdog.class); 476 mWatchdogPendingIntent = 477 PendingIntent.getBroadcast(mContext, 0, intent, 0); 478 } 479 // Set the alarm 480 AlarmManager am = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE); 481 am.set(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, 482 mWatchdogPendingIntent); 483 } 484 485 /** 486 * Do the work of starting an attachment download using the EmailService interface, and 487 * set our watchdog alarm 488 * 489 * @param serviceClass the class that will attempt the download 490 * @param req the DownloadRequest 491 * @throws RemoteException 492 */ startDownload(Intent intent, DownloadRequest req)493 private void startDownload(Intent intent, DownloadRequest req) 494 throws RemoteException { 495 req.startTime = System.currentTimeMillis(); 496 req.inProgress = true; 497 mDownloadsInProgress.put(req.attachmentId, req); 498 EmailServiceProxy proxy = 499 new EmailServiceProxy(mContext, intent, mServiceCallback); 500 proxy.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND); 501 setWatchdogAlarm(); 502 } 503 cancelDownload(DownloadRequest req)504 private void cancelDownload(DownloadRequest req) { 505 mDownloadsInProgress.remove(req.attachmentId); 506 req.inProgress = false; 507 } 508 509 /** 510 * Called when a download is finished; we get notified of this via our EmailServiceCallback 511 * @param attachmentId the id of the attachment whose download is finished 512 * @param statusCode the EmailServiceStatus code returned by the Service 513 */ endDownload(long attachmentId, int statusCode)514 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 515 // Say we're no longer downloading this 516 mDownloadsInProgress.remove(attachmentId); 517 518 // TODO: This code is conservative and treats connection issues as failures. 519 // Since we have no mechanism to throttle reconnection attempts, it makes 520 // sense to be cautious here. Once logic is in place to prevent connecting 521 // in a tight loop, we can exclude counting connection issues as "failures". 522 523 // Update the attachment failure list if needed 524 Integer downloadCount; 525 downloadCount = mAttachmentFailureMap.remove(attachmentId); 526 if (statusCode != EmailServiceStatus.SUCCESS) { 527 if (downloadCount == null) { 528 downloadCount = 0; 529 } 530 downloadCount += 1; 531 mAttachmentFailureMap.put(attachmentId, downloadCount); 532 } 533 534 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 535 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 536 // If this needs to be retried, just process the queue again 537 if (Email.DEBUG) { 538 Log.d(TAG, "== The download for attachment #" + attachmentId + 539 " will be retried"); 540 } 541 if (req != null) { 542 req.inProgress = false; 543 } 544 kick(); 545 return; 546 } 547 548 // If the request is still in the queue, remove it 549 if (req != null) { 550 remove(req); 551 } 552 if (Email.DEBUG) { 553 long secs = 0; 554 if (req != null) { 555 secs = (System.currentTimeMillis() - req.time) / 1000; 556 } 557 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 558 "Error " + statusCode; 559 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 560 " seconds from request, status: " + status); 561 } 562 563 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 564 if (attachment != null) { 565 long accountId = attachment.mAccountKey; 566 // Update our attachment storage for this account 567 Long currentStorage = mAttachmentStorageMap.get(accountId); 568 if (currentStorage == null) { 569 currentStorage = 0L; 570 } 571 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); 572 boolean deleted = false; 573 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 574 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 575 // If this is a forwarding download, and the attachment doesn't exist (or 576 // can't be downloaded) delete it from the outgoing message, lest that 577 // message never get sent 578 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 579 // TODO: Talk to UX about whether this is even worth doing 580 NotificationController nc = NotificationController.getInstance(mContext); 581 nc.showDownloadForwardFailedNotification(attachment); 582 deleted = true; 583 } 584 // If we're an attachment on forwarded mail, and if we're not still blocked, 585 // try to send pending mail now (as mediated by MailService) 586 if ((req != null) && 587 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 588 if (Email.DEBUG) { 589 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 590 } 591 MailService.actionSendPendingMail(mContext, req.accountId); 592 } 593 } 594 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) { 595 Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 596 if (msg == null) { 597 // If there's no associated message, delete the attachment 598 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 599 } else { 600 // If there really is a message, retry 601 kick(); 602 return; 603 } 604 } else if (!deleted) { 605 // Clear the download flags, since we're done for now. Note that this happens 606 // only for non-recoverable errors. When these occur for forwarded mail, we can 607 // ignore it and continue; otherwise, it was either 1) a user request, in which 608 // case the user can retry manually or 2) an opportunistic download, in which 609 // case the download wasn't critical 610 ContentValues cv = new ContentValues(); 611 int flags = 612 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 613 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 614 attachment.update(mContext, cv); 615 } 616 } 617 // Process the queue 618 kick(); 619 } 620 } 621 622 /** 623 * Calculate the download priority of an Attachment. A priority of zero means that the 624 * attachment is not marked for download. 625 * @param att the Attachment 626 * @return the priority key of the Attachment 627 */ getPriority(Attachment att)628 private static int getPriority(Attachment att) { 629 int priorityClass = PRIORITY_NONE; 630 int flags = att.mFlags; 631 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 632 priorityClass = PRIORITY_SEND_MAIL; 633 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 634 priorityClass = PRIORITY_FOREGROUND; 635 } 636 return priorityClass; 637 } 638 kick()639 private void kick() { 640 synchronized(mLock) { 641 mLock.notify(); 642 } 643 } 644 645 /** 646 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 647 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 648 * single callback that's defined by the EmailServiceCallback interface. 649 */ 650 private class ServiceCallback extends IEmailServiceCallback.Stub { 651 @Override loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress)652 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 653 int progress) { 654 // Record status and progress 655 DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId); 656 if (req != null) { 657 if (Email.DEBUG) { 658 String code; 659 switch(statusCode) { 660 case EmailServiceStatus.SUCCESS: code = "Success"; break; 661 case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break; 662 default: code = Integer.toString(statusCode); break; 663 } 664 if (statusCode != EmailServiceStatus.IN_PROGRESS) { 665 Log.d(TAG, ">> Attachment " + attachmentId + ": " + code); 666 } else if (progress >= (req.lastProgress + 15)) { 667 Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%"); 668 } 669 } 670 req.lastStatusCode = statusCode; 671 req.lastProgress = progress; 672 req.lastCallbackTime = System.currentTimeMillis(); 673 } 674 switch (statusCode) { 675 case EmailServiceStatus.IN_PROGRESS: 676 break; 677 default: 678 mDownloadSet.endDownload(attachmentId, statusCode); 679 break; 680 } 681 } 682 683 @Override sendMessageStatus(long messageId, String subject, int statusCode, int progress)684 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 685 throws RemoteException { 686 } 687 688 @Override syncMailboxListStatus(long accountId, int statusCode, int progress)689 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 690 throws RemoteException { 691 } 692 693 @Override syncMailboxStatus(long mailboxId, int statusCode, int progress)694 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 695 throws RemoteException { 696 } 697 698 @Override loadMessageStatus(long messageId, int statusCode, int progress)699 public void loadMessageStatus(long messageId, int statusCode, int progress) 700 throws RemoteException { 701 } 702 } 703 704 /** 705 * Return an Intent to be used used based on the account type of the provided account id. We 706 * cache the results to avoid repeated database access 707 * @param accountId the id of the account 708 * @return the Intent to be used for the account or null (if the account no longer exists) 709 */ getServiceIntentForAccount(long accountId)710 private synchronized Intent getServiceIntentForAccount(long accountId) { 711 // TODO: We should have some more data-driven way of determining the service intent. 712 Intent serviceIntent = mAccountServiceMap.get(accountId); 713 if (serviceIntent == null) { 714 String protocol = Account.getProtocol(mContext, accountId); 715 if (protocol == null) return null; 716 serviceIntent = new Intent(mContext, ControllerService.class); 717 if (protocol.equals("eas")) { 718 serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT); 719 } 720 mAccountServiceMap.put(accountId, serviceIntent); 721 } 722 return serviceIntent; 723 } 724 addServiceIntentForTest(long accountId, Intent intent)725 /*package*/ void addServiceIntentForTest(long accountId, Intent intent) { 726 mAccountServiceMap.put(accountId, intent); 727 } 728 onChange(Attachment att)729 /*package*/ void onChange(Attachment att) { 730 mDownloadSet.onChange(this, att); 731 } 732 isQueued(long attachmentId)733 /*package*/ boolean isQueued(long attachmentId) { 734 return mDownloadSet.findDownloadRequest(attachmentId) != null; 735 } 736 getSize()737 /*package*/ int getSize() { 738 return mDownloadSet.size(); 739 } 740 dequeue(long attachmentId)741 /*package*/ boolean dequeue(long attachmentId) { 742 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 743 if (req != null) { 744 if (Email.DEBUG) { 745 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 746 } 747 mDownloadSet.remove(req); 748 return true; 749 } 750 return false; 751 } 752 753 /** 754 * Ask the service for the number of items in the download queue 755 * @return the number of items queued for download 756 */ getQueueSize()757 public static int getQueueSize() { 758 AttachmentDownloadService service = sRunningService; 759 if (service != null) { 760 return service.getSize(); 761 } 762 return 0; 763 } 764 765 /** 766 * Ask the service whether a particular attachment is queued for download 767 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 768 * @return whether or not the attachment is queued for download 769 */ isAttachmentQueued(long attachmentId)770 public static boolean isAttachmentQueued(long attachmentId) { 771 AttachmentDownloadService service = sRunningService; 772 if (service != null) { 773 return service.isQueued(attachmentId); 774 } 775 return false; 776 } 777 778 /** 779 * Ask the service to remove an attachment from the download queue 780 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 781 * @return whether or not the attachment was removed from the queue 782 */ cancelQueuedAttachment(long attachmentId)783 public static boolean cancelQueuedAttachment(long attachmentId) { 784 AttachmentDownloadService service = sRunningService; 785 if (service != null) { 786 return service.dequeue(attachmentId); 787 } 788 return false; 789 } 790 watchdogAlarm()791 public static void watchdogAlarm() { 792 AttachmentDownloadService service = sRunningService; 793 if (service != null) { 794 service.mDownloadSet.onWatchdogAlarm(); 795 } 796 } 797 798 /** 799 * Called directly by EmailProvider whenever an attachment is inserted or changed 800 * @param context the caller's context 801 * @param id the attachment's id 802 * @param flags the new flags for the attachment 803 */ attachmentChanged(final Context context, final long id, final int flags)804 public static void attachmentChanged(final Context context, final long id, final int flags) { 805 Utility.runAsync(new Runnable() { 806 @Override 807 public void run() { 808 Attachment attachment = Attachment.restoreAttachmentWithId(context, id); 809 if (attachment != null) { 810 // Store the flags we got from EmailProvider; given that all of this 811 // activity is asynchronous, we need to use the newest data from 812 // EmailProvider 813 attachment.mFlags = flags; 814 Intent intent = new Intent(context, AttachmentDownloadService.class); 815 intent.putExtra(EXTRA_ATTACHMENT, attachment); 816 context.startService(intent); 817 } 818 }}); 819 } 820 821 /** 822 * Determine whether an attachment can be prefetched for the given account 823 * @return true if download is allowed, false otherwise 824 */ canPrefetchForAccount(Account account, File dir)825 public boolean canPrefetchForAccount(Account account, File dir) { 826 // Check account, just in case 827 if (account == null) return false; 828 // First, check preference and quickly return if prefetch isn't allowed 829 if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false; 830 831 long totalStorage = dir.getTotalSpace(); 832 long usableStorage = dir.getUsableSpace(); 833 long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); 834 835 // If there's not enough overall storage available, stop now 836 if (usableStorage < minAvailable) { 837 return false; 838 } 839 840 int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); 841 long perAccountMaxStorage = 842 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); 843 844 // Retrieve our idea of currently used attachment storage; since we don't track deletions, 845 // this number is the "worst case". If the number is greater than what's allowed per 846 // account, we walk the directory to determine the actual number 847 Long accountStorage = mAttachmentStorageMap.get(account.mId); 848 if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { 849 // Calculate the exact figure for attachment storage for this account 850 accountStorage = 0L; 851 File[] files = dir.listFiles(); 852 if (files != null) { 853 for (File file : files) { 854 accountStorage += file.length(); 855 } 856 } 857 // Cache the value 858 mAttachmentStorageMap.put(account.mId, accountStorage); 859 } 860 861 // Return true if we're using less than the maximum per account 862 if (accountStorage < perAccountMaxStorage) { 863 return true; 864 } else { 865 if (Email.DEBUG) { 866 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " + 867 accountStorage + ", limit " + perAccountMaxStorage); 868 } 869 return false; 870 } 871 } 872 873 @Override run()874 public void run() { 875 // These fields are only used within the service thread 876 mContext = this; 877 mConnectivityManager = new EmailConnectivityManager(this, TAG); 878 mAccountManagerStub = new AccountManagerStub(this); 879 880 // Run through all attachments in the database that require download and add them to 881 // the queue 882 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 883 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 884 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 885 new String[] {Integer.toString(mask)}, null); 886 try { 887 Log.d(TAG, "Count: " + c.getCount()); 888 while (c.moveToNext()) { 889 Attachment attachment = Attachment.restoreAttachmentWithId( 890 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 891 if (attachment != null) { 892 mDownloadSet.onChange(this, attachment); 893 } 894 } 895 } catch (Exception e) { 896 e.printStackTrace(); 897 } 898 finally { 899 c.close(); 900 } 901 902 // Loop until stopped, with a 30 minute wait loop 903 while (!mStop) { 904 // Here's where we run our attachment loading logic... 905 mConnectivityManager.waitForConnectivity(); 906 mDownloadSet.processQueue(); 907 if (mDownloadSet.isEmpty()) { 908 Log.d(TAG, "*** All done; shutting down service"); 909 stopSelf(); 910 break; 911 } 912 synchronized(mLock) { 913 try { 914 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 915 } catch (InterruptedException e) { 916 // That's ok; we'll just keep looping 917 } 918 } 919 } 920 921 // Unregister now that we're done 922 if (mConnectivityManager != null) { 923 mConnectivityManager.unregister(); 924 } 925 } 926 927 @Override onStartCommand(Intent intent, int flags, int startId)928 public int onStartCommand(Intent intent, int flags, int startId) { 929 if (sRunningService == null) { 930 sRunningService = this; 931 } 932 if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) { 933 Attachment att = (Attachment)intent.getParcelableExtra(EXTRA_ATTACHMENT); 934 onChange(att); 935 } 936 return Service.START_STICKY; 937 } 938 939 @Override onCreate()940 public void onCreate() { 941 // Start up our service thread 942 new Thread(this, "AttachmentDownloadService").start(); 943 } 944 @Override onBind(Intent intent)945 public IBinder onBind(Intent intent) { 946 return null; 947 } 948 949 @Override onDestroy()950 public void onDestroy() { 951 // Mark this instance of the service as stopped 952 mStop = true; 953 if (sRunningService != null) { 954 kick(); 955 sRunningService = null; 956 } 957 if (mConnectivityManager != null) { 958 mConnectivityManager.unregister(); 959 mConnectivityManager = null; 960 } 961 } 962 963 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)964 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 965 pw.println("AttachmentDownloadService"); 966 long time = System.currentTimeMillis(); 967 synchronized(mDownloadSet) { 968 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 969 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 970 // First, start up any required downloads, in priority order 971 while (iterator.hasNext()) { 972 DownloadRequest req = iterator.next(); 973 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 974 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 975 (req.inProgress ? " [In progress]" : "")); 976 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId); 977 if (att == null) { 978 pw.println(" Attachment not in database?"); 979 } else if (att.mFileName != null) { 980 String fileName = att.mFileName; 981 String suffix = "[none]"; 982 int lastDot = fileName.lastIndexOf('.'); 983 if (lastDot >= 0) { 984 suffix = fileName.substring(lastDot); 985 } 986 pw.print(" Suffix: " + suffix); 987 if (att.mContentUri != null) { 988 pw.print(" ContentUri: " + att.mContentUri); 989 } 990 pw.print(" Mime: "); 991 if (att.mMimeType != null) { 992 pw.print(att.mMimeType); 993 } else { 994 pw.print(AttachmentUtilities.inferMimeType(fileName, null)); 995 pw.print(" [inferred]"); 996 } 997 pw.println(" Size: " + att.mSize); 998 } 999 if (req.inProgress) { 1000 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 1001 req.lastProgress); 1002 pw.println(" Started: " + req.startTime + ", Callback: " + 1003 req.lastCallbackTime); 1004 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 1005 if (req.lastCallbackTime > 0) { 1006 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 1007 } 1008 } 1009 } 1010 } 1011 } 1012 } 1013