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