AttachmentDownloadService.java revision 2a5080ef51ba236078e10b8bacd575c8c27678e0
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 17package com.android.email.service; 18 19import android.accounts.AccountManager; 20import android.app.AlarmManager; 21import android.app.PendingIntent; 22import android.app.Service; 23import android.content.BroadcastReceiver; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.database.Cursor; 28import android.net.ConnectivityManager; 29import android.net.Uri; 30import android.os.IBinder; 31import android.os.RemoteException; 32import android.text.format.DateUtils; 33import android.util.Log; 34 35import com.android.email.AttachmentInfo; 36import com.android.email.Controller.ControllerService; 37import com.android.email.Email; 38import com.android.email.EmailConnectivityManager; 39import com.android.email.NotificationController; 40import com.android.emailcommon.provider.Account; 41import com.android.emailcommon.provider.EmailContent; 42import com.android.emailcommon.provider.EmailContent.Attachment; 43import com.android.emailcommon.provider.EmailContent.Message; 44import com.android.emailcommon.service.EmailServiceProxy; 45import com.android.emailcommon.service.EmailServiceStatus; 46import com.android.emailcommon.service.IEmailServiceCallback; 47import com.android.emailcommon.utility.AttachmentUtilities; 48import com.android.emailcommon.utility.Utility; 49 50import java.io.File; 51import java.io.FileDescriptor; 52import java.io.PrintWriter; 53import java.util.Comparator; 54import java.util.HashMap; 55import java.util.Iterator; 56import java.util.TreeSet; 57import java.util.concurrent.ConcurrentHashMap; 58 59public 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 = 15 * ((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 129 AccountManagerStub(Context context) { 130 if (context != null) { 131 mAccountManager = AccountManager.get(context); 132 } else { 133 mAccountManager = null; 134 } 135 } 136 137 /*package*/ int getNumberOfAccounts() { 138 if (mAccountManager != null) { 139 return mAccountManager.getAccounts().length; 140 } else { 141 return mNumberOfAccounts; 142 } 143 } 144 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 156 public void onReceive(final Context context, Intent intent) { 157 new Thread(new Runnable() { 158 public void run() { 159 watchdogAlarm(); 160 } 161 }, "AttachmentDownloadService Watchdog").start(); 162 } 163 } 164 165 public static class DownloadRequest { 166 final int priority; 167 final long time; 168 final long attachmentId; 169 final long messageId; 170 final long accountId; 171 boolean inProgress = false; 172 int lastStatusCode; 173 int lastProgress; 174 long lastCallbackTime; 175 long startTime; 176 177 private DownloadRequest(Context context, Attachment attachment) { 178 attachmentId = attachment.mId; 179 Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey); 180 if (msg != null) { 181 accountId = msg.mAccountKey; 182 messageId = msg.mId; 183 } else { 184 accountId = messageId = -1; 185 } 186 priority = getPriority(attachment); 187 time = System.currentTimeMillis(); 188 } 189 190 @Override 191 public int hashCode() { 192 return (int)attachmentId; 193 } 194 195 /** 196 * Two download requests are equals if their attachment id's are equals 197 */ 198 @Override 199 public boolean equals(Object object) { 200 if (!(object instanceof DownloadRequest)) return false; 201 DownloadRequest req = (DownloadRequest)object; 202 return req.attachmentId == attachmentId; 203 } 204 } 205 206 /** 207 * Comparator class for the download set; we first compare by priority. Requests with equal 208 * priority are compared by the time the request was created (older requests come first) 209 */ 210 /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> { 211 @Override 212 public int compare(DownloadRequest req1, DownloadRequest req2) { 213 int res; 214 if (req1.priority != req2.priority) { 215 res = (req1.priority < req2.priority) ? -1 : 1; 216 } else { 217 if (req1.time == req2.time) { 218 res = 0; 219 } else { 220 res = (req1.time > req2.time) ? -1 : 1; 221 } 222 } 223 return res; 224 } 225 } 226 227 /** 228 * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the 229 * time of the request. Higher priority requests 230 * are always processed first; among equals, the oldest request is processed first. The 231 * priority key represents this ordering. Note: All methods that change the attachment map are 232 * synchronized on the map itself 233 */ 234 /*package*/ class DownloadSet extends TreeSet<DownloadRequest> { 235 private static final long serialVersionUID = 1L; 236 private PendingIntent mWatchdogPendingIntent; 237 private AlarmManager mAlarmManager; 238 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 */ 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 */ 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 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 */ 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 */ 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 private void cancelWatchdogAlarm() { 409 if (mAlarmManager != null && mWatchdogPendingIntent != null) { 410 mAlarmManager.cancel(mWatchdogPendingIntent); 411 } 412 } 413 414 /** 415 * Watchdog for downloads; we use this in case we are hanging on a download, which might 416 * have failed silently (the connection dropped, for example) 417 */ 418 private void onWatchdogAlarm() { 419 // If our service instance is gone, just leave (but cancel alarm first!) 420 if (mStop) { 421 cancelWatchdogAlarm(); 422 return; 423 } 424 long now = System.currentTimeMillis(); 425 for (DownloadRequest req: mDownloadsInProgress.values()) { 426 // Check how long it's been since receiving a callback 427 long timeSinceCallback = now - req.lastCallbackTime; 428 if (timeSinceCallback > CALLBACK_TIMEOUT) { 429 if (Email.DEBUG) { 430 Log.d(TAG, "== Download of " + req.attachmentId + " timed out"); 431 } 432 cancelDownload(req); 433 } 434 } 435 // If there are downloads in progress, reset alarm 436 if (mDownloadsInProgress.isEmpty()) { 437 cancelWatchdogAlarm(); 438 } 439 // Check whether we can start new downloads... 440 if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) { 441 processQueue(); 442 } 443 } 444 445 /** 446 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 447 * parameter 448 * @param req the DownloadRequest 449 * @return whether or not the download was started 450 */ 451 /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { 452 Intent intent = getServiceIntentForAccount(req.accountId); 453 if (intent == null) return false; 454 455 // Do not download the same attachment multiple times 456 boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null; 457 if (alreadyInProgress) return false; 458 459 try { 460 if (Email.DEBUG) { 461 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 462 } 463 startDownload(intent, req); 464 } catch (RemoteException e) { 465 // TODO: Consider whether we need to do more in this case... 466 // For now, fix up our data to reflect the failure 467 cancelDownload(req); 468 } 469 return true; 470 } 471 472 private synchronized DownloadRequest getDownloadInProgress(long attachmentId) { 473 return mDownloadsInProgress.get(attachmentId); 474 } 475 476 /** 477 * Do the work of starting an attachment download using the EmailService interface, and 478 * set our watchdog alarm 479 * 480 * @param serviceClass the class that will attempt the download 481 * @param req the DownloadRequest 482 * @throws RemoteException 483 */ 484 private void startDownload(Intent intent, DownloadRequest req) 485 throws RemoteException { 486 req.startTime = System.currentTimeMillis(); 487 req.inProgress = true; 488 mDownloadsInProgress.put(req.attachmentId, req); 489 EmailServiceProxy proxy = 490 new EmailServiceProxy(mContext, intent, mServiceCallback); 491 proxy.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND); 492 // Lazily initialize our (reusable) pending intent 493 if (mWatchdogPendingIntent == null) { 494 createWatchdogPendingIntent(mContext); 495 } 496 // Set the alarm 497 mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, 498 System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL, 499 mWatchdogPendingIntent); 500 } 501 502 /*package*/ void createWatchdogPendingIntent(Context context) { 503 Intent alarmIntent = new Intent(context, Watchdog.class); 504 mWatchdogPendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0); 505 mAlarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 506 } 507 508 private void cancelDownload(DownloadRequest req) { 509 mDownloadsInProgress.remove(req.attachmentId); 510 req.inProgress = false; 511 } 512 513 /** 514 * Called when a download is finished; we get notified of this via our EmailServiceCallback 515 * @param attachmentId the id of the attachment whose download is finished 516 * @param statusCode the EmailServiceStatus code returned by the Service 517 */ 518 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 519 // Say we're no longer downloading this 520 mDownloadsInProgress.remove(attachmentId); 521 522 // TODO: This code is conservative and treats connection issues as failures. 523 // Since we have no mechanism to throttle reconnection attempts, it makes 524 // sense to be cautious here. Once logic is in place to prevent connecting 525 // in a tight loop, we can exclude counting connection issues as "failures". 526 527 // Update the attachment failure list if needed 528 Integer downloadCount; 529 downloadCount = mAttachmentFailureMap.remove(attachmentId); 530 if (statusCode != EmailServiceStatus.SUCCESS) { 531 if (downloadCount == null) { 532 downloadCount = 0; 533 } 534 downloadCount += 1; 535 mAttachmentFailureMap.put(attachmentId, downloadCount); 536 } 537 538 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 539 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 540 // If this needs to be retried, just process the queue again 541 if (Email.DEBUG) { 542 Log.d(TAG, "== The download for attachment #" + attachmentId + 543 " will be retried"); 544 } 545 if (req != null) { 546 req.inProgress = false; 547 } 548 kick(); 549 return; 550 } 551 552 // If the request is still in the queue, remove it 553 if (req != null) { 554 remove(req); 555 } 556 if (Email.DEBUG) { 557 long secs = 0; 558 if (req != null) { 559 secs = (System.currentTimeMillis() - req.time) / 1000; 560 } 561 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 562 "Error " + statusCode; 563 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 564 " seconds from request, status: " + status); 565 } 566 567 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 568 if (attachment != null) { 569 long accountId = attachment.mAccountKey; 570 // Update our attachment storage for this account 571 Long currentStorage = mAttachmentStorageMap.get(accountId); 572 if (currentStorage == null) { 573 currentStorage = 0L; 574 } 575 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); 576 boolean deleted = false; 577 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 578 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 579 // If this is a forwarding download, and the attachment doesn't exist (or 580 // can't be downloaded) delete it from the outgoing message, lest that 581 // message never get sent 582 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 583 // TODO: Talk to UX about whether this is even worth doing 584 NotificationController nc = NotificationController.getInstance(mContext); 585 nc.showDownloadForwardFailedNotification(attachment); 586 deleted = true; 587 } 588 // If we're an attachment on forwarded mail, and if we're not still blocked, 589 // try to send pending mail now (as mediated by MailService) 590 if ((req != null) && 591 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 592 if (Email.DEBUG) { 593 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 594 } 595 MailService.actionSendPendingMail(mContext, req.accountId); 596 } 597 } 598 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) { 599 Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 600 if (msg == null) { 601 // If there's no associated message, delete the attachment 602 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 603 } else { 604 // If there really is a message, retry 605 kick(); 606 return; 607 } 608 } else if (!deleted) { 609 // Clear the download flags, since we're done for now. Note that this happens 610 // only for non-recoverable errors. When these occur for forwarded mail, we can 611 // ignore it and continue; otherwise, it was either 1) a user request, in which 612 // case the user can retry manually or 2) an opportunistic download, in which 613 // case the download wasn't critical 614 ContentValues cv = new ContentValues(); 615 int flags = 616 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 617 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 618 attachment.update(mContext, cv); 619 } 620 } 621 // Process the queue 622 kick(); 623 } 624 } 625 626 /** 627 * Calculate the download priority of an Attachment. A priority of zero means that the 628 * attachment is not marked for download. 629 * @param att the Attachment 630 * @return the priority key of the Attachment 631 */ 632 private static int getPriority(Attachment att) { 633 int priorityClass = PRIORITY_NONE; 634 int flags = att.mFlags; 635 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 636 priorityClass = PRIORITY_SEND_MAIL; 637 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 638 priorityClass = PRIORITY_FOREGROUND; 639 } 640 return priorityClass; 641 } 642 643 private void kick() { 644 synchronized(mLock) { 645 mLock.notify(); 646 } 647 } 648 649 /** 650 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 651 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 652 * single callback that's defined by the EmailServiceCallback interface. 653 */ 654 private class ServiceCallback extends IEmailServiceCallback.Stub { 655 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 656 int progress) { 657 // Record status and progress 658 DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId); 659 if (req != null) { 660 if (Email.DEBUG) { 661 String code; 662 switch(statusCode) { 663 case EmailServiceStatus.SUCCESS: code = "Success"; break; 664 case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break; 665 default: code = Integer.toString(statusCode); break; 666 } 667 if (statusCode != EmailServiceStatus.IN_PROGRESS) { 668 Log.d(TAG, ">> Attachment " + attachmentId + ": " + code); 669 } else if (progress >= (req.lastProgress + 15)) { 670 Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%"); 671 } 672 } 673 req.lastStatusCode = statusCode; 674 req.lastProgress = progress; 675 req.lastCallbackTime = System.currentTimeMillis(); 676 } 677 switch (statusCode) { 678 case EmailServiceStatus.IN_PROGRESS: 679 break; 680 default: 681 mDownloadSet.endDownload(attachmentId, statusCode); 682 break; 683 } 684 } 685 686 @Override 687 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 688 throws RemoteException { 689 } 690 691 @Override 692 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 693 throws RemoteException { 694 } 695 696 @Override 697 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 698 throws RemoteException { 699 } 700 701 @Override 702 public void loadMessageStatus(long messageId, int statusCode, int progress) 703 throws RemoteException { 704 } 705 } 706 707 /** 708 * Return an Intent to be used used based on the account type of the provided account id. We 709 * cache the results to avoid repeated database access 710 * @param accountId the id of the account 711 * @return the Intent to be used for the account or null (if the account no longer exists) 712 */ 713 private synchronized Intent getServiceIntentForAccount(long accountId) { 714 // TODO: We should have some more data-driven way of determining the service intent. 715 Intent serviceIntent = mAccountServiceMap.get(accountId); 716 if (serviceIntent == null) { 717 String protocol = Account.getProtocol(mContext, accountId); 718 if (protocol == null) return null; 719 serviceIntent = new Intent(mContext, ControllerService.class); 720 if (protocol.equals("eas")) { 721 serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT); 722 } 723 mAccountServiceMap.put(accountId, serviceIntent); 724 } 725 return serviceIntent; 726 } 727 728 /*package*/ void addServiceIntentForTest(long accountId, Intent intent) { 729 mAccountServiceMap.put(accountId, intent); 730 } 731 732 /*package*/ void onChange(Attachment att) { 733 mDownloadSet.onChange(this, att); 734 } 735 736 /*package*/ boolean isQueued(long attachmentId) { 737 return mDownloadSet.findDownloadRequest(attachmentId) != null; 738 } 739 740 /*package*/ int getSize() { 741 return mDownloadSet.size(); 742 } 743 744 /*package*/ boolean dequeue(long attachmentId) { 745 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 746 if (req != null) { 747 if (Email.DEBUG) { 748 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 749 } 750 mDownloadSet.remove(req); 751 return true; 752 } 753 return false; 754 } 755 756 /** 757 * Ask the service for the number of items in the download queue 758 * @return the number of items queued for download 759 */ 760 public static int getQueueSize() { 761 AttachmentDownloadService service = sRunningService; 762 if (service != null) { 763 return service.getSize(); 764 } 765 return 0; 766 } 767 768 /** 769 * Ask the service whether a particular attachment is queued for download 770 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 771 * @return whether or not the attachment is queued for download 772 */ 773 public static boolean isAttachmentQueued(long attachmentId) { 774 AttachmentDownloadService service = sRunningService; 775 if (service != null) { 776 return service.isQueued(attachmentId); 777 } 778 return false; 779 } 780 781 /** 782 * Ask the service to remove an attachment from the download queue 783 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 784 * @return whether or not the attachment was removed from the queue 785 */ 786 public static boolean cancelQueuedAttachment(long attachmentId) { 787 AttachmentDownloadService service = sRunningService; 788 if (service != null) { 789 return service.dequeue(attachmentId); 790 } 791 return false; 792 } 793 794 public static void watchdogAlarm() { 795 AttachmentDownloadService service = sRunningService; 796 if (service != null) { 797 service.mDownloadSet.onWatchdogAlarm(); 798 } 799 } 800 801 /** 802 * Called directly by EmailProvider whenever an attachment is inserted or changed 803 * @param context the caller's context 804 * @param id the attachment's id 805 * @param flags the new flags for the attachment 806 */ 807 public static void attachmentChanged(final Context context, final long id, final int flags) { 808 Utility.runAsync(new Runnable() { 809 public void run() { 810 Attachment attachment = Attachment.restoreAttachmentWithId(context, id); 811 if (attachment != null) { 812 // Store the flags we got from EmailProvider; given that all of this 813 // activity is asynchronous, we need to use the newest data from 814 // EmailProvider 815 attachment.mFlags = flags; 816 Intent intent = new Intent(context, AttachmentDownloadService.class); 817 intent.putExtra(EXTRA_ATTACHMENT, attachment); 818 context.startService(intent); 819 } 820 }}); 821 } 822 823 /** 824 * Determine whether an attachment can be prefetched for the given account 825 * @return true if download is allowed, false otherwise 826 */ 827 public boolean canPrefetchForAccount(Account account, File dir) { 828 // Check account, just in case 829 if (account == null) return false; 830 // First, check preference and quickly return if prefetch isn't allowed 831 if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false; 832 833 long totalStorage = dir.getTotalSpace(); 834 long usableStorage = dir.getUsableSpace(); 835 long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); 836 837 // If there's not enough overall storage available, stop now 838 if (usableStorage < minAvailable) { 839 return false; 840 } 841 842 int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); 843 long perAccountMaxStorage = 844 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); 845 846 // Retrieve our idea of currently used attachment storage; since we don't track deletions, 847 // this number is the "worst case". If the number is greater than what's allowed per 848 // account, we walk the directory to determine the actual number 849 Long accountStorage = mAttachmentStorageMap.get(account.mId); 850 if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { 851 // Calculate the exact figure for attachment storage for this account 852 accountStorage = 0L; 853 File[] files = dir.listFiles(); 854 if (files != null) { 855 for (File file : files) { 856 accountStorage += file.length(); 857 } 858 } 859 // Cache the value 860 mAttachmentStorageMap.put(account.mId, accountStorage); 861 } 862 863 // Return true if we're using less than the maximum per account 864 if (accountStorage < perAccountMaxStorage) { 865 return true; 866 } else { 867 if (Email.DEBUG) { 868 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " + 869 accountStorage + ", limit " + perAccountMaxStorage); 870 } 871 return false; 872 } 873 } 874 875 public void run() { 876 // These fields are only used within the service thread 877 mContext = this; 878 mConnectivityManager = new EmailConnectivityManager(this, TAG); 879 mAccountManagerStub = new AccountManagerStub(this); 880 881 // Run through all attachments in the database that require download and add them to 882 // the queue 883 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 884 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 885 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 886 new String[] {Integer.toString(mask)}, null); 887 try { 888 Log.d(TAG, "Count: " + c.getCount()); 889 while (c.moveToNext()) { 890 Attachment attachment = Attachment.restoreAttachmentWithId( 891 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 892 if (attachment != null) { 893 mDownloadSet.onChange(this, attachment); 894 } 895 } 896 } catch (Exception e) { 897 e.printStackTrace(); 898 } 899 finally { 900 c.close(); 901 } 902 903 // Loop until stopped, with a 30 minute wait loop 904 while (!mStop) { 905 // Here's where we run our attachment loading logic... 906 mConnectivityManager.waitForConnectivity(); 907 mDownloadSet.processQueue(); 908 if (mDownloadSet.isEmpty()) { 909 Log.d(TAG, "*** All done; shutting down service"); 910 stopSelf(); 911 break; 912 } 913 synchronized(mLock) { 914 try { 915 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 916 } catch (InterruptedException e) { 917 // That's ok; we'll just keep looping 918 } 919 } 920 } 921 922 // Unregister now that we're done 923 if (mConnectivityManager != null) { 924 mConnectivityManager.unregister(); 925 } 926 } 927 928 @Override 929 public int onStartCommand(Intent intent, int flags, int startId) { 930 if (sRunningService == null) { 931 sRunningService = this; 932 } 933 if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) { 934 Attachment att = (Attachment)intent.getParcelableExtra(EXTRA_ATTACHMENT); 935 onChange(att); 936 } 937 return Service.START_STICKY; 938 } 939 940 @Override 941 public void onCreate() { 942 // Start up our service thread 943 new Thread(this, "AttachmentDownloadService").start(); 944 } 945 @Override 946 public IBinder onBind(Intent intent) { 947 return null; 948 } 949 950 @Override 951 public void onDestroy() { 952 // Mark this instance of the service as stopped 953 mStop = true; 954 if (sRunningService != null) { 955 kick(); 956 sRunningService = null; 957 } 958 if (mConnectivityManager != null) { 959 mConnectivityManager.unregister(); 960 mConnectivityManager = null; 961 } 962 } 963 964 @Override 965 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 966 pw.println("AttachmentDownloadService"); 967 long time = System.currentTimeMillis(); 968 synchronized(mDownloadSet) { 969 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 970 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 971 // First, start up any required downloads, in priority order 972 while (iterator.hasNext()) { 973 DownloadRequest req = iterator.next(); 974 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 975 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 976 (req.inProgress ? " [In progress]" : "")); 977 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId); 978 if (att == null) { 979 pw.println(" Attachment not in database?"); 980 } else if (att.mFileName != null) { 981 String fileName = att.mFileName; 982 String suffix = "[none]"; 983 int lastDot = fileName.lastIndexOf('.'); 984 if (lastDot >= 0) { 985 suffix = fileName.substring(lastDot); 986 } 987 pw.print(" Suffix: " + suffix); 988 if (att.mContentUri != null) { 989 pw.print(" ContentUri: " + att.mContentUri); 990 } 991 pw.print(" Mime: "); 992 if (att.mMimeType != null) { 993 pw.print(att.mMimeType); 994 } else { 995 pw.print(AttachmentUtilities.inferMimeType(fileName, null)); 996 pw.print(" [inferred]"); 997 } 998 pw.println(" Size: " + att.mSize); 999 } 1000 if (req.inProgress) { 1001 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 1002 req.lastProgress); 1003 pw.println(" Started: " + req.startTime + ", Callback: " + 1004 req.lastCallbackTime); 1005 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 1006 if (req.lastCallbackTime > 0) { 1007 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 1008 } 1009 } 1010 } 1011 } 1012 } 1013} 1014