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