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