• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 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 
17 package com.android.email.service;
18 
19 import com.android.email.AccountBackupRestore;
20 import com.android.email.Controller;
21 import com.android.email.Email;
22 import com.android.email.R;
23 import com.android.email.activity.MessageList;
24 import com.android.email.mail.MessagingException;
25 import com.android.email.provider.EmailContent.Account;
26 import com.android.email.provider.EmailContent.AccountColumns;
27 import com.android.email.provider.EmailContent.HostAuth;
28 import com.android.email.provider.EmailContent.Mailbox;
29 
30 import android.app.AlarmManager;
31 import android.app.Notification;
32 import android.app.NotificationManager;
33 import android.app.PendingIntent;
34 import android.app.Service;
35 import android.content.ContentUris;
36 import android.content.ContentValues;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.database.Cursor;
40 import android.media.AudioManager;
41 import android.net.Uri;
42 import android.os.IBinder;
43 import android.os.SystemClock;
44 import android.util.Config;
45 import android.util.Log;
46 
47 import java.util.HashMap;
48 
49 /**
50  * Background service for refreshing non-push email accounts.
51  */
52 public class MailService extends Service {
53     /** DO NOT CHECK IN "TRUE" */
54     private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;     // force 1-minute refresh
55 
56     private static final String LOG_TAG = "Email-MailService";
57 
58     public static final int NOTIFICATION_ID_NEW_MESSAGES = 1;
59     public static final int NOTIFICATION_ID_SECURITY_NEEDED = 2;
60     public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 3;
61 
62     private static final String ACTION_CHECK_MAIL =
63         "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
64     private static final String ACTION_RESCHEDULE =
65         "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
66     private static final String ACTION_CANCEL =
67         "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
68     private static final String ACTION_NOTIFY_MAIL =
69         "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
70 
71     private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
72     private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
73     private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG";
74 
75     private static final int WATCHDOG_DELAY = 10 * 60 * 1000;   // 10 minutes
76 
77     private static final String[] NEW_MESSAGE_COUNT_PROJECTION =
78         new String[] {AccountColumns.NEW_MESSAGE_COUNT};
79 
80     private final Controller.Result mControllerCallback = new ControllerResults();
81 
82     private int mStartId;
83 
84     /**
85      * Access must be synchronized, because there are accesses from the Controller callback
86      */
87     private static HashMap<Long,AccountSyncReport> mSyncReports =
88         new HashMap<Long,AccountSyncReport>();
89 
90     /**
91      * Simple template used for clearing new message count in accounts
92      */
93     private static final ContentValues CLEAR_NEW_MESSAGES;
94     static {
95         CLEAR_NEW_MESSAGES = new ContentValues();
CLEAR_NEW_MESSAGES.put(Account.NEW_MESSAGE_COUNT, 0)96         CLEAR_NEW_MESSAGES.put(Account.NEW_MESSAGE_COUNT, 0);
97     }
98 
actionReschedule(Context context)99     public static void actionReschedule(Context context) {
100         Intent i = new Intent();
101         i.setClass(context, MailService.class);
102         i.setAction(MailService.ACTION_RESCHEDULE);
103         context.startService(i);
104     }
105 
actionCancel(Context context)106     public static void actionCancel(Context context)  {
107         Intent i = new Intent();
108         i.setClass(context, MailService.class);
109         i.setAction(MailService.ACTION_CANCEL);
110         context.startService(i);
111     }
112 
113     /**
114      * Reset new message counts for one or all accounts.  This clears both our local copy and
115      * the values (if any) stored in the account records.
116      *
117      * @param accountId account to clear, or -1 for all accounts
118      */
resetNewMessageCount(Context context, long accountId)119     public static void resetNewMessageCount(Context context, long accountId) {
120         synchronized (mSyncReports) {
121             for (AccountSyncReport report : mSyncReports.values()) {
122                 if (accountId == -1 || accountId == report.accountId) {
123                     report.numNewMessages = 0;
124                 }
125             }
126         }
127         // now do the database - all accounts, or just one of them
128         Uri uri;
129         if (accountId == -1) {
130             uri = Account.CONTENT_URI;
131         } else {
132             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
133         }
134         context.getContentResolver().update(uri, CLEAR_NEW_MESSAGES, null, null);
135     }
136 
137     /**
138      * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
139      * messages.  This assumes that the push provider has already synced the messages into the
140      * appropriate database - this simply triggers the notification mechanism.
141      *
142      * @param context a context
143      * @param accountId the id of the account that is reporting new messages
144      * @param newCount the number of new messages
145      */
actionNotifyNewMessages(Context context, long accountId)146     public static void actionNotifyNewMessages(Context context, long accountId) {
147         Intent i = new Intent(ACTION_NOTIFY_MAIL);
148         i.setClass(context, MailService.class);
149         i.putExtra(EXTRA_CHECK_ACCOUNT, accountId);
150         context.startService(i);
151     }
152 
153     @Override
onStartCommand(Intent intent, int flags, int startId)154     public int onStartCommand(Intent intent, int flags, int startId) {
155         super.onStartCommand(intent, flags, startId);
156 
157         // Restore accounts, if it has not happened already
158         AccountBackupRestore.restoreAccountsIfNeeded(this);
159 
160         // TODO this needs to be passed through the controller and back to us
161         this.mStartId = startId;
162         String action = intent.getAction();
163 
164         Controller controller = Controller.getInstance(getApplication());
165         controller.addResultCallback(mControllerCallback);
166 
167         if (ACTION_CHECK_MAIL.equals(action)) {
168             // If we have the data, restore the last-sync-times for each account
169             // These are cached in the wakeup intent in case the process was killed.
170             restoreSyncReports(intent);
171 
172             // Sync a specific account if given
173             AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
174             long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
175             if (Config.LOGD && Email.DEBUG) {
176                 Log.d(LOG_TAG, "action: check mail for id=" + checkAccountId);
177             }
178             if (checkAccountId >= 0) {
179                 setWatchdog(checkAccountId, alarmManager);
180             }
181             // if no account given, or the given account cannot be synced - reschedule
182             if (checkAccountId == -1 || !syncOneAccount(controller, checkAccountId, startId)) {
183                 // Prevent runaway on the current account by pretending it updated
184                 if (checkAccountId != -1) {
185                     updateAccountReport(checkAccountId, 0);
186                 }
187                 // Find next account to sync, and reschedule
188                 reschedule(alarmManager);
189                 stopSelf(startId);
190             }
191         }
192         else if (ACTION_CANCEL.equals(action)) {
193             if (Config.LOGD && Email.DEBUG) {
194                 Log.d(LOG_TAG, "action: cancel");
195             }
196             cancel();
197             stopSelf(startId);
198         }
199         else if (ACTION_RESCHEDULE.equals(action)) {
200             if (Config.LOGD && Email.DEBUG) {
201                 Log.d(LOG_TAG, "action: reschedule");
202             }
203             // As a precaution, clear any outstanding Email notifications
204             // We could be smarter and only do this when the list of accounts changes,
205             // but that's an edge condition and this is much safer.
206             NotificationManager notificationManager =
207                 (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
208             notificationManager.cancel(NOTIFICATION_ID_NEW_MESSAGES);
209 
210             // When called externally, we refresh the sync reports table to pick up
211             // any changes in the account list or account settings
212             refreshSyncReports();
213             // Finally, scan for the next needing update, and set an alarm for it
214             AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
215             reschedule(alarmManager);
216             stopSelf(startId);
217         } else if (ACTION_NOTIFY_MAIL.equals(action)) {
218             long accountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
219             // Get the current new message count
220             Cursor c = getContentResolver().query(
221                     ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
222                     NEW_MESSAGE_COUNT_PROJECTION, null, null, null);
223             int newMessageCount = 0;
224             try {
225                 if (c.moveToFirst()) {
226                     newMessageCount = c.getInt(0);
227                 } else {
228                     // If the account no longer exists, set to -1 (which is handled below)
229                     accountId = -1;
230                 }
231             } finally {
232                 c.close();
233             }
234             if (Config.LOGD && Email.DEBUG) {
235                 Log.d(LOG_TAG, "notify accountId=" + Long.toString(accountId)
236                         + " count=" + newMessageCount);
237             }
238             if (accountId != -1) {
239                 updateAccountReport(accountId, newMessageCount);
240                 notifyNewMessages(accountId);
241             }
242             stopSelf(startId);
243         }
244 
245         // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory
246         // pressure, there will be no explicit restart.  This is OK;  Note that we set a watchdog
247         // alarm before each mailbox check.  If the mailbox check never completes, the watchdog
248         // will fire and get things running again.
249         return START_NOT_STICKY;
250     }
251 
252     @Override
onBind(Intent intent)253     public IBinder onBind(Intent intent) {
254         return null;
255     }
256 
257     @Override
onDestroy()258     public void onDestroy() {
259         super.onDestroy();
260         Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
261     }
262 
cancel()263     private void cancel() {
264         AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
265         PendingIntent pi = createAlarmIntent(-1, null, false);
266         alarmMgr.cancel(pi);
267     }
268 
269     /**
270      * Refresh the sync reports, to pick up any changes in the account list or account settings.
271      */
refreshSyncReports()272     private void refreshSyncReports() {
273         synchronized (mSyncReports) {
274             // Make shallow copy of sync reports so we can recover the prev sync times
275             HashMap<Long,AccountSyncReport> oldSyncReports =
276                 new HashMap<Long,AccountSyncReport>(mSyncReports);
277 
278             // Delete the sync reports to force a refresh from live account db data
279             mSyncReports.clear();
280             setupSyncReportsLocked(-1);
281 
282             // Restore prev-sync & next-sync times for any reports in the new list
283             for (AccountSyncReport newReport : mSyncReports.values()) {
284                 AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId);
285                 if (oldReport != null) {
286                     newReport.prevSyncTime = oldReport.prevSyncTime;
287                     if (newReport.syncInterval > 0 && newReport.prevSyncTime != 0) {
288                         newReport.nextSyncTime =
289                             newReport.prevSyncTime + (newReport.syncInterval * 1000 * 60);
290                     }
291                 }
292             }
293         }
294     }
295 
296     /**
297      * Create and send an alarm with the entire list.  This also sends a list of known last-sync
298      * times with the alarm, so if we are killed between alarms, we don't lose this info.
299      *
300      * @param alarmMgr passed in so we can mock for testing.
301      */
reschedule(AlarmManager alarmMgr)302     /* package */ void reschedule(AlarmManager alarmMgr) {
303         // restore the reports if lost
304         setupSyncReports(-1);
305         synchronized (mSyncReports) {
306             int numAccounts = mSyncReports.size();
307             long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
308             int accountInfoIndex = 0;
309 
310             long nextCheckTime = Long.MAX_VALUE;
311             AccountSyncReport nextAccount = null;
312             long timeNow = SystemClock.elapsedRealtime();
313 
314             for (AccountSyncReport report : mSyncReports.values()) {
315                 if (report.syncInterval <= 0) {                         // no timed checks - skip
316                     continue;
317                 }
318                 if ("eas".equals(report.protocol)) {                    // no checks for eas accts
319                     continue;
320                 }
321                 long prevSyncTime = report.prevSyncTime;
322                 long nextSyncTime = report.nextSyncTime;
323 
324                 // select next account to sync
325                 if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) {  // never checked, or overdue
326                     nextCheckTime = 0;
327                     nextAccount = report;
328                 } else if (nextSyncTime < nextCheckTime) {              // next to be checked
329                     nextCheckTime = nextSyncTime;
330                     nextAccount = report;
331                 }
332                 // collect last-sync-times for all accounts
333                 // this is using pairs of {long,long} to simplify passing in a bundle
334                 accountInfo[accountInfoIndex++] = report.accountId;
335                 accountInfo[accountInfoIndex++] = report.prevSyncTime;
336             }
337 
338             // Clear out any unused elements in the array
339             while (accountInfoIndex < accountInfo.length) {
340                 accountInfo[accountInfoIndex++] = -1;
341             }
342 
343             // set/clear alarm as needed
344             long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
345             PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false);
346 
347             if (nextAccount == null) {
348                 alarmMgr.cancel(pi);
349                 if (Config.LOGD && Email.DEBUG) {
350                     Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check");
351                 }
352             } else {
353                 alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
354                 if (Config.LOGD && Email.DEBUG) {
355                     Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime
356                             + " for " + nextAccount);
357                 }
358             }
359         }
360     }
361 
362     /**
363      * Create a watchdog alarm and set it.  This is used in case a mail check fails (e.g. we are
364      * killed by the system due to memory pressure.)  Normally, a mail check will complete and
365      * the watchdog will be replaced by the call to reschedule().
366      * @param accountId the account we were trying to check
367      * @param alarmMgr system alarm manager
368      */
setWatchdog(long accountId, AlarmManager alarmMgr)369     private void setWatchdog(long accountId, AlarmManager alarmMgr) {
370         PendingIntent pi = createAlarmIntent(accountId, null, true);
371         long timeNow = SystemClock.elapsedRealtime();
372         long nextCheckTime = timeNow + WATCHDOG_DELAY;
373         alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
374     }
375 
376     /**
377      * Return a pending intent for use by this alarm.  Most of the fields must be the same
378      * (in order for the intent to be recognized by the alarm manager) but the extras can
379      * be different, and are passed in here as parameters.
380      */
createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog)381     /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo,
382             boolean isWatchdog) {
383         Intent i = new Intent();
384         i.setClass(this, MailService.class);
385         i.setAction(ACTION_CHECK_MAIL);
386         i.putExtra(EXTRA_CHECK_ACCOUNT, checkId);
387         i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
388         if (isWatchdog) {
389             i.putExtra(EXTRA_DEBUG_WATCHDOG, true);
390         }
391         PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT);
392         return pi;
393     }
394 
395     /**
396      * Start a controller sync for a specific account
397      *
398      * @param controller The controller to do the sync work
399      * @param checkAccountId the account Id to try and check
400      * @param startId the id of this service launch
401      * @return true if mail checking has started, false if it could not (e.g. bad account id)
402      */
syncOneAccount(Controller controller, long checkAccountId, int startId)403     private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) {
404         long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
405         if (inboxId == Mailbox.NO_MAILBOX) {
406             return false;
407         } else {
408             controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback);
409             return true;
410         }
411     }
412 
413     /**
414      * Note:  Times are relative to SystemClock.elapsedRealtime()
415      */
416     private static class AccountSyncReport {
417         long accountId;
418         String protocol;
419         long prevSyncTime;      // 0 == unknown
420         long nextSyncTime;      // 0 == ASAP  -1 == don't sync
421         int numNewMessages;
422 
423         int syncInterval;
424         boolean notify;
425         boolean vibrate;
426         boolean vibrateWhenSilent;
427         Uri ringtoneUri;
428 
429         String displayName;     // temporary, for debug logging
430 
431 
432         @Override
toString()433         public String toString() {
434             return displayName + ": id=" + accountId + " prevSync=" + prevSyncTime
435                     + " nextSync=" + nextSyncTime + " numNew=" + numNewMessages;
436         }
437     }
438 
439     /**
440      * scan accounts to create a list of { acct, prev sync, next sync, #new }
441      * use this to create a fresh copy.  assumes all accounts need sync
442      *
443      * @param accountId -1 will rebuild the list if empty.  other values will force loading
444      *   of a single account (e.g if it was created after the original list population)
445      */
setupSyncReports(long accountId)446     /* package */ void setupSyncReports(long accountId) {
447         synchronized (mSyncReports) {
448             setupSyncReportsLocked(accountId);
449         }
450     }
451 
452     /**
453      * Handle the work of setupSyncReports.  Must be synchronized on mSyncReports.
454      */
setupSyncReportsLocked(long accountId)455     private void setupSyncReportsLocked(long accountId) {
456         if (accountId == -1) {
457             // -1 == reload the list if empty, otherwise exit immediately
458             if (mSyncReports.size() > 0) {
459                 return;
460             }
461         } else {
462             // load a single account if it doesn't already have a sync record
463             if (mSyncReports.containsKey(accountId)) {
464                 return;
465             }
466         }
467 
468         // setup to add a single account or all accounts
469         Uri uri;
470         if (accountId == -1) {
471             uri = Account.CONTENT_URI;
472         } else {
473             uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
474         }
475 
476         // TODO use a narrower projection here
477         Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION,
478                 null, null, null);
479         try {
480             while (c.moveToNext()) {
481                 AccountSyncReport report = new AccountSyncReport();
482                 int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN);
483                 int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
484                 String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN);
485 
486                 // For debugging only
487                 if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) {
488                     syncInterval = 1;
489                 }
490 
491                 long acctId = c.getLong(Account.CONTENT_ID_COLUMN);
492                 Account account = Account.restoreAccountWithId(this, acctId);
493                 if (account == null) continue;
494                 HostAuth hostAuth = HostAuth.restoreHostAuthWithId(this, account.mHostAuthKeyRecv);
495                 if (hostAuth == null) continue;
496                 report.accountId = acctId;
497                 report.protocol = hostAuth.mProtocol;
498                 report.prevSyncTime = 0;
499                 report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
500                 report.numNewMessages = 0;
501 
502                 report.syncInterval = syncInterval;
503                 report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
504                 report.vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
505                 report.vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
506                 report.ringtoneUri = (ringtoneString == null) ? null
507                         : Uri.parse(ringtoneString);
508 
509                 report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
510 
511                 // TODO lookup # new in inbox
512                 mSyncReports.put(report.accountId, report);
513             }
514         } finally {
515             c.close();
516         }
517     }
518 
519     /**
520      * Update list with a single account's sync times and unread count
521      *
522      * @param accountId the account being updated
523      * @param newCount the number of new messages, or -1 if not being reported (don't update)
524      * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
525      */
updateAccountReport(long accountId, int newCount)526     /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
527         // restore the reports if lost
528         setupSyncReports(accountId);
529         synchronized (mSyncReports) {
530             AccountSyncReport report = mSyncReports.get(accountId);
531             if (report == null) {
532                 // discard result - there is no longer an account with this id
533                 Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId));
534                 return null;
535             }
536 
537             // report found - update it (note - editing the report while in-place in the hashmap)
538             report.prevSyncTime = SystemClock.elapsedRealtime();
539             if (report.syncInterval > 0) {
540                 report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
541             }
542             if (newCount != -1) {
543                 report.numNewMessages = newCount;
544             }
545             if (Config.LOGD && Email.DEBUG) {
546                 Log.d(LOG_TAG, "update account " + report.toString());
547             }
548             return report;
549         }
550     }
551 
552     /**
553      * when we receive an alarm, update the account sync reports list if necessary
554      * this will be the case when if we have restarted the process and lost the data
555      * in the global.
556      *
557      * @param restoreIntent the intent with the list
558      */
restoreSyncReports(Intent restoreIntent)559     /* package */ void restoreSyncReports(Intent restoreIntent) {
560         // restore the reports if lost
561         setupSyncReports(-1);
562         synchronized (mSyncReports) {
563             long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
564             if (accountInfo == null) {
565                 Log.d(LOG_TAG, "no data in intent to restore");
566                 return;
567             }
568             int accountInfoIndex = 0;
569             int accountInfoLimit = accountInfo.length;
570             while (accountInfoIndex < accountInfoLimit) {
571                 long accountId = accountInfo[accountInfoIndex++];
572                 long prevSync = accountInfo[accountInfoIndex++];
573                 AccountSyncReport report = mSyncReports.get(accountId);
574                 if (report != null) {
575                     if (report.prevSyncTime == 0) {
576                         report.prevSyncTime = prevSync;
577                         if (report.syncInterval > 0 && report.prevSyncTime != 0) {
578                             report.nextSyncTime =
579                                 report.prevSyncTime + (report.syncInterval * 1000 * 60);
580                         }
581                     }
582                 }
583             }
584         }
585     }
586 
587     class ControllerResults implements Controller.Result {
588 
loadMessageForViewCallback(MessagingException result, long messageId, int progress)589         public void loadMessageForViewCallback(MessagingException result, long messageId,
590                 int progress) {
591         }
592 
loadAttachmentCallback(MessagingException result, long messageId, long attachmentId, int progress)593         public void loadAttachmentCallback(MessagingException result, long messageId,
594                 long attachmentId, int progress) {
595         }
596 
updateMailboxCallback(MessagingException result, long accountId, long mailboxId, int progress, int numNewMessages)597         public void updateMailboxCallback(MessagingException result, long accountId,
598                 long mailboxId, int progress, int numNewMessages) {
599             if (result != null || progress == 100) {
600                 // We only track the inbox here in the service - ignore other mailboxes
601                 long inboxId = Mailbox.findMailboxOfType(MailService.this,
602                         accountId, Mailbox.TYPE_INBOX);
603                 if (mailboxId == inboxId) {
604                     if (progress == 100) {
605                         updateAccountReport(accountId, numNewMessages);
606                         if (numNewMessages > 0) {
607                             notifyNewMessages(accountId);
608                         }
609                     } else {
610                         updateAccountReport(accountId, -1);
611                     }
612                 }
613                 // Call the global refresh tracker for all mailboxes
614                 Email.updateMailboxRefreshTime(mailboxId);
615             }
616         }
617 
updateMailboxListCallback(MessagingException result, long accountId, int progress)618         public void updateMailboxListCallback(MessagingException result, long accountId,
619                 int progress) {
620         }
621 
serviceCheckMailCallback(MessagingException result, long accountId, long mailboxId, int progress, long tag)622         public void serviceCheckMailCallback(MessagingException result, long accountId,
623                 long mailboxId, int progress, long tag) {
624             if (result != null || progress == 100) {
625                 if (result != null) {
626                     // the checkmail ended in an error.  force an update of the refresh
627                     // time, so we don't just spin on this account
628                     updateAccountReport(accountId, -1);
629                 }
630                 AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
631                 reschedule(alarmManager);
632                 int serviceId = MailService.this.mStartId;
633                 if (tag != 0) {
634                     serviceId = (int) tag;
635                 }
636                 stopSelf(serviceId);
637             }
638         }
639 
sendMailCallback(MessagingException result, long accountId, long messageId, int progress)640         public void sendMailCallback(MessagingException result, long accountId, long messageId,
641                 int progress) {
642         }
643     }
644 
645     /**
646      * Prepare notifications for a given new account having received mail
647      * The notification is organized around the account that has the new mail (e.g. selecting
648      * the alert preferences) but the notification will include a summary if other
649      * accounts also have new mail.
650      */
notifyNewMessages(long accountId)651     private void notifyNewMessages(long accountId) {
652         boolean notify = false;
653         boolean vibrate = false;
654         boolean vibrateWhenSilent = false;
655         Uri ringtone = null;
656         int accountsWithNewMessages = 0;
657         int numNewMessages = 0;
658         String reportName = null;
659         synchronized (mSyncReports) {
660             for (AccountSyncReport report : mSyncReports.values()) {
661                 if (report.numNewMessages == 0) {
662                     continue;
663                 }
664                 numNewMessages += report.numNewMessages;
665                 accountsWithNewMessages += 1;
666                 if (report.accountId == accountId) {
667                     notify = report.notify;
668                     vibrate = report.vibrate;
669                     vibrateWhenSilent = report.vibrateWhenSilent;
670                     ringtone = report.ringtoneUri;
671                     reportName = report.displayName;
672                 }
673             }
674         }
675         if (!notify) {
676             return;
677         }
678 
679         // set up to post a notification
680         Intent intent;
681         String reportString;
682 
683         if (accountsWithNewMessages == 1) {
684             // Prepare a report for a single account
685             // "12 unread (gmail)"
686             reportString = getResources().getQuantityString(
687                     R.plurals.notification_new_one_account_fmt, numNewMessages,
688                     numNewMessages, reportName);
689             intent = MessageList.createIntent(this, accountId, -1, Mailbox.TYPE_INBOX);
690         } else {
691             // Prepare a report for multiple accounts
692             // "4 accounts"
693             reportString = getResources().getQuantityString(
694                     R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages,
695                     accountsWithNewMessages);
696             intent = MessageList.createIntent(this, -1, Mailbox.QUERY_ALL_INBOXES, -1);
697         }
698 
699         // prepare appropriate pending intent, set up notification, and send
700         PendingIntent pending =
701             PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
702 
703         Notification notification = new Notification(
704                 R.drawable.stat_notify_email_generic,
705                 getString(R.string.notification_new_title),
706                 System.currentTimeMillis());
707         notification.setLatestEventInfo(this,
708                 getString(R.string.notification_new_title),
709                 reportString,
710                 pending);
711 
712         notification.sound = ringtone;
713         AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
714         boolean nowSilent = audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
715 
716         // Use same code here as in Gmail and GTalk for vibration
717         if (vibrate || (vibrateWhenSilent && nowSilent)) {
718             notification.defaults |= Notification.DEFAULT_VIBRATE;
719         }
720 
721         // This code is identical to that used by Gmail and GTalk for notifications
722         notification.flags |= Notification.FLAG_SHOW_LIGHTS;
723         notification.defaults |= Notification.DEFAULT_LIGHTS;
724 
725         NotificationManager notificationManager =
726             (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
727         notificationManager.notify(NOTIFICATION_ID_NEW_MESSAGES, notification);
728     }
729 }
730