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