AttachmentDownloadService.java revision 69fc25244ba1b30856426c77c2e4be3964eb50da
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.Utility; 24import com.android.email.provider.AttachmentProvider; 25import com.android.email.provider.EmailContent; 26import com.android.email.provider.EmailContent.Account; 27import com.android.email.provider.EmailContent.Attachment; 28import com.android.email.provider.EmailContent.Message; 29import com.android.exchange.ExchangeService; 30 31import android.app.AlarmManager; 32import android.app.PendingIntent; 33import android.app.Service; 34import android.content.BroadcastReceiver; 35import android.content.ContentValues; 36import android.content.Context; 37import android.content.Intent; 38import android.database.Cursor; 39import android.os.IBinder; 40import android.os.RemoteException; 41import android.text.format.DateUtils; 42import android.util.Log; 43 44import java.io.File; 45import java.io.FileDescriptor; 46import java.io.PrintWriter; 47import java.util.Comparator; 48import java.util.HashMap; 49import java.util.Iterator; 50import java.util.TreeSet; 51import java.util.concurrent.ConcurrentHashMap; 52 53public class AttachmentDownloadService extends Service implements Runnable { 54 public static final String TAG = "AttachmentService"; 55 56 // Our idle time, waiting for notifications; this is something of a failsafe 57 private static final int PROCESS_QUEUE_WAIT_TIME = 30 * ((int)DateUtils.MINUTE_IN_MILLIS); 58 // How often our watchdog checks for callback timeouts 59 private static final int WATCHDOG_CHECK_INTERVAL = 15 * ((int)DateUtils.SECOND_IN_MILLIS); 60 // How long we'll wait for a callback before canceling a download and retrying 61 private static final int CALLBACK_TIMEOUT = 30 * ((int)DateUtils.SECOND_IN_MILLIS); 62 63 private static final int PRIORITY_NONE = -1; 64 @SuppressWarnings("unused") 65 // Low priority will be used for opportunistic downloads 66 private static final int PRIORITY_LOW = 0; 67 // Normal priority is for forwarded downloads in outgoing mail 68 private static final int PRIORITY_NORMAL = 1; 69 // High priority is for user requests 70 private static final int PRIORITY_HIGH = 2; 71 72 // We can try various values here; I think 2 is completely reasonable as a first pass 73 private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; 74 // Limit on the number of simultaneous downloads per account 75 // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) 76 private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; 77 78 /*package*/ static AttachmentDownloadService sRunningService = null; 79 80 /*package*/ Context mContext; 81 /*package*/ final DownloadSet mDownloadSet = new DownloadSet(new DownloadComparator()); 82 83 private final HashMap<Long, Class<? extends Service>> mAccountServiceMap = 84 new HashMap<Long, Class<? extends Service>>(); 85 private final ServiceCallback mServiceCallback = new ServiceCallback(); 86 private final Object mLock = new Object(); 87 private volatile boolean mStop = false; 88 89 90 /** 91 * Watchdog alarm receiver; responsible for making sure that downloads in progress are not 92 * stalled, as determined by the timing of the most recent service callback 93 */ 94 public static class Watchdog extends BroadcastReceiver { 95 @Override 96 public void onReceive(final Context context, Intent intent) { 97 new Thread(new Runnable() { 98 public void run() { 99 watchdogAlarm(); 100 } 101 }, "AttachmentDownloadService Watchdog").start(); 102 } 103 } 104 105 public static class DownloadRequest { 106 final int priority; 107 final long time; 108 final long attachmentId; 109 final long messageId; 110 final long accountId; 111 boolean inProgress = false; 112 int lastStatusCode; 113 int lastProgress; 114 long lastCallbackTime; 115 long startTime; 116 117 private DownloadRequest(Context context, Attachment attachment) { 118 attachmentId = attachment.mId; 119 Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey); 120 if (msg != null) { 121 accountId = msg.mAccountKey; 122 messageId = msg.mId; 123 } else { 124 accountId = messageId = -1; 125 } 126 priority = getPriority(attachment); 127 time = System.currentTimeMillis(); 128 } 129 130 @Override 131 public int hashCode() { 132 return (int)attachmentId; 133 } 134 135 /** 136 * Two download requests are equals if their attachment id's are equals 137 */ 138 @Override 139 public boolean equals(Object object) { 140 if (!(object instanceof DownloadRequest)) return false; 141 DownloadRequest req = (DownloadRequest)object; 142 return req.attachmentId == attachmentId; 143 } 144 } 145 146 /** 147 * Comparator class for the download set; we first compare by priority. Requests with equal 148 * priority are compared by the time the request was created (older requests come first) 149 */ 150 /*protected*/ static class DownloadComparator implements Comparator<DownloadRequest> { 151 @Override 152 public int compare(DownloadRequest req1, DownloadRequest req2) { 153 int res; 154 if (req1.priority != req2.priority) { 155 res = (req1.priority < req2.priority) ? -1 : 1; 156 } else { 157 if (req1.time == req2.time) { 158 res = 0; 159 } else { 160 res = (req1.time > req2.time) ? -1 : 1; 161 } 162 } 163 //Log.d(TAG, "Compare " + req1.attachmentId + " to " + req2.attachmentId + " = " + res); 164 return res; 165 } 166 } 167 168 /** 169 * The DownloadSet is a TreeSet sorted by priority class (e.g. low, high, etc.) and the 170 * time of the request. Higher priority requests 171 * are always processed first; among equals, the oldest request is processed first. The 172 * priority key represents this ordering. Note: All methods that change the attachment map are 173 * synchronized on the map itself 174 */ 175 /*package*/ class DownloadSet extends TreeSet<DownloadRequest> { 176 private static final long serialVersionUID = 1L; 177 private PendingIntent mWatchdogPendingIntent; 178 private AlarmManager mAlarmManager; 179 180 /*package*/ DownloadSet(Comparator<? super DownloadRequest> comparator) { 181 super(comparator); 182 } 183 184 /** 185 * Maps attachment id to DownloadRequest 186 */ 187 /*package*/ final ConcurrentHashMap<Long, DownloadRequest> mDownloadsInProgress = 188 new ConcurrentHashMap<Long, DownloadRequest>(); 189 190 /** 191 * onChange is called by the AttachmentReceiver upon receipt of a valid notification from 192 * EmailProvider that an attachment has been inserted or modified. It's not strictly 193 * necessary that we detect a deleted attachment, as the code always checks for the 194 * existence of an attachment before acting on it. 195 */ 196 public synchronized void onChange(Attachment att) { 197 DownloadRequest req = findDownloadRequest(att.mId); 198 long priority = getPriority(att); 199 if (priority == PRIORITY_NONE) { 200 if (Email.DEBUG) { 201 Log.d(TAG, "== Attachment changed: " + att.mId); 202 } 203 // In this case, there is no download priority for this attachment 204 if (req != null) { 205 // If it exists in the map, remove it 206 // NOTE: We don't yet support deleting downloads in progress 207 if (Email.DEBUG) { 208 Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing"); 209 } 210 remove(req); 211 } 212 } else { 213 // Ignore changes that occur during download 214 if (mDownloadsInProgress.containsKey(att.mId)) return; 215 // If this is new, add the request to the queue 216 if (req == null) { 217 req = new DownloadRequest(mContext, att); 218 add(req); 219 } 220 // If the request already existed, we'll update the priority (so that the time is 221 // up-to-date); otherwise, we create a new request 222 if (Email.DEBUG) { 223 Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " + 224 req.priority + ", priority time " + req.time); 225 } 226 } 227 // Process the queue if we're in a wait 228 kick(); 229 } 230 231 /** 232 * Find a queued DownloadRequest, given the attachment's id 233 * @param id the id of the attachment 234 * @return the DownloadRequest for that attachment (or null, if none) 235 */ 236 /*package*/ synchronized DownloadRequest findDownloadRequest(long id) { 237 Iterator<DownloadRequest> iterator = iterator(); 238 while(iterator.hasNext()) { 239 DownloadRequest req = iterator.next(); 240 if (req.attachmentId == id) { 241 return req; 242 } 243 } 244 return null; 245 } 246 247 /** 248 * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing 249 * the limit on maximum downloads 250 */ 251 /*package*/ synchronized void processQueue() { 252 if (Email.DEBUG) { 253 Log.d(TAG, "== Checking attachment queue, " + mDownloadSet.size() + " entries"); 254 } 255 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 256 // First, start up any required downloads, in priority order 257 while (iterator.hasNext() && 258 (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) { 259 DownloadRequest req = iterator.next(); 260 if (!req.inProgress) { 261 mDownloadSet.tryStartDownload(req); 262 } 263 } 264 // Then, try opportunistic download of appropriate attachments 265 int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size(); 266 if (backgroundDownloads > 0) { 267 // TODO Code for background downloads here 268 if (Email.DEBUG) { 269 Log.d(TAG, "== We'd look for up to " + backgroundDownloads + 270 " background download(s) now..."); 271 } 272 } 273 } 274 275 /** 276 * Count the number of running downloads in progress for this account 277 * @param accountId the id of the account 278 * @return the count of running downloads 279 */ 280 /*package*/ synchronized int downloadsForAccount(long accountId) { 281 int count = 0; 282 for (DownloadRequest req: mDownloadsInProgress.values()) { 283 if (req.accountId == accountId) { 284 count++; 285 } 286 } 287 return count; 288 } 289 290 private void onWatchdogAlarm() { 291 long now = System.currentTimeMillis(); 292 for (DownloadRequest req: mDownloadsInProgress.values()) { 293 // Check how long it's been since receiving a callback 294 long timeSinceCallback = now - req.lastCallbackTime; 295 if (timeSinceCallback > CALLBACK_TIMEOUT) { 296 if (Email.DEBUG) { 297 Log.d(TAG, "== , Download of " + req.attachmentId + 298 " timed out"); 299 } 300 cancelDownload(req); 301 // STOPSHIP Remove this before ship 302 } else if (Email.DEBUG) { 303 Log.d(TAG, "== , Download of " + req.attachmentId + 304 " last callback " + (timeSinceCallback/1000) + " secs ago"); 305 } 306 } 307 // If there are downloads in progress, reset alarm 308 if (mDownloadsInProgress.isEmpty()) { 309 if (mAlarmManager != null && mWatchdogPendingIntent != null) { 310 mAlarmManager.cancel(mWatchdogPendingIntent); 311 } 312 } 313 // Check whether we can start new downloads... 314 processQueue(); 315 } 316 317 /** 318 * Do the work of starting an attachment download using the EmailService interface, and 319 * set our watchdog alarm 320 * 321 * @param serviceClass the class that will attempt the download 322 * @param req the DownloadRequest 323 * @throws RemoteException 324 */ 325 private void startDownload(Class<? extends Service> serviceClass, DownloadRequest req) 326 throws RemoteException { 327 File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId, 328 req.attachmentId); 329 req.startTime = System.currentTimeMillis(); 330 req.inProgress = true; 331 mDownloadsInProgress.put(req.attachmentId, req); 332 if (serviceClass.equals(NullEmailService.class)) return; 333 // Now, call the service 334 EmailServiceProxy proxy = 335 new EmailServiceProxy(mContext, serviceClass, mServiceCallback); 336 proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(), 337 AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId) 338 .toString()); 339 // Lazily initialize our (reusable) pending intent 340 if (mWatchdogPendingIntent == null) { 341 Intent alarmIntent = new Intent(mContext, Watchdog.class); 342 mWatchdogPendingIntent = PendingIntent.getBroadcast(mContext, 0, alarmIntent, 0); 343 mAlarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); 344 } 345 // Set the alarm 346 mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, 347 System.currentTimeMillis() + WATCHDOG_CHECK_INTERVAL, WATCHDOG_CHECK_INTERVAL, 348 mWatchdogPendingIntent); 349 } 350 351 /** 352 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 353 * parameter 354 * @param req the DownloadRequest 355 * @return whether or not the download was started 356 */ 357 /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { 358 // Enforce per-account limit 359 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { 360 if (Email.DEBUG) { 361 Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + 362 req.accountId); 363 } 364 return false; 365 } 366 Class<? extends Service> serviceClass = getServiceClassForAccount(req.accountId); 367 if (serviceClass == null) return false; 368 try { 369 if (Email.DEBUG) { 370 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 371 } 372 startDownload(serviceClass, req); 373 } catch (RemoteException e) { 374 // TODO: Consider whether we need to do more in this case... 375 // For now, fix up our data to reflect the failure 376 cancelDownload(req); 377 } 378 return true; 379 } 380 381 private void cancelDownload(DownloadRequest req) { 382 mDownloadsInProgress.remove(req.attachmentId); 383 req.inProgress = false; 384 } 385 386 /** 387 * Called when a download is finished; we get notified of this via our EmailServiceCallback 388 * @param attachmentId the id of the attachment whose download is finished 389 * @param statusCode the EmailServiceStatus code returned by the Service 390 */ 391 /*package*/ synchronized void endDownload(long attachmentId, int statusCode) { 392 // Say we're no longer downloading this 393 mDownloadsInProgress.remove(attachmentId); 394 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 395 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 396 // If this needs to be retried, just process the queue again 397 if (Email.DEBUG) { 398 Log.d(TAG, "== The download for attachment #" + attachmentId + 399 " will be retried"); 400 } 401 if (req != null) { 402 req.inProgress = false; 403 } 404 kick(); 405 return; 406 } 407 408 // Remove the request from the queue 409 remove(req); 410 if (Email.DEBUG) { 411 long secs = 0; 412 if (req != null) { 413 secs = (System.currentTimeMillis() - req.time) / 1000; 414 } 415 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 416 "Error " + statusCode; 417 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 418 " seconds from request, status: " + status); 419 } 420 421 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 422 if (attachment != null) { 423 boolean deleted = false; 424 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 425 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 426 // If this is a forwarding download, and the attachment doesn't exist (or 427 // can't be downloaded) delete it from the outgoing message, lest that 428 // message never get sent 429 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 430 // TODO: Talk to UX about whether this is even worth doing 431 NotificationController nc = NotificationController.getInstance(mContext); 432 nc.showDownloadForwardFailedNotification(attachment); 433 deleted = true; 434 } 435 // If we're an attachment on forwarded mail, and if we're not still blocked, 436 // try to send pending mail now (as mediated by MailService) 437 if ((req != null) && 438 !Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 439 if (Email.DEBUG) { 440 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 441 } 442 MailService.actionSendPendingMail(mContext, req.accountId); 443 } 444 } 445 if (!deleted) { 446 // Clear the download flags, since we're done for now. Note that this happens 447 // only for non-recoverable errors. When these occur for forwarded mail, we can 448 // ignore it and continue; otherwise, it was either 1) a user request, in which 449 // case the user can retry manually or 2) an opportunistic download, in which 450 // case the download wasn't critical 451 ContentValues cv = new ContentValues(); 452 int flags = 453 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 454 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 455 attachment.update(mContext, cv); 456 } 457 } 458 // Process the queue 459 kick(); 460 } 461 } 462 463 /** 464 * Calculate the download priority of an Attachment. A priority of zero means that the 465 * attachment is not marked for download. 466 * @param att the Attachment 467 * @return the priority key of the Attachment 468 */ 469 private static int getPriority(Attachment att) { 470 int priorityClass = PRIORITY_NONE; 471 int flags = att.mFlags; 472 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 473 priorityClass = PRIORITY_NORMAL; 474 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 475 priorityClass = PRIORITY_HIGH; 476 } 477 return priorityClass; 478 } 479 480 private void kick() { 481 synchronized(mLock) { 482 mLock.notify(); 483 } 484 } 485 486 /** 487 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 488 * come from either Controller (IMAP) or ExchangeService (EAS). Note that we only implement the 489 * single callback that's defined by the EmailServiceCallback interface. 490 */ 491 private class ServiceCallback extends IEmailServiceCallback.Stub { 492 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 493 int progress) { 494 if (Email.DEBUG) { 495 String code; 496 switch(statusCode) { 497 case EmailServiceStatus.SUCCESS: 498 code = "Success"; 499 break; 500 case EmailServiceStatus.IN_PROGRESS: 501 code = "In progress"; 502 break; 503 default: 504 code = Integer.toString(statusCode); 505 } 506 Log.d(TAG, "loadAttachmentStatus, id = " + attachmentId + " code = "+ code + 507 ", " + progress + "%"); 508 } 509 // Record status and progress 510 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 511 if (req != null) { 512 req.lastStatusCode = statusCode; 513 req.lastProgress = progress; 514 req.lastCallbackTime = System.currentTimeMillis(); 515 } 516 switch (statusCode) { 517 case EmailServiceStatus.IN_PROGRESS: 518 break; 519 default: 520 mDownloadSet.endDownload(attachmentId, statusCode); 521 break; 522 } 523 } 524 525 @Override 526 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 527 throws RemoteException { 528 } 529 530 @Override 531 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 532 throws RemoteException { 533 } 534 535 @Override 536 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 537 throws RemoteException { 538 } 539 } 540 541 /** 542 * Return the class of the service used by the account type of the provided account id. We 543 * cache the results to avoid repeated database access 544 * @param accountId the id of the account 545 * @return the service class for the account 546 */ 547 private synchronized Class<? extends Service> getServiceClassForAccount(long accountId) { 548 // TODO: We should have some more data-driven way of determining the service class. I'd 549 // suggest adding an attribute in the stores.xml file 550 Class<? extends Service> serviceClass = mAccountServiceMap.get(accountId); 551 if (serviceClass == null) { 552 String protocol = Account.getProtocol(mContext, accountId); 553 if (protocol.equals("eas")) { 554 serviceClass = ExchangeService.class; 555 } else { 556 serviceClass = ControllerService.class; 557 } 558 mAccountServiceMap.put(accountId, serviceClass); 559 } 560 return serviceClass; 561 } 562 563 /*protected*/ void addServiceClass(long accountId, Class<? extends Service> serviceClass) { 564 mAccountServiceMap.put(accountId, serviceClass); 565 } 566 567 /*package*/ void onChange(Attachment att) { 568 mDownloadSet.onChange(att); 569 } 570 571 /*package*/ boolean isQueued(long attachmentId) { 572 return mDownloadSet.findDownloadRequest(attachmentId) != null; 573 } 574 575 /*package*/ int getSize() { 576 return mDownloadSet.size(); 577 } 578 579 /*package*/ boolean dequeue(long attachmentId) { 580 DownloadRequest req = mDownloadSet.findDownloadRequest(attachmentId); 581 if (req != null) { 582 if (Email.DEBUG) { 583 Log.d(TAG, "Dequeued attachmentId: " + attachmentId); 584 } 585 mDownloadSet.remove(req); 586 return true; 587 } 588 return false; 589 } 590 591 /** 592 * Ask the service for the number of items in the download queue 593 * @return the number of items queued for download 594 */ 595 public static int getQueueSize() { 596 if (sRunningService != null) { 597 return sRunningService.getSize(); 598 } 599 return 0; 600 } 601 602 /** 603 * Ask the service whether a particular attachment is queued for download 604 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 605 * @return whether or not the attachment is queued for download 606 */ 607 public static boolean isAttachmentQueued(long attachmentId) { 608 if (sRunningService != null) { 609 return sRunningService.isQueued(attachmentId); 610 } 611 return false; 612 } 613 614 /** 615 * Ask the service to remove an attachment from the download queue 616 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 617 * @return whether or not the attachment was removed from the queue 618 */ 619 public static boolean cancelQueuedAttachment(long attachmentId) { 620 if (sRunningService != null) { 621 return sRunningService.dequeue(attachmentId); 622 } 623 return false; 624 } 625 626 public static void watchdogAlarm() { 627 if (sRunningService != null) { 628 sRunningService.mDownloadSet.onWatchdogAlarm(); 629 } 630 } 631 632 /** 633 * Called directly by EmailProvider whenever an attachment is inserted or changed 634 * @param id the attachment's id 635 * @param flags the new flags for the attachment 636 */ 637 public static void attachmentChanged(final long id, final int flags) { 638 if (sRunningService == null) return; 639 Utility.runAsync(new Runnable() { 640 public void run() { 641 final Attachment attachment = 642 Attachment.restoreAttachmentWithId(sRunningService, id); 643 if (attachment != null) { 644 // Store the flags we got from EmailProvider; given that all of this 645 // activity is asynchronous, we need to use the newest data from 646 // EmailProvider 647 attachment.mFlags = flags; 648 sRunningService.onChange(attachment); 649 } 650 }}); 651 } 652 653 public void run() { 654 mContext = this; 655 // Run through all attachments in the database that require download and add them to 656 // the queue 657 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 658 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 659 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 660 new String[] {Integer.toString(mask)}, null); 661 try { 662 Log.d(TAG, "Count: " + c.getCount()); 663 while (c.moveToNext()) { 664 Attachment attachment = Attachment.restoreAttachmentWithId( 665 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 666 if (attachment != null) { 667 mDownloadSet.onChange(attachment); 668 } 669 } 670 } catch (Exception e) { 671 e.printStackTrace(); 672 } 673 finally { 674 c.close(); 675 } 676 677 // Loop until stopped, with a 30 minute wait loop 678 while (!mStop) { 679 // Here's where we run our attachment loading logic... 680 mDownloadSet.processQueue(); 681 synchronized(mLock) { 682 try { 683 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 684 } catch (InterruptedException e) { 685 // That's ok; we'll just keep looping 686 } 687 } 688 } 689 } 690 691 @Override 692 public int onStartCommand(Intent intent, int flags, int startId) { 693 sRunningService = this; 694 return Service.START_STICKY; 695 } 696 697 /** 698 * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called 699 * throughout the code, in particular 1) after boot and 2) after accounts are added or removed 700 * The goal is that this service should be running at all times when there's at least one 701 * email account present. 702 */ 703 @Override 704 public void onCreate() { 705 // Start up our service thread 706 new Thread(this, "AttachmentDownloadService").start(); 707 } 708 @Override 709 public IBinder onBind(Intent intent) { 710 return null; 711 } 712 713 @Override 714 public void onDestroy() { 715 Log.d(TAG, "**** ON DESTROY!"); 716 if (sRunningService != null) { 717 mStop = true; 718 kick(); 719 } 720 sRunningService = null; 721 } 722 723 @Override 724 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 725 pw.println("AttachmentDownloadService"); 726 long time = System.currentTimeMillis(); 727 synchronized(mDownloadSet) { 728 pw.println(" Queue, " + mDownloadSet.size() + " entries"); 729 Iterator<DownloadRequest> iterator = mDownloadSet.descendingIterator(); 730 // First, start up any required downloads, in priority order 731 while (iterator.hasNext()) { 732 DownloadRequest req = iterator.next(); 733 pw.println(" Account: " + req.accountId + ", Attachment: " + req.attachmentId); 734 pw.println(" Priority: " + req.priority + ", Time: " + req.time + 735 (req.inProgress ? " [In progress]" : "")); 736 Attachment att = Attachment.restoreAttachmentWithId(mContext, req.attachmentId); 737 if (att == null) { 738 pw.println(" Attachment not in database?"); 739 } else if (att.mFileName != null) { 740 String fileName = att.mFileName; 741 String suffix = "[none]"; 742 int lastDot = fileName.lastIndexOf('.'); 743 if (lastDot >= 0) { 744 suffix = fileName.substring(lastDot); 745 } 746 pw.print(" Suffix: " + suffix); 747 if (att.mContentUri != null) { 748 pw.print(" ContentUri: " + att.mContentUri); 749 } 750 pw.print(" Mime: "); 751 if (att.mMimeType != null) { 752 pw.print(att.mMimeType); 753 } else { 754 pw.print(AttachmentProvider.inferMimeType(fileName, null)); 755 pw.print(" [inferred]"); 756 } 757 pw.println(" Size: " + att.mSize); 758 } 759 if (req.inProgress) { 760 pw.println(" Status: " + req.lastStatusCode + ", Progress: " + 761 req.lastProgress); 762 pw.println(" Started: " + req.startTime + ", Callback: " + 763 req.lastCallbackTime); 764 pw.println(" Elapsed: " + ((time - req.startTime) / 1000L) + "s"); 765 if (req.lastCallbackTime > 0) { 766 pw.println(" CB: " + ((time - req.lastCallbackTime) / 1000L) + "s"); 767 } 768 } 769 } 770 } 771 } 772} 773