MailService.java revision cbe513d4305893edde60aca645b290f31f7643da
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
17package com.android.email.service;
18
19import com.android.email.Controller;
20import com.android.email.Email;
21import com.android.email.R;
22import com.android.email.activity.MessageList;
23import com.android.email.mail.MessagingException;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.Mailbox;
26
27import android.app.AlarmManager;
28import android.app.Notification;
29import android.app.NotificationManager;
30import android.app.PendingIntent;
31import android.app.Service;
32import android.content.ContentUris;
33import android.content.ContentValues;
34import android.content.Context;
35import android.content.Intent;
36import android.database.Cursor;
37import android.net.Uri;
38import android.os.IBinder;
39import android.os.SystemClock;
40import android.util.Config;
41import android.util.Log;
42
43import java.util.HashMap;
44
45/**
46 * Background service for refreshing non-push email accounts.
47 */
48public class MailService extends Service {
49    /** DO NOT CHECK IN "TRUE" */
50    private static final boolean DEBUG_FORCE_QUICK_REFRESH = false;        // force 1-minute refresh
51
52    public static int NEW_MESSAGE_NOTIFICATION_ID = 1;
53
54    private static final String ACTION_CHECK_MAIL =
55        "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
56    private static final String ACTION_RESCHEDULE =
57        "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
58    private static final String ACTION_CANCEL =
59        "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
60    private static final String ACTION_NOTIFY_MAIL =
61        "com.android.email.intent.action.MAIL_SERVICE_NOTIFY";
62
63    private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
64    private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO";
65    private static final String EXTRA_MESSAGE_COUNT = "com.android.email.intent.extra.COUNT";
66
67    private Controller.Result mControllerCallback = new ControllerResults();
68
69    private int mStartId;
70
71    /**
72     * Access must be synchronized, because there are accesses from the Controller callback
73     */
74    private static HashMap<Long,AccountSyncReport> mSyncReports =
75        new HashMap<Long,AccountSyncReport>();
76
77    /**
78     * Simple template used for clearing new message count in accounts
79     */
80    static ContentValues mClearNewMessages;
81    static {
82        mClearNewMessages = new ContentValues();
83        mClearNewMessages.put(Account.NEW_MESSAGE_COUNT, 0);
84    }
85
86    public static void actionReschedule(Context context) {
87        Intent i = new Intent();
88        i.setClass(context, MailService.class);
89        i.setAction(MailService.ACTION_RESCHEDULE);
90        context.startService(i);
91    }
92
93    public static void actionCancel(Context context)  {
94        Intent i = new Intent();
95        i.setClass(context, MailService.class);
96        i.setAction(MailService.ACTION_CANCEL);
97        context.startService(i);
98    }
99
100    /**
101     * Reset new message counts for one or all accounts.  This clears both our local copy and
102     * the values (if any) stored in the account records.
103     *
104     * @param accountId account to clear, or -1 for all accounts
105     */
106    public static void resetNewMessageCount(Context context, long accountId) {
107        synchronized (mSyncReports) {
108            for (AccountSyncReport report : mSyncReports.values()) {
109                if (accountId == -1 || accountId == report.accountId) {
110                    report.numNewMessages = 0;
111                }
112            }
113        }
114        // now do the database - all accounts, or just one of them
115        Uri uri;
116        if (accountId == -1) {
117            uri = Account.CONTENT_URI;
118        } else {
119            uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
120        }
121        context.getContentResolver().update(uri, mClearNewMessages, null, null);
122    }
123
124    /**
125     * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
126     * messages.  This assumes that the push provider has already synced the messages into the
127     * appropriate database - this simply triggers the notification mechanism.
128     *
129     * @param context a context
130     * @param accountId the id of the account that is reporting new messages
131     * @param newCount the number of new messages
132     */
133    public static void actionNotifyNewMessages(Context context, long accountId, int newCount) {
134        Intent i = new Intent(ACTION_NOTIFY_MAIL);
135        i.setClass(context, MailService.class);
136        i.putExtra(EXTRA_CHECK_ACCOUNT, accountId);
137        i.putExtra(EXTRA_MESSAGE_COUNT, newCount);
138        context.startService(i);
139    }
140
141    @Override
142    public void onStart(Intent intent, int startId) {
143        super.onStart(intent, startId);
144
145        // TODO this needs to be passed through the controller and back to us
146        this.mStartId = startId;
147        String action = intent.getAction();
148
149        Controller controller = Controller.getInstance(getApplication());
150        controller.addResultCallback(mControllerCallback);
151
152        if (ACTION_CHECK_MAIL.equals(action)) {
153            if (Config.LOGD && Email.DEBUG) {
154                Log.d(Email.LOG_TAG, "*** MailService: checking mail");
155            }
156            // If we have the data, restore the last-sync-times for each account
157            // These are cached in the wakeup intent in case the process was killed.
158            restoreSyncReports(intent);
159
160            // Sync a specific account if given
161            long checkAccountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
162            if (checkAccountId != -1) {
163                // launch an account sync in the controller
164                syncOneAccount(controller, checkAccountId, startId);
165            } else {
166                // Find next account to sync, and reschedule
167                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
168                reschedule(alarmManager);
169                stopSelf(startId);
170            }
171        }
172        else if (ACTION_CANCEL.equals(action)) {
173            if (Config.LOGD && Email.DEBUG) {
174                Log.d(Email.LOG_TAG, "*** MailService: cancel");
175            }
176            cancel();
177            stopSelf(startId);
178        }
179        else if (ACTION_RESCHEDULE.equals(action)) {
180            if (Config.LOGD && Email.DEBUG) {
181                Log.d(Email.LOG_TAG, "*** MailService: reschedule");
182            }
183            AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
184            reschedule(alarmManager);
185            stopSelf(startId);
186        } else if (ACTION_NOTIFY_MAIL.equals(action)) {
187            long accountId = intent.getLongExtra(EXTRA_CHECK_ACCOUNT, -1);
188            int newMessageCount = intent.getIntExtra(EXTRA_MESSAGE_COUNT, -1);
189            if (Config.LOGD && Email.DEBUG) {
190                Log.d(Email.LOG_TAG, "*** MailService: notify accountId=" + Long.toString(accountId)
191                        + " count=" + newMessageCount);
192            }
193            if (accountId != -1) {
194                updateAccountReport(accountId, newMessageCount);
195                notifyNewMessages(accountId);
196            }
197            stopSelf(startId);
198        }
199    }
200
201    @Override
202    public IBinder onBind(Intent intent) {
203        return null;
204    }
205
206    @Override
207    public void onDestroy() {
208        super.onDestroy();
209        Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback);
210    }
211
212    private void cancel() {
213        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
214        PendingIntent pi = createAlarmIntent(-1, null);
215        alarmMgr.cancel(pi);
216    }
217
218    /**
219     * Create and send an alarm with the entire list.  This also sends a list of known last-sync
220     * times with the alarm, so if we are killed between alarms, we don't lose this info.
221     *
222     * @param alarmMgr passed in so we can mock for testing.
223     */
224    /* package */ void reschedule(AlarmManager alarmMgr) {
225        // restore the reports if lost
226        setupSyncReports(-1);
227        synchronized (mSyncReports) {
228            int numAccounts = mSyncReports.size();
229            long[] accountInfo = new long[numAccounts * 2];     // pairs of { accountId, lastSync }
230            int accountInfoIndex = 0;
231
232            long nextCheckTime = Long.MAX_VALUE;
233            AccountSyncReport nextAccount = null;
234            long timeNow = SystemClock.elapsedRealtime();
235
236            for (AccountSyncReport report : mSyncReports.values()) {
237                if (report.syncInterval <= 0) {                         // no timed checks - skip
238                    continue;
239                }
240                // select next account to sync
241                if ((report.prevSyncTime == 0)                          // never checked
242                        || (report.nextSyncTime < timeNow)) {           // overdue
243                    nextCheckTime = 0;
244                    nextAccount = report;
245                } else if (report.nextSyncTime < nextCheckTime) {       // next to be checked
246                    nextCheckTime = report.nextSyncTime;
247                    nextAccount = report;
248                }
249                // collect last-sync-times for all accounts
250                // this is using pairs of {long,long} to simplify passing in a bundle
251                accountInfo[accountInfoIndex++] = report.accountId;
252                accountInfo[accountInfoIndex++] = report.prevSyncTime;
253            }
254
255            // set/clear alarm as needed
256            long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId;
257            PendingIntent pi = createAlarmIntent(idToCheck, accountInfo);
258
259            if (nextAccount == null) {
260                alarmMgr.cancel(pi);
261                Log.d(Email.LOG_TAG, "alarm cancel - no account to check");
262            } else {
263                alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi);
264                Log.d(Email.LOG_TAG, "alarm set at " + nextCheckTime + " for " + nextAccount);
265            }
266        }
267    }
268
269    /**
270     * Return a pending intent for use by this alarm.  Most of the fields must be the same
271     * (in order for the intent to be recognized by the alarm manager) but the extras can
272     * be different, and are passed in here as parameters.
273     */
274    /* package */ PendingIntent createAlarmIntent(long checkId, long[] accountInfo) {
275        Intent i = new Intent();
276        i.setClassName("com.android.email", "com.android.email.service.MailService");
277        i.setAction(ACTION_CHECK_MAIL);
278        i.putExtra(EXTRA_CHECK_ACCOUNT, checkId);
279        i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo);
280        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
281        return pi;
282    }
283
284    /**
285     * Start a controller sync for a specific account
286     */
287    private void syncOneAccount(Controller controller, long checkAccountId, int startId) {
288        long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX);
289        if (inboxId == Mailbox.NO_MAILBOX) {
290            // no inbox??  sync mailboxes
291        } else {
292            controller.serviceCheckMail(checkAccountId, inboxId, startId, mControllerCallback);
293        }
294    }
295
296    /**
297     * Note:  Times are relative to SystemClock.elapsedRealtime()
298     */
299    private static class AccountSyncReport {
300        long accountId;
301        long prevSyncTime;      // 0 == unknown
302        long nextSyncTime;      // 0 == ASAP  -1 == don't sync
303        int numNewMessages;
304
305        int syncInterval;
306        boolean notify;
307        boolean vibrate;
308        Uri ringtoneUri;
309
310        String displayName;     // temporary, for debug logging
311
312
313        @Override
314        public String toString() {
315            return displayName + ": prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime
316                    + " numNew=" + numNewMessages;
317        }
318    }
319
320    /**
321     * scan accounts to create a list of { acct, prev sync, next sync, #new }
322     * use this to create a fresh copy.  assumes all accounts need sync
323     *
324     * @param accountId -1 will rebuild the list if empty.  other values will force loading
325     *   of a single account (e.g if it was created after the original list population)
326     */
327    /* package */ void setupSyncReports(long accountId) {
328        synchronized (mSyncReports) {
329            if (accountId == -1) {
330                // -1 == reload the list if empty, otherwise exit immediately
331                if (mSyncReports.size() > 0) {
332                    return;
333                }
334            } else {
335                // load a single account if it doesn't already have a sync record
336                if (mSyncReports.containsKey(accountId)) {
337                    return;
338                }
339            }
340
341            // setup to add a single account or all accounts
342            Uri uri;
343            if (accountId == -1) {
344                uri = Account.CONTENT_URI;
345            } else {
346                uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId);
347            }
348
349            // TODO use a narrower projection here
350            Cursor c = getContentResolver().query(uri, Account.CONTENT_PROJECTION,
351                    null, null, null);
352            try {
353                while (c.moveToNext()) {
354                    AccountSyncReport report = new AccountSyncReport();
355                    int syncInterval = c.getInt(Account.CONTENT_SYNC_INTERVAL_COLUMN);
356                    int flags = c.getInt(Account.CONTENT_FLAGS_COLUMN);
357                    String ringtoneString = c.getString(Account.CONTENT_RINGTONE_URI_COLUMN);
358
359                    // For debugging only
360                    if (DEBUG_FORCE_QUICK_REFRESH && syncInterval >= 0) {
361                        syncInterval = 1;
362                    }
363
364                    report.accountId = c.getLong(Account.CONTENT_ID_COLUMN);
365                    report.prevSyncTime = 0;
366                    report.nextSyncTime = (syncInterval > 0) ? 0 : -1;  // 0 == ASAP -1 == no sync
367                    report.numNewMessages = 0;
368
369                    report.syncInterval = syncInterval;
370                    report.notify = (flags & Account.FLAGS_NOTIFY_NEW_MAIL) != 0;
371                    report.vibrate = (flags & Account.FLAGS_VIBRATE) != 0;
372                    report.ringtoneUri = (ringtoneString == null) ? null
373                                                                  : Uri.parse(ringtoneString);
374
375                    report.displayName = c.getString(Account.CONTENT_DISPLAY_NAME_COLUMN);
376
377                    // TODO lookup # new in inbox
378                    mSyncReports.put(report.accountId, report);
379                }
380            } finally {
381                c.close();
382            }
383        }
384    }
385
386    /**
387     * Update list with a single account's sync times and unread count
388     *
389     * @param accountId the account being udpated
390     * @param newCount the number of new messages, or -1 if not being reported (don't update)
391     * @return the report for the updated account, or null if it doesn't exist (e.g. deleted)
392     */
393    /* package */ AccountSyncReport updateAccountReport(long accountId, int newCount) {
394        // restore the reports if lost
395        setupSyncReports(accountId);
396        synchronized (mSyncReports) {
397            AccountSyncReport report = mSyncReports.get(accountId);
398            if (report == null) {
399                // discard result - there is no longer an account with this id
400                Log.d(Email.LOG_TAG, "No account to update for id=" + Long.toString(accountId));
401                return null;
402            }
403
404            // report found - update it (note - editing the report while in-place in the hashmap)
405            report.prevSyncTime = SystemClock.elapsedRealtime();
406            if (report.syncInterval > 0) {
407                report.nextSyncTime = report.prevSyncTime + (report.syncInterval * 1000 * 60);
408            }
409            if (newCount != -1) {
410                report.numNewMessages = newCount;
411            }
412            Log.d(Email.LOG_TAG, "update account " + report.toString());
413            return report;
414        }
415    }
416
417    /**
418     * when we receive an alarm, update the account sync reports list if necessary
419     * this will be the case when if we have restarted the process and lost the data
420     * in the global.
421     *
422     * @param restoreIntent the intent with the list
423     */
424    /* package */ void restoreSyncReports(Intent restoreIntent) {
425        // restore the reports if lost
426        setupSyncReports(-1);
427        synchronized (mSyncReports) {
428            long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO);
429            if (accountInfo == null) {
430                Log.d(Email.LOG_TAG, "no data in intent to restore");
431                return;
432            }
433            int accountInfoIndex = 0;
434            int accountInfoLimit = accountInfo.length;
435            while (accountInfoIndex < accountInfoLimit) {
436                long accountId = accountInfo[accountInfoIndex++];
437                long prevSync = accountInfo[accountInfoIndex++];
438                AccountSyncReport report = mSyncReports.get(accountId);
439                if (report != null) {
440                    if (report.prevSyncTime == 0) {
441                        report.prevSyncTime = prevSync;
442                        Log.d(Email.LOG_TAG, "restore prev sync for account" + report);
443                    }
444                }
445            }
446        }
447    }
448
449    class ControllerResults implements Controller.Result {
450
451        public void loadAttachmentCallback(MessagingException result, long messageId,
452                long attachmentId, int progress) {
453        }
454
455        public void updateMailboxCallback(MessagingException result, long accountId,
456                long mailboxId, int progress, int numNewMessages) {
457            if (result == null) {
458                updateAccountReport(accountId, numNewMessages);
459                if (numNewMessages > 0) {
460                    notifyNewMessages(accountId);
461                }
462            } else {
463                updateAccountReport(accountId, -1);
464            }
465        }
466
467        public void updateMailboxListCallback(MessagingException result, long accountId,
468                int progress) {
469        }
470
471        public void serviceCheckMailCallback(MessagingException result, long accountId,
472                long mailboxId, int progress, long tag) {
473            if (progress == 100) {
474                AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
475                reschedule(alarmManager);
476                int serviceId = MailService.this.mStartId;
477                if (tag != 0) {
478                    serviceId = (int) tag;
479                }
480                stopSelf(serviceId);
481            }
482        }
483    }
484
485    /**
486     * Prepare notifications for a given new account having received mail
487     * The notification is organized around the account that has the new mail (e.g. selecting
488     * the alert preferences) but the notification will include a summary if other
489     * accounts also have new mail.
490     */
491    private void notifyNewMessages(long accountId) {
492        boolean notify = false;
493        boolean vibrate = false;
494        Uri ringtone = null;
495        int accountsWithNewMessages = 0;
496        int numNewMessages = 0;
497        String reportName = null;
498        synchronized (mSyncReports) {
499            for (AccountSyncReport report : mSyncReports.values()) {
500                if (report.numNewMessages == 0) {
501                    continue;
502                }
503                numNewMessages += report.numNewMessages;
504                accountsWithNewMessages += 1;
505                if (report.accountId == accountId) {
506                    notify = report.notify;
507                    vibrate = report.vibrate;
508                    ringtone = report.ringtoneUri;
509                    reportName = report.displayName;
510                }
511            }
512        }
513        if (!notify) {
514            return;
515        }
516
517        // set up to post a notification
518        Intent intent;
519        String reportString;
520
521        if (accountsWithNewMessages == 1) {
522            // Prepare a report for a single account
523            // "12 unread (gmail)"
524            reportString = getResources().getQuantityString(
525                    R.plurals.notification_new_one_account_fmt, numNewMessages,
526                    numNewMessages, reportName);
527            intent = MessageList.actionHandleAccountIntent(this,
528                    accountId, -1, Mailbox.TYPE_INBOX);
529        } else {
530            // Prepare a report for multiple accounts
531            // "4 accounts"
532            reportString = getResources().getQuantityString(
533                    R.plurals.notification_new_multi_account_fmt, accountsWithNewMessages,
534                    accountsWithNewMessages);
535            intent = MessageList.actionHandleAccountIntent(this,
536                    -1, MessageList.QUERY_ALL_INBOXES, -1);
537        }
538
539        // prepare appropriate pending intent, set up notification, and send
540        PendingIntent pending = PendingIntent.getActivity(this, 0, intent, 0);
541
542        Notification notification = new Notification(
543                R.drawable.stat_notify_email_generic,
544                getString(R.string.notification_new_title),
545                System.currentTimeMillis());
546        notification.setLatestEventInfo(this,
547                getString(R.string.notification_new_title),
548                reportString,
549                pending);
550
551        notification.sound = ringtone;
552        notification.defaults = vibrate
553            ? Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE
554            : Notification.DEFAULT_LIGHTS;
555
556        NotificationManager notificationManager =
557            (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
558        notificationManager.notify(NEW_MESSAGE_NOTIFICATION_ID, notification);
559    }
560}
561