AttachmentDownloadService.java revision 7894ee82b3a9f22d460a0c6f79e87be27686a649
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.Email; 20import com.android.email.R; 21import com.android.email.Utility; 22import com.android.email.activity.Welcome; 23import com.android.email.provider.AttachmentProvider; 24import com.android.email.provider.EmailContent; 25import com.android.email.provider.EmailProvider; 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.SyncManager; 30 31import android.app.Notification; 32import android.app.NotificationManager; 33import android.app.PendingIntent; 34import android.app.Service; 35import android.content.BroadcastReceiver; 36import android.content.ContentValues; 37import android.content.Context; 38import android.content.Intent; 39import android.content.IntentFilter; 40import android.content.IntentFilter.MalformedMimeTypeException; 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; 47import android.widget.RemoteViews; 48 49import java.io.File; 50import java.util.HashMap; 51import java.util.TreeMap; 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 59 private static final long PRIORITY_NONE = -1; 60 @SuppressWarnings("unused") 61 private static final long PRIORITY_LOW = 0; 62 private static final long PRIORITY_NORMAL = 1; 63 private static final long PRIORITY_HIGH = 2; 64 65 // We can try various values here; I think 2 is completely reasonable as a first pass 66 private static final int MAX_SIMULTANEOUS_DOWNLOADS = 2; 67 // Limit on the number of simultaneous downloads per account 68 // Note that a limit of 1 is currently enforced by both Services (MailService and Controller) 69 private static final int MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT = 1; 70 71 private static AttachmentDownloadService sRunningService = null; 72 73 private AttachmentReceiver mAttachmentReceiver; 74 private Context mContext; 75 private final AttachmentMap mAttachmentMap = new AttachmentMap(); 76 private final HashMap<Long, Class<? extends Service>> mAccountServiceMap = 77 new HashMap<Long, Class<? extends Service>>(); 78 private final ServiceCallback mServiceCallback = new ServiceCallback(); 79 private final Object mLock = new Object(); 80 private volatile boolean mStop = false; 81 82 private static class DownloadRequest { 83 long attachmentId; 84 long messageId = -1; 85 long accountId; 86 boolean inProgress = false; 87 88 private DownloadRequest(Context context, Attachment attachment) { 89 attachmentId = attachment.mId; 90 Message msg = Message.restoreMessageWithId(context, attachment.mMessageKey); 91 if (msg != null) { 92 accountId = msg.mAccountKey; 93 messageId = msg.mId; 94 } 95 } 96 } 97 98 /** 99 * The AttachmentMap is a TreeMap sorted by "priority key", which is determined first by the 100 * priority class (e.g. low, high, etc.) and the time of the request. Higher priority requests 101 * are always processed first; among equals, the oldest request is processed first. The 102 * priority key represents this ordering. Note: All methods that change the attachment map are 103 * synchronized on the map itself 104 */ 105 private class AttachmentMap extends TreeMap<Long, DownloadRequest> { 106 private static final long serialVersionUID = 1L; 107 108 private final HashMap<Long, DownloadRequest> mDownloadsInProgress = 109 new HashMap<Long, DownloadRequest>(); 110 111 /** 112 * Calculate the download priority of an Attachment. A priority of zero means that the 113 * attachment is not marked for download. 114 * @param att the Attachment 115 * @return the priority key of the Attachment 116 */ 117 private long getPriorityKey(Attachment att) { 118 long priorityClass = PRIORITY_NONE; 119 int flags = att.mFlags; 120 if ((flags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 121 priorityClass = PRIORITY_NORMAL; 122 } else if ((flags & Attachment.FLAG_DOWNLOAD_USER_REQUEST) != 0) { 123 priorityClass = PRIORITY_HIGH; 124 } else { 125 return 0; 126 } 127 return (priorityKey(priorityClass, System.currentTimeMillis())); 128 } 129 130 /** 131 * onChange is called by the AttachmentReceiver upon receipt of a valid notification from 132 * EmailProvider that an attachment has been inserted or modified. It's not strictly 133 * necessary that we detect a deleted attachment, as the code always checks for the 134 * existence of an attachment before acting on it. 135 */ 136 public synchronized void onChange(Attachment att) { 137 long attKey = findPriorityKey(att.mId); 138 long priorityKey = getPriorityKey(att); 139 if (priorityKey == 0) { 140 if (Email.DEBUG) { 141 Log.d(TAG, "== Attachment changed: " + att.mId); 142 } 143 // In this case, there is no download priority for this attachment 144 if (attKey != 0) { 145 // If it exists in the map, remove it and try to stop any download in progress 146 // NOTE: We don't yet support deleting downloads in progress 147 if (Email.DEBUG) { 148 Log.d(TAG, "== Attachment " + att.mId + " was in queue, removing"); 149 } 150 remove(attKey, true); 151 } 152 } else { 153 // Ignore changes that occur during download 154 if (mDownloadsInProgress.containsKey(att.mId)) return; 155 // Remove from the map, but don't stop a download in progress 156 DownloadRequest req = remove(attKey, false); 157 if (req == null) { 158 req = new DownloadRequest(mContext, att); 159 } 160 // If the request already existed, we'll update the priority (so that the time is 161 // up-to-date); otherwise, we create a new request 162 if (Email.DEBUG) { 163 Log.d(TAG, "== Download queued for attachment " + att.mId + ", class " + 164 priorityClass(priorityKey) + ", priority time " + 165 priorityTime(priorityKey)); 166 } 167 // Store the request away 168 put(priorityKey, req); 169 } 170 // Process the queue if we're in a wait 171 kick(); 172 } 173 174 /** 175 * Remove a DownloadRequest from the queue, given its priority key 176 * @param priorityKey the priority key to remove (a Long) 177 * @param stopDownload whether we should try to stop an in-progress download of the 178 * attachment with this priority key (not implemented) 179 * @return the DownloadRequest for this priority key (or null, if none) 180 */ 181 public synchronized DownloadRequest remove(Object priorityKey, boolean stopDownload) { 182 DownloadRequest req = remove(priorityKey); 183 if ((req != null) && req.inProgress && stopDownload) { 184 // TODO: Stop download 185 if (Email.DEBUG) { 186 Log.d(TAG, "== Download of " + req.attachmentId + " stopped; NOT implemented"); 187 } 188 } 189 return req; 190 } 191 192 /** 193 * Find the priority key of a queued DownloadRequest, given the attachment's id 194 * @param id the id of the attachment 195 * @return the priority key for that attachment (or zero, if none) 196 */ 197 private synchronized long findPriorityKey(long id) { 198 for (Entry<Long, DownloadRequest> entry: entrySet()) { 199 if (entry.getValue().attachmentId == id) { 200 return entry.getKey(); 201 } 202 } 203 return 0; 204 } 205 206 /** 207 * Run through the AttachmentMap and find DownloadRequests that can be executed, enforcing 208 * the limit on maximum downloads 209 */ 210 private synchronized void processQueue() { 211 if (Email.DEBUG) { 212 Log.d(TAG, "== Checking attachment queue, " + mAttachmentMap.size() + " entries"); 213 } 214 Entry<Long, DownloadRequest> entry = mAttachmentMap.lastEntry(); 215 // First, start up any required downloads, in priority order 216 while ((entry != null) && (mDownloadsInProgress.size() < MAX_SIMULTANEOUS_DOWNLOADS)) { 217 DownloadRequest req = entry.getValue(); 218 if (!req.inProgress) { 219 mAttachmentMap.tryStartDownload(req); 220 } 221 entry = mAttachmentMap.lowerEntry(entry.getKey()); 222 } 223 // Then, try opportunistic download of appropriate attachments 224 int backgroundDownloads = MAX_SIMULTANEOUS_DOWNLOADS - mDownloadsInProgress.size(); 225 if (backgroundDownloads > 0) { 226 // TODO Code for background downloads here 227 if (Email.DEBUG) { 228 Log.d(TAG, "== We'd look for up to " + backgroundDownloads + 229 " background download(s) now..."); 230 } 231 } 232 } 233 234 /** 235 * Count the number of running downloads in progress for this account 236 * @param accountId the id of the account 237 * @return the count of running downloads 238 */ 239 private int downloadsForAccount(long accountId) { 240 int count = 0; 241 for (DownloadRequest req: mDownloadsInProgress.values()) { 242 if (req.accountId == accountId) { 243 count++; 244 } 245 } 246 return count; 247 } 248 249 /** 250 * Attempt to execute the DownloadRequest, enforcing the maximum downloads per account 251 * parameter 252 * @param req the DownloadRequest 253 * @return whether or not the download was started 254 */ 255 private synchronized boolean tryStartDownload(DownloadRequest req) { 256 // Enforce per-account limit 257 if (downloadsForAccount(req.accountId) >= MAX_SIMULTANEOUS_DOWNLOADS_PER_ACCOUNT) { 258 if (Email.DEBUG) { 259 Log.d(TAG, "== Skip #" + req.attachmentId + "; maxed for acct #" + 260 req.accountId); 261 } 262 return false; 263 } 264 Class<? extends Service> serviceClass = getServiceClassForAccount(req.accountId); 265 if (serviceClass == null) return false; 266 EmailServiceProxy proxy = 267 new EmailServiceProxy(mContext, serviceClass, mServiceCallback); 268 try { 269 File file = AttachmentProvider.getAttachmentFilename(mContext, req.accountId, 270 req.attachmentId); 271 if (Email.DEBUG) { 272 Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); 273 } 274 proxy.loadAttachment(req.attachmentId, file.getAbsolutePath(), 275 AttachmentProvider.getAttachmentUri(req.accountId, req.attachmentId) 276 .toString()); 277 mDownloadsInProgress.put(req.attachmentId, req); 278 req.inProgress = true; 279 } catch (RemoteException e) { 280 // TODO: Consider whether we need to do more in this case... 281 // For now, fix up our data to reflect the failure 282 mDownloadsInProgress.remove(req.attachmentId); 283 req.inProgress = false; 284 } 285 return true; 286 } 287 288 /** 289 * Called when a download is finished; we get notified of this via our EmailServiceCallback 290 * @param attachmentId the id of the attachment whose download is finished 291 * @param statusCode the EmailServiceStatus code returned by the Service 292 */ 293 private synchronized void endDownload(long attachmentId, int statusCode) { 294 // Say we're no longer downloading this 295 mDownloadsInProgress.remove(attachmentId); 296 long priorityKey = mAttachmentMap.findPriorityKey(attachmentId); 297 if (statusCode == EmailServiceStatus.CONNECTION_ERROR) { 298 // If this needs to be retried, just process the queue again 299 if (Email.DEBUG) { 300 Log.d(TAG, "== The download for attachment #" + attachmentId + 301 " will be retried"); 302 } 303 DownloadRequest req = mAttachmentMap.get(priorityKey); 304 if (req != null) { 305 req.inProgress = false; 306 } 307 kick(); 308 return; 309 } 310 DownloadRequest req = mAttachmentMap.remove(priorityKey); 311 if (Email.DEBUG) { 312 long secs = (priorityTime(System.currentTimeMillis()) - 313 priorityKeyToSystemTime(priorityKey)) / 1000; 314 String status = (statusCode == EmailServiceStatus.SUCCESS) ? "Success" : 315 "Error " + statusCode; 316 Log.d(TAG, "<< Download finished for attachment #" + attachmentId + "; " + secs + 317 " seconds from request, status: " + status); 318 } 319 Attachment attachment = Attachment.restoreAttachmentWithId(mContext, attachmentId); 320 if (attachment != null) { 321 boolean deleted = false; 322 if ((attachment.mFlags & Attachment.FLAG_DOWNLOAD_FORWARD) != 0) { 323 if (statusCode == EmailServiceStatus.ATTACHMENT_NOT_FOUND) { 324 // If this is a forwarding download, and the attachment doesn't exist (or 325 // can't be downloaded) delete it from the outgoing message, lest that 326 // message never get sent 327 EmailContent.delete(mContext, Attachment.CONTENT_URI, attachment.mId); 328 // TODO: Talk to UX about whether this is even worth doing 329 showDownloadForwardFailedNotification(attachment); 330 deleted = true; 331 } 332 // If we're an attachment on forwarded mail, and if we're not still blocked, 333 // try to send pending mail now (as mediated by MailService) 334 if (!Utility.hasUnloadedAttachments(mContext, attachment.mMessageKey)) { 335 if (Email.DEBUG) { 336 Log.d(TAG, "== Downloads finished for outgoing msg #" + req.messageId); 337 } 338 MailService.actionSendPendingMail(mContext, req.accountId); 339 } 340 } 341 if (!deleted) { 342 // Clear the download flags, since we're done for now. Note that this happens 343 // only for non-recoverable errors. When these occur for forwarded mail, we can 344 // ignore it and continue; otherwise, it was either 1) a user request, in which 345 // case the user can retry manually or 2) an opportunistic download, in which 346 // case the download wasn't critical 347 ContentValues cv = new ContentValues(); 348 int flags = 349 Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 350 cv.put(Attachment.FLAGS, attachment.mFlags &= ~flags); 351 attachment.update(mContext, cv); 352 } 353 } 354 // Process the queue 355 kick(); 356 } 357 } 358 359 private static int systemTimeToPriorityTime(long systemTime) { 360 return Integer.MAX_VALUE - priorityTime(systemTime); 361 } 362 363 private static long priorityKeyToSystemTime(long priorityKey) { 364 return Integer.MAX_VALUE - priorityTime(priorityKey); 365 } 366 367 private static int priorityTime(long priorityKeyOrSystemTime) { 368 return (int)(priorityKeyOrSystemTime & 0xFFFFFFFF); 369 } 370 371 private static long priorityClass(long priorityKey) { 372 return priorityKey >> 32; 373 } 374 375 private static long priorityKey(long priorityClass, long timeInMillis) { 376 // High 32 bits = priority class 377 // Low 32 bits = priority time 378 return (priorityClass<<32) | systemTimeToPriorityTime(timeInMillis); 379 } 380 381 private void kick() { 382 synchronized(mLock) { 383 mLock.notify(); 384 } 385 } 386 387 /** 388 * The AttachmentReceiver handles broadcasts from EmailProvider when an attachment is inserted 389 * or updated. Assuming that the attachment still exists, we just call the AttachmentMap's 390 * onChange() method 391 */ 392 public class AttachmentReceiver extends BroadcastReceiver { 393 @Override 394 public void onReceive(Context context, Intent intent) { 395 final Uri attachmentUri = intent.getData(); 396 String action = intent.getAction(); 397 if (action.equals(EmailProvider.ACTION_ATTACHMENT_UPDATED)) { 398 // We need to look at the flags and see if loading is appropriate 399 final int flags = 400 intent.getIntExtra(EmailProvider.ATTACHMENT_UPDATED_EXTRA_FLAGS, -1); 401 // If the flags didn't change, we're done 402 if (flags == -1) return; 403 new Thread(new Runnable() { 404 public void run() { 405 long id = Long.parseLong(attachmentUri.getLastPathSegment()); 406 Attachment attachment = 407 Attachment.restoreAttachmentWithId(AttachmentDownloadService.this, id); 408 if (attachment != null) { 409 // Store the flags we got from EmailProvider; given that all of this 410 // activity is asynchronous, we need to use the newest data from 411 // EmailProvider 412 attachment.mFlags = flags; 413 mAttachmentMap.onChange(attachment); 414 } 415 }}).run(); 416 } 417 } 418 } 419 420 /** 421 * We use an EmailServiceCallback to keep track of the progress of downloads. These callbacks 422 * come from either Controller (IMAP) or SyncManager (EAS). Note that we only implement the 423 * single callback that's defined by the EmailServiceCallback interface. 424 */ 425 private class ServiceCallback extends IEmailServiceCallback.Stub { 426 public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, 427 int progress) { 428 if (Email.DEBUG) { 429 String code; 430 switch(statusCode) { 431 case EmailServiceStatus.SUCCESS: 432 code = "Success"; 433 break; 434 case EmailServiceStatus.IN_PROGRESS: 435 code = "In progress"; 436 break; 437 default: 438 code = Integer.toString(statusCode); 439 } 440 Log.d(TAG, "loadAttachmentStatus, id = " + attachmentId + " code = "+ code + 441 ", " + progress + "%"); 442 } 443 // The only thing we're interested in here is whether the download is finished and, if 444 // so, what the result was. 445 switch (statusCode) { 446 case EmailServiceStatus.IN_PROGRESS: 447 break; 448 default: 449 mAttachmentMap.endDownload(attachmentId, statusCode); 450 break; 451 } 452 } 453 454 @Override 455 public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) 456 throws RemoteException { 457 } 458 459 @Override 460 public void syncMailboxListStatus(long accountId, int statusCode, int progress) 461 throws RemoteException { 462 } 463 464 @Override 465 public void syncMailboxStatus(long mailboxId, int statusCode, int progress) 466 throws RemoteException { 467 } 468 } 469 470 /** 471 * Alert the user that an attachment couldn't be forwarded. This is a very unusual case, and 472 * perhaps we shouldn't even send a notification. For now, it's helpful for debugging. 473 * Note the STOPSHIP below... 474 */ 475 void showDownloadForwardFailedNotification(Attachment att) { 476 // STOPSHIP: Tentative UI; if we use a notification, replace this text with a resource 477 RemoteViews contentView = new RemoteViews(getPackageName(), 478 R.layout.attachment_forward_failed_notification); 479 contentView.setImageViewResource(R.id.image, R.drawable.ic_email_attachment); 480 contentView.setTextViewText(R.id.text, 481 getString(R.string.forward_download_failed_notification, att.mFileName)); 482 Notification n = new Notification(R.drawable.stat_notify_email_generic, 483 getString(R.string.forward_download_failed_ticker), System.currentTimeMillis()); 484 n.contentView = contentView; 485 Intent i = new Intent(mContext, Welcome.class); 486 PendingIntent pending = PendingIntent.getActivity(mContext, 0, i, 487 PendingIntent.FLAG_UPDATE_CURRENT); 488 n.contentIntent = pending; 489 n.flags = Notification.FLAG_AUTO_CANCEL; 490 NotificationManager nm = 491 (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); 492 nm.notify(MailService.NOTIFICATION_ID_WARNING, n); 493 } 494 495 /** 496 * Return the class of the service used by the account type of the provided account id. We 497 * cache the results to avoid repeated database access 498 * @param accountId the id of the account 499 * @return the service class for the account 500 */ 501 private synchronized Class<? extends Service> getServiceClassForAccount(long accountId) { 502 // TODO: We should have some more data-driven way of determining the service class. I'd 503 // suggest adding an attribute in the stores.xml file 504 Class<? extends Service> serviceClass = mAccountServiceMap.get(accountId); 505 if (serviceClass == null) { 506 String protocol = Account.getProtocol(mContext, accountId); 507 if (protocol.equals("eas")) { 508 serviceClass = SyncManager.class; 509 } else { 510 serviceClass = com.android.email.Controller.class; 511 } 512 mAccountServiceMap.put(accountId, serviceClass); 513 } 514 return serviceClass; 515 } 516 517 private void onChange(Attachment att) { 518 mAttachmentMap.onChange(att); 519 } 520 521 private boolean isQueued(long attachmentId) { 522 return mAttachmentMap.findPriorityKey(attachmentId) != 0; 523 } 524 525 private boolean dequeue(long attachmentId) { 526 long priority = mAttachmentMap.findPriorityKey(attachmentId); 527 if (priority == 0) return false; 528 DownloadRequest req = mAttachmentMap.remove(priority); 529 return req != null; 530 } 531 532 /** 533 * Ask the service whether a particular attachment is queued for download 534 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 535 * @return whether or not the attachment is queued for download 536 */ 537 public static boolean isAttachmentQueued(long attachmentId) { 538 if (sRunningService != null) { 539 return sRunningService.isQueued(attachmentId); 540 } 541 return false; 542 } 543 544 /** 545 * Ask the service to remove an attachment from the download queue 546 * @param attachmentId the id of the Attachment (as stored by EmailProvider) 547 * @return whether or not the attachment was removed from the queue 548 */ 549 public static boolean cancelQueuedAttachment(long attachmentId) { 550 if (sRunningService != null) { 551 return sRunningService.dequeue(attachmentId); 552 } 553 return false; 554 } 555 556 /** 557 * Called directly by EmailProvider whenever an attachment is inserted or changed 558 * @param id the attachment's id 559 * @param flags the new flags for the attachment 560 */ 561 public static void attachmentChanged(final long id, final int flags) { 562 Utility.runAsync(new Runnable() { 563 public void run() { 564 final Attachment attachment = 565 Attachment.restoreAttachmentWithId(sRunningService, id); 566 if (attachment != null) { 567 // Store the flags we got from EmailProvider; given that all of this 568 // activity is asynchronous, we need to use the newest data from 569 // EmailProvider 570 attachment.mFlags = flags; 571 sRunningService.onChange(attachment); 572 } 573 }}); 574 } 575 576 public void run() { 577 mContext = this; 578 // Set up our receiver for EmailProvider attachment updates 579 mAttachmentReceiver = new AttachmentReceiver(); 580 try { 581 getApplicationContext().registerReceiver(mAttachmentReceiver, 582 new IntentFilter(EmailProvider.ACTION_ATTACHMENT_UPDATED, 583 EmailProvider.EMAIL_ATTACHMENT_MIME_TYPE)); 584 } catch (MalformedMimeTypeException e1) { 585 // Since we're passing in a constant mime type, this can't happen 586 } 587 588 // Run through all attachments in the database that require download and add them to 589 // the queue 590 int mask = Attachment.FLAG_DOWNLOAD_FORWARD | Attachment.FLAG_DOWNLOAD_USER_REQUEST; 591 Cursor c = getContentResolver().query(Attachment.CONTENT_URI, 592 EmailContent.ID_PROJECTION, "(" + Attachment.FLAGS + " & ?) != 0", 593 new String[] {Integer.toString(mask)}, null); 594 try { 595 Log.d(TAG, "Count: " + c.getCount()); 596 while (c.moveToNext()) { 597 Attachment attachment = Attachment.restoreAttachmentWithId( 598 this, c.getLong(EmailContent.ID_PROJECTION_COLUMN)); 599 if (attachment != null) { 600 mAttachmentMap.onChange(attachment); 601 } 602 } 603 } catch (Exception e) { 604 e.printStackTrace(); 605 } 606 finally { 607 c.close(); 608 } 609 610 // Loop until stopped, with a 30 minute wait loop 611 while (!mStop) { 612 // Here's where we run our attachment loading logic... 613 mAttachmentMap.processQueue(); 614 synchronized(mLock) { 615 try { 616 mLock.wait(PROCESS_QUEUE_WAIT_TIME); 617 } catch (InterruptedException e) { 618 // That's ok; we'll just keep looping 619 } 620 } 621 } 622 } 623 624 @Override 625 public int onStartCommand(Intent intent, int flags, int startId) { 626 sRunningService = this; 627 return Service.START_STICKY; 628 } 629 630 /** 631 * The lifecycle of this service is managed by Email.setServicesEnabled(), which is called 632 * throughout the code, in particular 1) after boot and 2) after accounts are added or removed 633 * The goal is that this service should be running at all times when there's at least one 634 * email account present. 635 */ 636 @Override 637 public void onCreate() { 638 // Start up our service thread 639 new Thread(this, "AttachmentDownloadService").start(); 640 } 641 @Override 642 public IBinder onBind(Intent intent) { 643 return null; 644 } 645 646 @Override 647 public void onDestroy() { 648 Log.d(TAG, "**** ON DESTROY!"); 649 if (sRunningService != null) { 650 mStop = true; 651 kick(); 652 } 653 sRunningService = null; 654 } 655} 656