AttachmentDownloadService.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
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.Account; 25import com.android.emailcommon.provider.EmailContent; 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.PRECACHE_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 485 private void cancelDownload(DownloadRequest req) { 486 mDownloadsInProgress.remove(req.attachmentId); 487 req.inProgress = false; 488 } 489 490 /** 491 * Called when a download is finished; we get notified of this via our EmailServiceCallback 492 * @param attachmentId the id of the attachment whose download is finished 493 * @param statusCode the EmailServiceStatus code returned by the Service 494 */ 495 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 496 // Say we're no longer downloading this 497 mDownloadsInProgress.remove(attachmentId); 498 499 // TODO: This code is conservative and treats connection issues as failures. 500 // Since we have no mechanism to throttle reconnection attempts, it makes 501 // sense to be cautious here. Once logic is in place to prevent connecting 502 // in a tight loop, we can exclude counting connection issues as "failures". 503 504 // Update the attachment failure list if needed 505 Integer downloadCount; 506 downloadCount = mAttachmentFailureMap.remove(attachmentId); 507 if (statusCode != EmailServiceStatus.SUCCESS) { 508 if (downloadCount == null) { 509 downloadCount = 0; 510 } 511 downloadCount += 1; 512 mAttachmentFailureMap.put(attachmentId, downloadCount); 513 } 514 515 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 516 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 517 // If this needs to be retried, just process the queue again 518 if (Email.DEBUG) { 519 Log.d(TAG, "== The download for attachment #" + attachmentId + 520 " will be retried"); 521 } 522 if (req != null) { 523 req.inProgress = false; 524 } 525 kick(); 526 return; 527 } 528 529 // If the request is still in the queue, remove it 530 if (req != null) { 531 remove(req); 532 } 533 if (Email.DEBUG) { 534 long secs = 0; 535 if (req != null) { 536 secs = (System.currentTimeMillis() - req.time) / 1000; 537 } 538 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 539 "Error " + statusCode; 540 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 541 " seconds from request, status: " + status); 542 } 543 544 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 545 if (attachment != null) { 546 long accountId = attachment.mAccountKey; 547 // Update our attachment storage for this account 548 Long currentStorage = mAttachmentStorageMap.get(accountId); 549 if (currentStorage == null) { 550 currentStorage = 0L; 551 } 552 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); 553 boolean deleted = false; 554 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 555 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 556 // If this is a forwarding download, and the attachment doesn't exist (or 557 // can't be downloaded) delete it from the outgoing message, lest that 558 // message never get sent 559 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 560 // TODO: Talk to UX about whether this is even worth doing 561 NotificationController nc = NotificationController.getInstance(mContext); 562 nc.showDownloadForwardFailedNotification(attachment); 563 deleted = true; 564 } 565 // If we're an attachment on forwarded mail, and if we're not still blocked, 566 // try to send pending mail now (as mediated by MailService) 567 if ((req != null) && 568 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 569 if (Email.DEBUG) { 570 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 571 } 572 MailService.actionSendPendingMail(mContext, req.accountId); 573 } 574 } 575 if (statusCode == EmailServiceStatus.MESSAGE_NOT_FOUND) { 576 Message msg = Message.restoreMessageWithId(mContext, attachment.mMessageKey); 577 if (msg == null) { 578 // If there's no associated message, delete the attachment 579 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 580 } else { 581 // If there really is a message, retry 582 kick(); 583 return; 584 } 585 } else if (!deleted) { 586 // Clear the download flags, since we're done for now. Note that this happens 587 // only for non-recoverable errors. When these occur for forwarded mail, we can 588 // ignore it and continue; otherwise, it was either 1) a user request, in which 589 // case the user can retry manually or 2) an opportunistic download, in which 590 // case the download wasn't critical 591 ContentValues cv = new ContentValues(); 592 int flags = 593 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 594 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 595 attachment.update(mContext, cv); 596 } 597 } 598 // Process the queue 599 kick(); 600 } 601 } 602 603 /** 604 * Calculate the download priority of an Attachment. A priority of zero means that the 605 * attachment is not marked for download. 606 * @param att the Attachment 607 * @return the priority key of the Attachment 608 */ 609 private static int getPriority(Attachment att) { 610 int priorityClass = PRIORITY_NONE; 611 int flags = att.mFlags; 612 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 613 priorityClass = PRIORITY_SEND_MAIL; 614 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 615 priorityClass = PRIORITY_FOREGROUND; 616 } 617 return priorityClass; 618 } 619 620 private void kick() { 621 synchronized(mLock) { 622 mLock.notify(); 623 } 624 } 625 626 /** 627 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 628 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 629 * single callback that's defined by the EmailServiceCallback interface. 630 */ 631 private class ServiceCallback extends IEmailServiceCallback.Stub { 632 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 633 int progress) { 634 // Record status and progress 635 DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId); 636 if (req != null) { 637 if (Email.DEBUG) { 638 String code; 639 switch(statusCode) { 640 case EmailServiceStatus.SUCCESS: code = "Success"; break; 641 case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break; 642 default: code = Integer.toString(statusCode); break; 643 } 644 if (statusCode != EmailServiceStatus.IN_PROGRESS) { 645 Log.d(TAG, ">> Attachment " + attachmentId + ": " + code); 646 } else if (progress >= (req.lastProgress + 15)) { 647 Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%"); 648 } 649 } 650 req.lastStatusCode = statusCode; 651 req.lastProgress = progress; 652 req.lastCallbackTime = System.currentTimeMillis(); 653 } 654 switch (statusCode) { 655 case EmailServiceStatus.IN_PROGRESS: 656 break; 657 default: 658 mDownloadSet.endDownload(attachmentId, statusCode); 659 break; 660 } 661 } 662 663 @Override 664 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 665 throws RemoteException { 666 } 667 668 @Override 669 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 670 throws RemoteException { 671 } 672 673 @Override 674 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 675 throws RemoteException { 676 } 677 } 678 679 /** 680 * Return an Intent to be used used based on the account type of the provided account id. We 681 * cache the results to avoid repeated database access 682 * @param accountId the id of the account 683 * @return the Intent to be used for the account or null (if the account no longer exists) 684 */ 685 private synchronized Intent getServiceIntentForAccount(long accountId) { 686 // TODO: We should have some more data-driven way of determining the service intent. 687 Intent serviceIntent = mAccountServiceMap.get(accountId); 688 if (serviceIntent == null) { 689 String protocol = Account.getProtocol(mContext, accountId); 690 if (protocol == null) return null; 691 serviceIntent = new Intent(mContext, ControllerService.class); 692 if (protocol.equals("eas")) { 693 serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT); 694 } 695 mAccountServiceMap.put(accountId, serviceIntent); 696 } 697 return serviceIntent; 698 } 699 700 /*package*/ void addServiceIntentForTest(long accountId, Intent intent) { 701 mAccountServiceMap.put(accountId, intent); 702 } 703 704 /*package*/ void onChange(Attachment att) { 705 mDownloadSet.onChange(this, att); 706 } 707 708 /*package*/ boolean isQueued(long attachmentId) { 709 return mDownloadSet.findDownloadRequest(attachmentId) != null; 710 } 711 712 /*package*/ int getSize() { 713 return mDownloadSet.size(); 714 } 715 716 /*package*/ boolean dequeue(long attachmentId) { 717 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 718 if (req != null) { 719 if (Email.DEBUG) { 720 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 721 } 722 mDownloadSet.remove(req); 723 return true; 724 } 725 return false; 726 } 727 728 /** 729 * Ask the service for the number of items in the download queue 730 * @return the number of items queued for download 731 */ 732 public static int getQueueSize() { 733 AttachmentDownloadService service = sRunningService; 734 if (service != null) { 735 return service.getSize(); 736 } 737 return 0; 738 } 739 740 /** 741 * Ask the service whether a particular attachment is queued for download 742 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 743 * @return whether or not the attachment is queued for download 744 */ 745 public static boolean isAttachmentQueued(long attachmentId) { 746 AttachmentDownloadService service = sRunningService; 747 if (service != null) { 748 return service.isQueued(attachmentId); 749 } 750 return false; 751 } 752 753 /** 754 * Ask the service to remove an attachment from the download queue 755 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 756 * @return whether or not the attachment was removed from the queue 757 */ 758 public static boolean cancelQueuedAttachment(long attachmentId) { 759 AttachmentDownloadService service = sRunningService; 760 if (service != null) { 761 return service.dequeue(attachmentId); 762 } 763 return false; 764 } 765 766 public static void watchdogAlarm() { 767 AttachmentDownloadService service = sRunningService; 768 if (service != null) { 769 service.mDownloadSet.onWatchdogAlarm(); 770 } 771 } 772 773 /** 774 * Called directly by EmailProvider whenever an attachment is inserted or changed 775 * @param id the attachment's id 776 * @param flags the new flags for the attachment 777 */ 778 public static void attachmentChanged(final long id, final int flags) { 779 final AttachmentDownloadService service = sRunningService; 780 if (service == null) return; 781 Utility.runAsync(new Runnable() { 782 public void run() { 783 final Attachment attachment = 784 Attachment.restoreAttachmentWithId(service, id); 785 if (attachment != null) { 786 // Store the flags we got from EmailProvider; given that all of this 787 // activity is asynchronous, we need to use the newest data from 788 // EmailProvider 789 attachment.mFlags = flags; 790 service.onChange(attachment); 791 } 792 }}); 793 } 794 795 /** 796 * Determine whether an attachment can be prefetched for the given account 797 * @return true if download is allowed, false otherwise 798 */ 799 public boolean canPrefetchForAccount(Account account, File dir) { 800 // Check account, just in case 801 if (account == null) return false; 802 // First, check preference and quickly return if prefetch isn't allowed 803 if ((account.mFlags & Account.FLAGS_BACKGROUND_ATTACHMENTS) == 0) return false; 804 805 long totalStorage = dir.getTotalSpace(); 806 long usableStorage = dir.getUsableSpace(); 807 long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); 808 809 // If there's not enough overall storage available, stop now 810 if (usableStorage < minAvailable) { 811 return false; 812 } 813 814 int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); 815 long perAccountMaxStorage = 816 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); 817 818 // Retrieve our idea of currently used attachment storage; since we don't track deletions, 819 // this number is the "worst case". If the number is greater than what's allowed per 820 // account, we walk the directory to determine the actual number 821 Long accountStorage = mAttachmentStorageMap.get(account.mId); 822 if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { 823 // Calculate the exact figure for attachment storage for this account 824 accountStorage = 0L; 825 File[] files = dir.listFiles(); 826 if (files != null) { 827 for (File file : files) { 828 accountStorage += file.length(); 829 } 830 } 831 // Cache the value 832 mAttachmentStorageMap.put(account.mId, accountStorage); 833 } 834 835 // Return true if we're using less than the maximum per account 836 if (accountStorage < perAccountMaxStorage) { 837 return true; 838 } else { 839 if (Email.DEBUG) { 840 Log.d(TAG, ">> Prefetch not allowed for account " + account.mId + "; used " + 841 accountStorage + ", limit " + perAccountMaxStorage); 842 } 843 return false; 844 } 845 } 846 847 public void run() { 848 // These fields are only used within the service thread 849 mContext = this; 850 mConnectivityManager = new EmailConnectivityManager(this, TAG); 851 mAccountManagerStub = new AccountManagerStub(this); 852 853 // Run through all attachments in the database that require download and add them to 854 // the queue 855 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 856 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 857 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 858 new String[] {Integer.toString(mask)}, null); 859 try { 860 Log.d(TAG, "Count: " + c.getCount()); 861 while (c.moveToNext()) { 862 Attachment attachment = Attachment.restoreAttachmentWithId( 863 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 864 if (attachment != null) { 865 mDownloadSet.onChange(this, attachment); 866 } 867 } 868 } catch (Exception e) { 869 e.printStackTrace(); 870 } 871 finally { 872 c.close(); 873 } 874 875 // Loop until stopped, with a 30 minute wait loop 876 while (!mStop) { 877 // Here's where we run our attachment loading logic... 878 mConnectivityManager.waitForConnectivity(); 879 mDownloadSet.processQueue(); 880 synchronized(mLock) { 881 try { 882 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 883 } catch (InterruptedException e) { 884 // That's ok; we'll just keep looping 885 } 886 } 887 } 888 889 // Unregister now that we're done 890 mConnectivityManager.unregister(); 891 } 892 893 @Override 894 public int onStartCommand(Intent intent, int flags, int startId) { 895 sRunningService = this; 896 return Service.START_STICKY; 897 } 898 899 /** 900 * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called 901 * throughout the code, in particular 1) after boot and 2) after accounts are added or removed 902 * The goal is that this service should be running at all times when there's at least one 903 * email account present. 904 */ 905 @Override 906 public void onCreate() { 907 // Start up our service thread 908 new Thread(this, "AttachmentDownloadService").start(); 909 } 910 @Override 911 public IBinder onBind(Intent intent) { 912 return null; 913 } 914 915 @Override 916 public void onDestroy() { 917 Log.d(TAG, "**** ON DESTROY!"); 918 if (sRunningService != null) { 919 mStop = true; 920 kick(); 921 } 922 sRunningService = null; 923 } 924 925 @Override 926 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 927 pw.println("AttachmentDownloadService"); 928 long time = System.currentTimeMillis(); 929 synchronized(mDownloadSet) { 930 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 931 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 932 // First, start up any required downloads, in priority order 933 while (iterator.hasNext()) { 934 DownloadRequest req = iterator.next(); 935 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 936 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 937 (req.inProgress ? " [In progress]" : "")); 938 Attachment att = Attachment.restoreAttachmentWithId(this, req.attachmentId); 939 if (att == null) { 940 pw.println(" Attachment not in database?"); 941 } else if (att.mFileName != null) { 942 String fileName = att.mFileName; 943 String suffix = "[none]"; 944 int lastDot = fileName.lastIndexOf('.'); 945 if (lastDot >= 0) { 946 suffix = fileName.substring(lastDot); 947 } 948 pw.print(" Suffix: " + suffix); 949 if (att.mContentUri != null) { 950 pw.print(" ContentUri: " + att.mContentUri); 951 } 952 pw.print(" Mime: "); 953 if (att.mMimeType != null) { 954 pw.print(att.mMimeType); 955 } else { 956 pw.print(AttachmentUtilities.inferMimeType(fileName, null)); 957 pw.print(" [inferred]"); 958 } 959 pw.println(" Size: " + att.mSize); 960 } 961 if (req.inProgress) { 962 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 963 req.lastProgress); 964 pw.println(" Started: " + req.startTime + ", Callback: " + 965 req.lastCallbackTime); 966 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 967 if (req.lastCallbackTime > 0) { 968 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 969 } 970 } 971 } 972 } 973 } 974} 975