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