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