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