AttachmentDownloadService.java revision 7fbcefff7d5a745335d1ec562e783a59087cc0b1
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.Controller.ControllerService; 20import com.android.email.Email; 21import com.android.email.ExchangeUtils.NullEmailService; 22import com.android.email.NotificationController; 23import com.android.email.Preferences; 24import com.android.email.Utility; 25import com.android.email.provider.AttachmentProvider; 26import com.android.email.provider.EmailContent; 27import com.android.email.provider.EmailContent.Account; 28import com.android.email.provider.EmailContent.Attachment; 29import com.android.email.provider.EmailContent.AttachmentColumns; 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 67 private static final int PRIORITY_NONE = -1; 68 @SuppressWarnings("unused") 69 // Low priority will be used for opportunistic downloads 70 private static final int PRIORITY_LOW = 0; 71 // Normal priority is for forwarded downloads in outgoing mail 72 private static final int PRIORITY_NORMAL = 1; 73 // High priority is for user requests 74 private static final int PRIORITY_HIGH = 2; 75 76 // Minimum free storage in order to perform prefetch (25% of total memory) 77 private static final float PREFETCH_MINIMUM_STORAGE_AVAILABLE = 0.25F; 78 // Maximum prefetch storage (also 25% of total memory) 79 private static final float PREFETCH_MAXIMUM_ATTACHMENT_STORAGE = 0.25F; 80 81 // We can try various values here; I think 2 is completely reasonable as a first pass 82 private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; 83 // Limit on the number of simultaneous downloads per account 84 // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) 85 private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; 86 87 private static final Uri SINGLE_ATTACHMENT_URI = 88 EmailContent.uriWithLimit(Attachment.CONTENT_URI, 1); 89 90 /*package*/ static AttachmentDownloadService sRunningService = null; 91 92 /*package*/ Context mContext; 93 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 // Respect the user's preference for background downloads 323 if (!mPreferences.getBackgroundAttachments()) { 324 return; 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 boolean repeat = true; 332 while (repeat) { 333 // We'll take the most recent unloaded attachment 334 Long prefetchId = Utility.getFirstRowLong(mContext, SINGLE_ATTACHMENT_URI, 335 Attachment.ID_PROJECTION, AttachmentColumns.CONTENT_URI 336 + " isnull AND " + Attachment.FLAGS + "=0", null, 337 Attachment.RECORD_ID + " DESC", Attachment.ID_PROJECTION_COLUMN); 338 if (prefetchId == null) break; 339 if (Email.DEBUG) { 340 Log.d(TAG, ">> Prefetch attachment " + prefetchId); 341 } 342 Attachment att = Attachment.restoreAttachmentWithId(mContext, prefetchId); 343 // If att is null, the attachment must have been deleted out from under us 344 if (att == null) continue; 345 if (getServiceClassForAccount(att.mAccountKey) == null) { 346 // Clean up this orphaned attachment; there's no point in keeping it 347 // around; then try to find another one 348 EmailContent.delete(mContext, Attachment.CONTENT_URI, prefetchId); 349 continue; 350 } 351 repeat = false; 352 // TODO It's possible that we're just over limit for this particular account 353 // Handle this so that attachments from other accounts (if any) can be tried 354 if (canPrefetchForAccount(att.mAccountKey, mContext.getCacheDir())) { 355 DownloadRequest req = new DownloadRequest(mContext, att); 356 mDownloadSet.tryStartDownload(req); 357 } 358 } 359 } 360 } 361 362 /** 363 * Count the number of running downloads in progress for this account 364 * @param accountId the id of the account 365 * @return the count of running downloads 366 */ 367 /*package*/ synchronized int downloadsForAccount(long accountId) { 368 int count = 0; 369 for (DownloadRequest req: mDownloadsInProgress.values()) { 370 if (req.accountId == accountId) { 371 count++; 372 } 373 } 374 return count; 375 } 376 377 private void onWatchdogAlarm() { 378 long now = System.currentTimeMillis(); 379 for (DownloadRequest req: mDownloadsInProgress.values()) { 380 // Check how long it's been since receiving a callback 381 long timeSinceCallback = now - req.lastCallbackTime; 382 if (timeSinceCallback > CALLBACK_TIMEOUT) { 383 if (Email.DEBUG) { 384 Log.d(TAG, "== Download of " + req.attachmentId + " timed out"); 385 } 386 cancelDownload(req); 387 // STOPSHIP Remove this before ship 388 } else if (Email.DEBUG) { 389 Log.d(TAG, "== , Download of " + req.attachmentId + 390 " last callback " + (timeSinceCallback/1000) + " secs ago"); 391 } 392 } 393 // If there are downloads in progress, reset alarm 394 if (mDownloadsInProgress.isEmpty()) { 395 if (mAlarmManager != null && mWatchdogPendingIntent != null) { 396 mAlarmManager.cancel(mWatchdogPendingIntent); 397 } 398 } 399 // Check whether we can start new downloads... 400 processQueue(); 401 } 402 403 /** 404 * Do the work of starting an attachment download using the EmailService interface, and 405 * set our watchdog alarm 406 * 407 * @param serviceClass the class that will attempt the download 408 * @param req the DownloadRequest 409 * @throws RemoteException 410 */ 411 private void startDownload(Class<? extends Service> serviceClass, DownloadRequest req) 412 throws RemoteException { 413 File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId, 414 req.attachmentId); 415 req.startTime = System.currentTimeMillis(); 416 req.inProgress = true; 417 mDownloadsInProgress.put(req.attachmentId, req); 418 if (serviceClass.equals(NullEmailService.class)) return; 419 // Now, call the service 420 EmailServiceProxy proxy = 421 new EmailServiceProxy(mContext, serviceClass, mServiceCallback); 422 proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(), 423 AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId) 424 .toString()); 425 // Lazily initialize our (reusable) pending intent 426 if (mWatchdogPendingIntent == null) { 427 Intent alarmIntent = new Intent(mContext, Watchdog.class); 428 mWatchdogPendingIntent = PendingIntent.getBroadcast(mContext, 0, alarmIntent, 0); 429 mAlarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 430 } 431 // Set the alarm 432 mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, 433 System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL, 434 mWatchdogPendingIntent); 435 } 436 437 private synchronized DownloadRequest getDownloadInProgress(long attachmentId) { 438 return mDownloadsInProgress.get(attachmentId); 439 } 440 441 /** 442 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 443 * parameter 444 * @param req the DownloadRequest 445 * @return whether or not the download was started 446 */ 447 /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { 448 Class<? extends Service> serviceClass = getServiceClassForAccount(req.accountId); 449 if (serviceClass == null) return false; 450 try { 451 if (Email.DEBUG) { 452 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 453 } 454 startDownload(serviceClass, req); 455 } catch (RemoteException e) { 456 // TODO: Consider whether we need to do more in this case... 457 // For now, fix up our data to reflect the failure 458 cancelDownload(req); 459 } 460 return true; 461 } 462 463 private void cancelDownload(DownloadRequest req) { 464 mDownloadsInProgress.remove(req.attachmentId); 465 req.inProgress = false; 466 } 467 468 /** 469 * Called when a download is finished; we get notified of this via our EmailServiceCallback 470 * @param attachmentId the id of the attachment whose download is finished 471 * @param statusCode the EmailServiceStatus code returned by the Service 472 */ 473 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 474 // Say we're no longer downloading this 475 mDownloadsInProgress.remove(attachmentId); 476 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 477 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 478 // If this needs to be retried, just process the queue again 479 if (Email.DEBUG) { 480 Log.d(TAG, "== The download for attachment #" + attachmentId + 481 " will be retried"); 482 } 483 if (req != null) { 484 req.inProgress = false; 485 } 486 kick(); 487 return; 488 } 489 490 // Remove the request from the queue 491 remove(req); 492 if (Email.DEBUG) { 493 long secs = 0; 494 if (req != null) { 495 secs = (System.currentTimeMillis() - req.time) / 1000; 496 } 497 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 498 "Error " + statusCode; 499 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 500 " seconds from request, status: " + status); 501 } 502 503 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 504 if (attachment != null) { 505 long accountId = attachment.mAccountKey; 506 // Update our attachment storage for this account 507 Long currentStorage = mAttachmentStorageMap.get(accountId); 508 if (currentStorage == null) { 509 currentStorage = 0L; 510 } 511 mAttachmentStorageMap.put(accountId, currentStorage + attachment.mSize); 512 boolean deleted = false; 513 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 514 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 515 // If this is a forwarding download, and the attachment doesn't exist (or 516 // can't be downloaded) delete it from the outgoing message, lest that 517 // message never get sent 518 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 519 // TODO: Talk to UX about whether this is even worth doing 520 NotificationController nc = NotificationController.getInstance(mContext); 521 nc.showDownloadForwardFailedNotification(attachment); 522 deleted = true; 523 } 524 // If we're an attachment on forwarded mail, and if we're not still blocked, 525 // try to send pending mail now (as mediated by MailService) 526 if ((req != null) && 527 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 528 if (Email.DEBUG) { 529 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 530 } 531 MailService.actionSendPendingMail(mContext, req.accountId); 532 } 533 } 534 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 /** 553 * Calculate the download priority of an Attachment. A priority of zero means that the 554 * attachment is not marked for download. 555 * @param att the Attachment 556 * @return the priority key of the Attachment 557 */ 558 private static int getPriority(Attachment att) { 559 int priorityClass = PRIORITY_NONE; 560 int flags = att.mFlags; 561 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 562 priorityClass = PRIORITY_NORMAL; 563 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 564 priorityClass = PRIORITY_HIGH; 565 } 566 return priorityClass; 567 } 568 569 private void kick() { 570 synchronized(mLock) { 571 mLock.notify(); 572 } 573 } 574 575 /** 576 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 577 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 578 * single callback that's defined by the EmailServiceCallback interface. 579 */ 580 private class ServiceCallback extends IEmailServiceCallback.Stub { 581 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 582 int progress) { 583 // Record status and progress 584 DownloadRequest req = mDownloadSet.getDownloadInProgress(attachmentId); 585 if (req != null) { 586 if (Email.DEBUG) { 587 String code; 588 switch(statusCode) { 589 case EmailServiceStatus.SUCCESS: code = "Success"; break; 590 case EmailServiceStatus.IN_PROGRESS: code = "In progress"; break; 591 default: code = Integer.toString(statusCode); break; 592 } 593 if (statusCode != EmailServiceStatus.IN_PROGRESS) { 594 Log.d(TAG, ">> Attachment " + attachmentId + ": " + code); 595 } else if (progress >= (req.lastProgress + 15)) { 596 Log.d(TAG, ">> Attachment " + attachmentId + ": " + progress + "%"); 597 } 598 } 599 req.lastStatusCode = statusCode; 600 req.lastProgress = progress; 601 req.lastCallbackTime = System.currentTimeMillis(); 602 } 603 switch (statusCode) { 604 case EmailServiceStatus.IN_PROGRESS: 605 break; 606 default: 607 mDownloadSet.endDownload(attachmentId, statusCode); 608 break; 609 } 610 } 611 612 @Override 613 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 614 throws RemoteException { 615 } 616 617 @Override 618 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 619 throws RemoteException { 620 } 621 622 @Override 623 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 624 throws RemoteException { 625 } 626 } 627 628 /** 629 * Return the class of the service used by the account type of the provided account id. We 630 * cache the results to avoid repeated database access 631 * @param accountId the id of the account 632 * @return the service class for the account or null (if the account no longer exists) 633 */ 634 private synchronized Class<? extends Service> getServiceClassForAccount(long accountId) { 635 // TODO: We should have some more data-driven way of determining the service class. I'd 636 // suggest adding an attribute in the stores.xml file 637 Class<? extends Service> serviceClass = mAccountServiceMap.get(accountId); 638 if (serviceClass == null) { 639 String protocol = Account.getProtocol(mContext, accountId); 640 if (protocol == null) return null; 641 if (protocol.equals("eas")) { 642 serviceClass = ExchangeService.class; 643 } else { 644 serviceClass = ControllerService.class; 645 } 646 mAccountServiceMap.put(accountId, serviceClass); 647 } 648 return serviceClass; 649 } 650 651 /*protected*/ void addServiceClass(long accountId, Class<? extends Service> serviceClass) { 652 mAccountServiceMap.put(accountId, serviceClass); 653 } 654 655 /*package*/ void onChange(Attachment att) { 656 mDownloadSet.onChange(att); 657 } 658 659 /*package*/ boolean isQueued(long attachmentId) { 660 return mDownloadSet.findDownloadRequest(attachmentId) != null; 661 } 662 663 /*package*/ int getSize() { 664 return mDownloadSet.size(); 665 } 666 667 /*package*/ boolean dequeue(long attachmentId) { 668 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 669 if (req != null) { 670 if (Email.DEBUG) { 671 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 672 } 673 mDownloadSet.remove(req); 674 return true; 675 } 676 return false; 677 } 678 679 /** 680 * Ask the service for the number of items in the download queue 681 * @return the number of items queued for download 682 */ 683 public static int getQueueSize() { 684 if (sRunningService != null) { 685 return sRunningService.getSize(); 686 } 687 return 0; 688 } 689 690 /** 691 * Ask the service whether a particular attachment is queued for download 692 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 693 * @return whether or not the attachment is queued for download 694 */ 695 public static boolean isAttachmentQueued(long attachmentId) { 696 if (sRunningService != null) { 697 return sRunningService.isQueued(attachmentId); 698 } 699 return false; 700 } 701 702 /** 703 * Ask the service to remove an attachment from the download queue 704 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 705 * @return whether or not the attachment was removed from the queue 706 */ 707 public static boolean cancelQueuedAttachment(long attachmentId) { 708 if (sRunningService != null) { 709 return sRunningService.dequeue(attachmentId); 710 } 711 return false; 712 } 713 714 public static void watchdogAlarm() { 715 if (sRunningService != null) { 716 sRunningService.mDownloadSet.onWatchdogAlarm(); 717 } 718 } 719 720 /** 721 * Called directly by EmailProvider whenever an attachment is inserted or changed 722 * @param id the attachment's id 723 * @param flags the new flags for the attachment 724 */ 725 public static void attachmentChanged(final long id, final int flags) { 726 if (sRunningService == null) return; 727 Utility.runAsync(new Runnable() { 728 public void run() { 729 final Attachment attachment = 730 Attachment.restoreAttachmentWithId(sRunningService, id); 731 if (attachment != null) { 732 // Store the flags we got from EmailProvider; given that all of this 733 // activity is asynchronous, we need to use the newest data from 734 // EmailProvider 735 attachment.mFlags = flags; 736 sRunningService.onChange(attachment); 737 } 738 }}); 739 } 740 741 /** 742 * Determine whether an attachment can be prefetched for the given account 743 * @return true if download is allowed, false otherwise 744 */ 745 /*package*/ boolean canPrefetchForAccount(long accountId, File dir) { 746 long totalStorage = dir.getTotalSpace(); 747 long usableStorage = dir.getUsableSpace(); 748 long minAvailable = (long)(totalStorage * PREFETCH_MINIMUM_STORAGE_AVAILABLE); 749 750 // If there's not enough overall storage available, stop now 751 if (usableStorage < minAvailable) { 752 return false; 753 } 754 755 int numberOfAccounts = mAccountManagerStub.getNumberOfAccounts(); 756 long perAccountMaxStorage = 757 (long)(totalStorage * PREFETCH_MAXIMUM_ATTACHMENT_STORAGE / numberOfAccounts); 758 759 // Retrieve our idea of currently used attachment storage; since we don't track deletions, 760 // this number is the "worst case". If the number is greater than what's allowed per 761 // account, we walk the directory to determine the actual number 762 Long accountStorage = mAttachmentStorageMap.get(accountId); 763 if (accountStorage == null || (accountStorage > perAccountMaxStorage)) { 764 // Calculate the exact figure for attachment storage for this account 765 accountStorage = 0L; 766 File[] files = dir.listFiles(); 767 if (files != null) { 768 for (File file : files) { 769 accountStorage += file.length(); 770 } 771 } 772 // Cache the value 773 mAttachmentStorageMap.put(accountId, accountStorage); 774 } 775 776 // Return true if we're using less than the maximum per account 777 if (accountStorage < perAccountMaxStorage) { 778 return true; 779 } else { 780 if (Email.DEBUG) { 781 Log.d(TAG, ">> Prefetch not allowed for account " + accountId + "; used " + 782 accountStorage + ", limit " + perAccountMaxStorage); 783 } 784 return false; 785 } 786 } 787 788 public void run() { 789 mContext = this; 790 mAccountManagerStub = new AccountManagerStub(this); 791 mPreferences = Preferences.getPreferences(this); 792 793 // Run through all attachments in the database that require download and add them to 794 // the queue 795 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 796 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 797 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 798 new String[] {Integer.toString(mask)}, null); 799 try { 800 Log.d(TAG, "Count: " + c.getCount()); 801 while (c.moveToNext()) { 802 Attachment attachment = Attachment.restoreAttachmentWithId( 803 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 804 if (attachment != null) { 805 mDownloadSet.onChange(attachment); 806 } 807 } 808 } catch (Exception e) { 809 e.printStackTrace(); 810 } 811 finally { 812 c.close(); 813 } 814 815 // Loop until stopped, with a 30 minute wait loop 816 while (!mStop) { 817 // Here's where we run our attachment loading logic... 818 mDownloadSet.processQueue(); 819 synchronized(mLock) { 820 try { 821 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 822 } catch (InterruptedException e) { 823 // That's ok; we'll just keep looping 824 } 825 } 826 } 827 } 828 829 @Override 830 public int onStartCommand(Intent intent, int flags, int startId) { 831 sRunningService = this; 832 return Service.START_STICKY; 833 } 834 835 /** 836 * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called 837 * throughout the code, in particular 1) after boot and 2) after accounts are added or removed 838 * The goal is that this service should be running at all times when there's at least one 839 * email account present. 840 */ 841 @Override 842 public void onCreate() { 843 // Start up our service thread 844 new Thread(this, "AttachmentDownloadService").start(); 845 } 846 @Override 847 public IBinder onBind(Intent intent) { 848 return null; 849 } 850 851 @Override 852 public void onDestroy() { 853 Log.d(TAG, "**** ON DESTROY!"); 854 if (sRunningService != null) { 855 mStop = true; 856 kick(); 857 } 858 sRunningService = null; 859 } 860 861 @Override 862 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 863 pw.println("AttachmentDownloadService"); 864 long time = System.currentTimeMillis(); 865 synchronized(mDownloadSet) { 866 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 867 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 868 // First, start up any required downloads, in priority order 869 while (iterator.hasNext()) { 870 DownloadRequest req = iterator.next(); 871 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 872 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 873 (req.inProgress ? " [In progress]" : "")); 874 Attachment att = Attachment.restoreAttachmentWithId(mContext, req.attachmentId); 875 if (att == null) { 876 pw.println(" Attachment not in database?"); 877 } else if (att.mFileName != null) { 878 String fileName = att.mFileName; 879 String suffix = "[none]"; 880 int lastDot = fileName.lastIndexOf('.'); 881 if (lastDot >= 0) { 882 suffix = fileName.substring(lastDot); 883 } 884 pw.print(" Suffix: " + suffix); 885 if (att.mContentUri != null) { 886 pw.print(" ContentUri: " + att.mContentUri); 887 } 888 pw.print(" Mime: "); 889 if (att.mMimeType != null) { 890 pw.print(att.mMimeType); 891 } else { 892 pw.print(AttachmentProvider.inferMimeType(fileName, null)); 893 pw.print(" [inferred]"); 894 } 895 pw.println(" Size: " + att.mSize); 896 } 897 if (req.inProgress) { 898 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 899 req.lastProgress); 900 pw.println(" Started: " + req.startTime + ", Callback: " + 901 req.lastCallbackTime); 902 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 903 if (req.lastCallbackTime > 0) { 904 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 905 } 906 } 907 } 908 } 909 } 910} 911