AttachmentDownloadService.java revision 420e97f0f82afe0db64c3651eaf3cb15e149ba01
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.Email; 37import com.android.email.EmailConnectivityManager; 38import com.android.email.NotificationController; 39import com.android.emailcommon.provider.Account; 40import com.android.emailcommon.provider.EmailContent; 41import com.android.emailcommon.provider.EmailContent.Attachment; 42import com.android.emailcommon.provider.EmailContent.Message; 43import com.android.emailcommon.service.EmailServiceProxy; 44import com.android.emailcommon.service.EmailServiceStatus; 45import com.android.emailcommon.service.IEmailServiceCallback; 46import com.android.emailcommon.utility.AttachmentUtilities; 47import com.android.emailcommon.utility.Utility; 48import com.android.mail.providers.UIProvider.AttachmentColumns; 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 @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 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 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 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 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 private AlarmManager mAlarmManager; 239 240 /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) { 241 super(comparator); 242 } 243 244 /** 245 * Maps attachment id to DownloadRequest 246 */ 247 /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress = 248 new ConcurrentHashMap<Long, DownloadRequest>(); 249 250 /** 251 * onChange is called by the AttachmentReceiver upon receipt of a valid notification from 252 * EmailProvider that an attachment has been inserted or modified. It's not strictly 253 * necessary that we detect a deleted attachment, as the code always checks for the 254 * existence of an attachment before acting on it. 255 */ 256 public synchronized void onChange(Context context, Attachment att) { 257 DownloadRequest req = findDownloadRequest(att.mId); 258 long priority = getPriority(att); 259 if (priority == PRIORITY_NONE) { 260 if (Email.DEBUG) { 261 Log.d(TAG, "== Attachment changed: " + att.mId); 262 } 263 // In this case, there is no download priority for this attachment 264 if (req != null) { 265 // If it exists in the map, remove it 266 // NOTE: We don't yet support deleting downloads in progress 267 if (Email.DEBUG) { 268 Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing"); 269 } 270 remove(req); 271 } 272 } else { 273 // Ignore changes that occur during download 274 if (mDownloadsInProgress.containsKey(att.mId)) return; 275 // If this is new, add the request to the queue 276 if (req == null) { 277 req = new DownloadRequest(context, att); 278 add(req); 279 } 280 // If the request already existed, we'll update the priority (so that the time is 281 // up-to-date); otherwise, we create a new request 282 if (Email.DEBUG) { 283 Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " + 284 req.priority + ", priority time " + req.time); 285 } 286 } 287 // Process the queue if we're in a wait 288 kick(); 289 } 290 291 /** 292 * Find a queued DownloadRequest, given the attachment's id 293 * @param id the id of the attachment 294 * @return the DownloadRequest for that attachment (or null, if none) 295 */ 296 /*package*/ synchronized DownloadRequest findDownloadRequest(long id) { 297 Iterator<DownloadRequest> iterator = iterator(); 298 while(iterator.hasNext()) { 299 DownloadRequest req = iterator.next(); 300 if (req.attachmentId == id) { 301 return req; 302 } 303 } 304 return null; 305 } 306 307 @Override 308 public synchronized boolean isEmpty() { 309 return super.isEmpty() && mDownloadsInProgress.isEmpty(); 310 } 311 312 /** 313 * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing 314 * the limit on maximum downloads 315 */ 316 /*package*/ synchronized void processQueue() { 317 if (Email.DEBUG) { 318 Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries"); 319 } 320 321 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 322 // First, start up any required downloads, in priority order 323 while (iterator.hasNext() && 324 (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) { 325 DownloadRequest req = iterator.next(); 326 // Enforce per-account limit here 327 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { 328 if (Email.DEBUG) { 329 Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + 330 req.accountId); 331 } 332 continue; 333 } 334 335 if (!req.inProgress) { 336 mDownloadSet.tryStartDownload(req); 337 } 338 } 339 340 // Don't prefetch if background downloading is disallowed 341 EmailConnectivityManager ecm = mConnectivityManager; 342 if (ecm == null) return; 343 if (!ecm.isAutoSyncAllowed()) return; 344 // Don't prefetch unless we're on a WiFi network 345 if (ecm.getActiveNetworkType() != ConnectivityManager.TYPE_WIFI) { 346 return; 347 } 348 // Then, try opportunistic download of appropriate attachments 349 int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size(); 350 // Always leave one slot for user requested download 351 if (backgroundDownloads > (MAX_SIMULTANEOUS_DOWNLOADS - 1)) { 352 // We'll load up the newest 25 attachments that aren't loaded or queued 353 Uri lookupUri = EmailContent.uriWithLimit(Attachment.CONTENT_URI, 354 MAX_ATTACHMENTS_TO_CHECK); 355 Cursor c = mContext.getContentResolver().query(lookupUri, AttachmentInfo.PROJECTION, 356 EmailContent.Attachment.PRECACHE_INBOX_SELECTION, 357 null, Attachment.RECORD_ID + " DESC"); 358 File cacheDir = mContext.getCacheDir(); 359 try { 360 while (c.moveToNext()) { 361 long accountKey = c.getLong(AttachmentInfo.COLUMN_ACCOUNT_KEY); 362 long id = c.getLong(AttachmentInfo.COLUMN_ID); 363 Account account = Account.restoreAccountWithId(mContext, accountKey); 364 if (account == null) { 365 // Clean up this orphaned attachment; there's no point in keeping it 366 // around; then try to find another one 367 EmailContent.delete(mContext, Attachment.CONTENT_URI, id); 368 } else if (canPrefetchForAccount(account, cacheDir)) { 369 // Check that the attachment meets system requirements for download 370 AttachmentInfo info = new AttachmentInfo(mContext, c); 371 if (info.isEligibleForDownload()) { 372 Attachment att = Attachment.restoreAttachmentWithId(mContext, id); 373 if (att != null) { 374 Integer tryCount; 375 tryCount = mAttachmentFailureMap.get(att.mId); 376 if (tryCount != null && tryCount > MAX_DOWNLOAD_RETRIES) { 377 // move onto the next attachment 378 continue; 379 } 380 // Start this download and we're done 381 DownloadRequest req = new DownloadRequest(mContext, att); 382 mDownloadSet.tryStartDownload(req); 383 break; 384 } 385 } 386 } 387 } 388 } finally { 389 c.close(); 390 } 391 } 392 } 393 394 /** 395 * Count the number of running downloads in progress for this account 396 * @param accountId the id of the account 397 * @return the count of running downloads 398 */ 399 /*package*/ synchronized int downloadsForAccount(long accountId) { 400 int count = 0; 401 for (DownloadRequest req: mDownloadsInProgress.values()) { 402 if (req.accountId == accountId) { 403 count++; 404 } 405 } 406 return count; 407 } 408 409 private void cancelWatchdogAlarm() { 410 if (mAlarmManager != null && mWatchdogPendingIntent != null) { 411 mAlarmManager.cancel(mWatchdogPendingIntent); 412 } 413 } 414 415 /** 416 * Watchdog for downloads; we use this in case we are hanging on a download, which might 417 * have failed silently (the connection dropped, for example) 418 */ 419 private void onWatchdogAlarm() { 420 // If our service instance is gone, just leave (but cancel alarm first!) 421 if (mStop) { 422 cancelWatchdogAlarm(); 423 return; 424 } 425 long now = System.currentTimeMillis(); 426 for (DownloadRequest req: mDownloadsInProgress.values()) { 427 // Check how long it's been since receiving a callback 428 long timeSinceCallback = now - req.lastCallbackTime; 429 if (timeSinceCallback > CALLBACK_TIMEOUT) { 430 if (Email.DEBUG) { 431 Log.d(TAG, "== Download of " + req.attachmentId + " timed out"); 432 } 433 cancelDownload(req); 434 } 435 } 436 // If there are downloads in progress, reset alarm 437 if (mDownloadsInProgress.isEmpty()) { 438 cancelWatchdogAlarm(); 439 } 440 // Check whether we can start new downloads... 441 if (mConnectivityManager != null && mConnectivityManager.hasConnectivity()) { 442 processQueue(); 443 } 444 } 445 446 /** 447 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 448 * parameter 449 * @param req the DownloadRequest 450 * @return whether or not the download was started 451 */ 452 /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { 453 EmailServiceProxy service = EmailServiceUtils.getServiceForAccount( 454 AttachmentDownloadService.this, mServiceCallback, req.accountId); 455 456 // Do not download the same attachment multiple times 457 boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null; 458 if (alreadyInProgress) return false; 459 460 try { 461 if (Email.DEBUG) { 462 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 463 } 464 startDownload(service, req); 465 } catch (RemoteException e) { 466 // TODO: Consider whether we need to do more in this case... 467 // For now, fix up our data to reflect the failure 468 cancelDownload(req); 469 } 470 return true; 471 } 472 473 private synchronized DownloadRequest getDownloadInProgress(long attachmentId) { 474 return mDownloadsInProgress.get(attachmentId); 475 } 476 477 /** 478 * Do the work of starting an attachment download using the EmailService interface, and 479 * set our watchdog alarm 480 * 481 * @param serviceClass the service handling the download 482 * @param req the DownloadRequest 483 * @throws RemoteException 484 */ 485 private void startDownload(EmailServiceProxy service, DownloadRequest req) 486 throws RemoteException { 487 req.startTime = System.currentTimeMillis(); 488 req.inProgress = true; 489 mDownloadsInProgress.put(req.attachmentId, req); 490 service.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND); 491 // Lazily initialize our (reusable) pending intent 492 if (mWatchdogPendingIntent == null) { 493 createWatchdogPendingIntent(mContext); 494 } 495 // Set the alarm 496 mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, 497 System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL, 498 mWatchdogPendingIntent); 499 } 500 501 /*package*/ void createWatchdogPendingIntent(Context context) { 502 Intent alarmIntent = new Intent(context, Watchdog.class); 503 mWatchdogPendingIntent = PendingIntent.getBroadcast(context, 0, alarmIntent, 0); 504 mAlarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); 505 } 506 507 private void cancelDownload(DownloadRequest req) { 508 mDownloadsInProgress.remove(req.attachmentId); 509 req.inProgress = false; 510 } 511 512 /** 513 * Called when a download is finished; we get notified of this via our EmailServiceCallback 514 * @param attachmentId the id of the attachment whose download is finished 515 * @param statusCode the EmailServiceStatus code returned by the Service 516 */ 517 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 518 // Say we're no longer downloading this 519 mDownloadsInProgress.remove(attachmentId); 520 521 // TODO: This code is conservative and treats connection issues as failures. 522 // Since we have no mechanism to throttle reconnection attempts, it makes 523 // sense to be cautious here. Once logic is in place to prevent connecting 524 // in a tight loop, we can exclude counting connection issues as "failures". 525 526 // Update the attachment failure list if needed 527 Integer downloadCount; 528 downloadCount = mAttachmentFailureMap.remove(attachmentId); 529 if (statusCode != EmailServiceStatus.SUCCESS) { 530 if (downloadCount == null) { 531 downloadCount = 0; 532 } 533 downloadCount += 1; 534 mAttachmentFailureMap.put(attachmentId, downloadCount); 535 } 536 537 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 538 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 539 // If this needs to be retried, just process the queue again 540 if (Email.DEBUG) { 541 Log.d(TAG, "== The download for attachment #" + attachmentId + 542 " will be retried"); 543 } 544 if (req != null) { 545 req.inProgress = false; 546 } 547 kick(); 548 return; 549 } 550 551 // If the request is still in the queue, remove it 552 if (req != null) { 553 remove(req); 554 } 555 if (Email.DEBUG) { 556 long secs = 0; 557 if (req != null) { 558 secs = (System.currentTimeMillis() - req.time) / 1000; 559 } 560 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 561 "Error " + statusCode; 562 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 563 " seconds from request, status: " + status); 564 } 565 566 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 567 if (attachment != null) { 568 long accountId = attachment.mAccountKey; 569 // Update our attachment storage for this account 570 Long currentStorage = mAttachmentStorageMap.get(accountId); 571 if (currentStorage == null) { 572 currentStorage = 0L; 573 } 574 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); 575 boolean deleted = false; 576 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 577 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 578 // If this is a forwarding download, and the attachment doesn't exist (or 579 // can't be downloaded) delete it from the outgoing message, lest that 580 // message never get sent 581 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 582 // TODO: Talk to UX about whether this is even worth doing 583 NotificationController nc = NotificationController.getInstance(mContext); 584 nc.showDownloadForwardFailedNotification(attachment); 585 deleted = true; 586 } 587 // If we're an attachment on forwarded mail, and if we're not still blocked, 588 // try to send pending mail now (as mediated by MailService) 589 if ((req != null) && 590 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 591 if (Email.DEBUG) { 592 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 593 } 594 MailService.actionSendPendingMail(mContext, req.accountId); 595 } 596 } 597 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) { 598 Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 599 if (msg == null) { 600 // If there's no associated message, delete the attachment 601 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 602 } else { 603 // If there really is a message, retry 604 kick(); 605 return; 606 } 607 } else if (!deleted) { 608 // Clear the download flags, since we're done for now. Note that this happens 609 // only for non-recoverable errors. When these occur for forwarded mail, we can 610 // ignore it and continue; otherwise, it was either 1) a user request, in which 611 // case the user can retry manually or 2) an opportunistic download, in which 612 // case the download wasn't critical 613 ContentValues cv = new ContentValues(); 614 int flags = 615 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 616 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 617 attachment.update(mContext, cv); 618 } 619 } 620 // Process the queue 621 kick(); 622 } 623 } 624 625 /** 626 * Calculate the download priority of an Attachment. A priority of zero means that the 627 * attachment is not marked for download. 628 * @param att the Attachment 629 * @return the priority key of the Attachment 630 */ 631 private static int getPriority(Attachment att) { 632 int priorityClass = PRIORITY_NONE; 633 int flags = att.mFlags; 634 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 635 priorityClass = PRIORITY_SEND_MAIL; 636 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 637 priorityClass = PRIORITY_FOREGROUND; 638 } 639 return priorityClass; 640 } 641 642 private void kick() { 643 synchronized(mLock) { 644 mLock.notify(); 645 } 646 } 647 648 /** 649 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 650 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 651 * single callback that's defined by the EmailServiceCallback interface. 652 */ 653 private class ServiceCallback extends IEmailServiceCallback.Stub { 654 @Override 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 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 677 if (attachment != null && statusCode == EmailServiceStatus.IN_PROGRESS) { 678 ContentValues values = new ContentValues(); 679 values.put(AttachmentColumns.DOWNLOADED_SIZE, 680 attachment.mSize * 100 / progress); 681 // Update UIProvider with updated download size 682 // Individual services will set contentUri and state when finished 683 attachment.update(mContext, values); 684 } 685 } 686 switch (statusCode) { 687 case EmailServiceStatus.IN_PROGRESS: 688 break; 689 default: 690 mDownloadSet.endDownload(attachmentId, statusCode); 691 break; 692 } 693 } 694 695 @Override 696 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 697 throws RemoteException { 698 } 699 700 @Override 701 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 702 throws RemoteException { 703 } 704 705 @Override 706 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 707 throws RemoteException { 708 } 709 710 @Override 711 public void loadMessageStatus(long messageId, int statusCode, int progress) 712 throws RemoteException { 713 } 714 } 715 716 /*package*/ void addServiceIntentForTest(long accountId, Intent intent) { 717 mAccountServiceMap.put(accountId, intent); 718 } 719 720 /*package*/ void onChange(Attachment att) { 721 mDownloadSet.onChange(this, att); 722 } 723 724 /*package*/ boolean isQueued(long attachmentId) { 725 return mDownloadSet.findDownloadRequest(attachmentId) != null; 726 } 727 728 /*package*/ int getSize() { 729 return mDownloadSet.size(); 730 } 731 732 /*package*/ boolean dequeue(long attachmentId) { 733 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 734 if (req != null) { 735 if (Email.DEBUG) { 736 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 737 } 738 mDownloadSet.remove(req); 739 return true; 740 } 741 return false; 742 } 743 744 /** 745 * Ask the service for the number of items in the download queue 746 * @return the number of items queued for download 747 */ 748 public static int getQueueSize() { 749 AttachmentDownloadService service = sRunningService; 750 if (service != null) { 751 return service.getSize(); 752 } 753 return 0; 754 } 755 756 /** 757 * Ask the service whether a particular attachment is queued for download 758 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 759 * @return whether or not the attachment is queued for download 760 */ 761 public static boolean isAttachmentQueued(long attachmentId) { 762 AttachmentDownloadService service = sRunningService; 763 if (service != null) { 764 return service.isQueued(attachmentId); 765 } 766 return false; 767 } 768 769 /** 770 * Ask the service to remove an attachment from the download queue 771 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 772 * @return whether or not the attachment was removed from the queue 773 */ 774 public static boolean cancelQueuedAttachment(long attachmentId) { 775 AttachmentDownloadService service = sRunningService; 776 if (service != null) { 777 return service.dequeue(attachmentId); 778 } 779 return false; 780 } 781 782 public static void watchdogAlarm() { 783 AttachmentDownloadService service = sRunningService; 784 if (service != null) { 785 service.mDownloadSet.onWatchdogAlarm(); 786 } 787 } 788 789 /** 790 * Called directly by EmailProvider whenever an attachment is inserted or changed 791 * @param context the caller's context 792 * @param id the attachment's id 793 * @param flags the new flags for the attachment 794 */ 795 public static void attachmentChanged(final Context context, final long id, final int flags) { 796 Utility.runAsync(new Runnable() { 797 @Override 798 public void run() { 799 Attachment attachment = Attachment.restoreAttachmentWithId(context, id); 800 if (attachment != null) { 801 // Store the flags we got from EmailProvider; given that all of this 802 // activity is asynchronous, we need to use the newest data from 803 // EmailProvider 804 attachment.mFlags = flags; 805 Intent intent = new Intent(context, AttachmentDownloadService.class); 806 intent.putExtra(EXTRA_ATTACHMENT, attachment); 807 context.startService(intent); 808 } 809 }}); 810 } 811 812 /** 813 * Determine whether an attachment can be prefetched for the given account 814 * @return true if download is allowed, false otherwise 815 */ 816 public boolean canPrefetchForAccount(Account account, File dir) { 817 // Check account, just in case 818 if (account == null) return false; 819 // First, check preference and quickly return if prefetch isn't allowed 820 if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false; 821 822 long totalStorage = dir.getTotalSpace(); 823 long usableStorage = dir.getUsableSpace(); 824 long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); 825 826 // If there's not enough overall storage available, stop now 827 if (usableStorage < minAvailable) { 828 return false; 829 } 830 831 int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); 832 long perAccountMaxStorage = 833 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); 834 835 // Retrieve our idea of currently used attachment storage; since we don't track deletions, 836 // this number is the "worst case". If the number is greater than what's allowed per 837 // account, we walk the directory to determine the actual number 838 Long accountStorage = mAttachmentStorageMap.get(account.mId); 839 if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { 840 // Calculate the exact figure for attachment storage for this account 841 accountStorage = 0L; 842 File[] files = dir.listFiles(); 843 if (files != null) { 844 for (File file : files) { 845 accountStorage += file.length(); 846 } 847 } 848 // Cache the value 849 mAttachmentStorageMap.put(account.mId, accountStorage); 850 } 851 852 // Return true if we're using less than the maximum per account 853 if (accountStorage < perAccountMaxStorage) { 854 return true; 855 } else { 856 if (Email.DEBUG) { 857 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " + 858 accountStorage + ", limit " + perAccountMaxStorage); 859 } 860 return false; 861 } 862 } 863 864 @Override 865 public void run() { 866 // These fields are only used within the service thread 867 mContext = this; 868 mConnectivityManager = new EmailConnectivityManager(this, TAG); 869 mAccountManagerStub = new AccountManagerStub(this); 870 871 // Run through all attachments in the database that require download and add them to 872 // the queue 873 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 874 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 875 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 876 new String[] {Integer.toString(mask)}, null); 877 try { 878 Log.d(TAG, "Count: " + c.getCount()); 879 while (c.moveToNext()) { 880 Attachment attachment = Attachment.restoreAttachmentWithId( 881 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 882 if (attachment != null) { 883 mDownloadSet.onChange(this, attachment); 884 } 885 } 886 } catch (Exception e) { 887 e.printStackTrace(); 888 } 889 finally { 890 c.close(); 891 } 892 893 // Loop until stopped, with a 30 minute wait loop 894 while (!mStop) { 895 // Here's where we run our attachment loading logic... 896 mConnectivityManager.waitForConnectivity(); 897 mDownloadSet.processQueue(); 898 if (mDownloadSet.isEmpty()) { 899 Log.d(TAG, "*** All done; shutting down service"); 900 stopSelf(); 901 break; 902 } 903 synchronized(mLock) { 904 try { 905 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 906 } catch (InterruptedException e) { 907 // That's ok; we'll just keep looping 908 } 909 } 910 } 911 912 // Unregister now that we're done 913 if (mConnectivityManager != null) { 914 mConnectivityManager.unregister(); 915 } 916 } 917 918 @Override 919 public int onStartCommand(Intent intent, int flags, int startId) { 920 if (sRunningService == null) { 921 sRunningService = this; 922 } 923 if (intent != null && intent.hasExtra(EXTRA_ATTACHMENT)) { 924 Attachment att = (Attachment)intent.getParcelableExtra(EXTRA_ATTACHMENT); 925 onChange(att); 926 } 927 return Service.START_STICKY; 928 } 929 930 @Override 931 public void onCreate() { 932 // Start up our service thread 933 new Thread(this, "AttachmentDownloadService").start(); 934 } 935 @Override 936 public IBinder onBind(Intent intent) { 937 return null; 938 } 939 940 @Override 941 public void onDestroy() { 942 // Mark this instance of the service as stopped 943 mStop = true; 944 if (sRunningService != null) { 945 kick(); 946 sRunningService = null; 947 } 948 if (mConnectivityManager != null) { 949 mConnectivityManager.unregister(); 950 mConnectivityManager = null; 951 } 952 } 953 954 @Override 955 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 956 pw.println("AttachmentDownloadService"); 957 long time = System.currentTimeMillis(); 958 synchronized(mDownloadSet) { 959 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 960 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 961 // First, start up any required downloads, in priority order 962 while (iterator.hasNext()) { 963 DownloadRequest req = iterator.next(); 964 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 965 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 966 (req.inProgress ? " [In progress]" : "")); 967 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId); 968 if (att == null) { 969 pw.println(" Attachment not in database?"); 970 } else if (att.mFileName != null) { 971 String fileName = att.mFileName; 972 String suffix = "[none]"; 973 int lastDot = fileName.lastIndexOf('.'); 974 if (lastDot >= 0) { 975 suffix = fileName.substring(lastDot); 976 } 977 pw.print(" Suffix: " + suffix); 978 if (att.mContentUri != null) { 979 pw.print(" ContentUri: " + att.mContentUri); 980 } 981 pw.print(" Mime: "); 982 if (att.mMimeType != null) { 983 pw.print(att.mMimeType); 984 } else { 985 pw.print(AttachmentUtilities.inferMimeType(fileName, null)); 986 pw.print(" [inferred]"); 987 } 988 pw.println(" Size: " + att.mSize); 989 } 990 if (req.inProgress) { 991 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 992 req.lastProgress); 993 pw.println(" Started: " + req.startTime + ", Callback: " + 994 req.lastCallbackTime); 995 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 996 if (req.lastCallbackTime > 0) { 997 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 998 } 999 } 1000 } 1001 } 1002 } 1003} 1004