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