MailService.java revision 72dce733900a812cf11a5ac08756362030556849
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.Account;
20import com.android.email.Email;
21import com.android.email.MessagingController;
22import com.android.email.MessagingListener;
23import com.android.email.R;
24import com.android.email.activity.AccountFolderList;
25import com.android.email.activity.FolderMessageList;
26import com.android.email.mail.MessagingException;
27import com.android.email.mail.Store;
28import com.android.email.mail.store.LocalStore;
29import com.android.email.provider.EmailContent;
30
31import android.app.AlarmManager;
32import android.app.Notification;
33import android.app.NotificationManager;
34import android.app.PendingIntent;
35import android.app.Service;
36import android.content.Context;
37import android.content.Intent;
38import android.database.Cursor;
39import android.net.Uri;
40import android.os.IBinder;
41import android.os.SystemClock;
42import android.text.TextUtils;
43import android.util.Config;
44import android.util.Log;
45
46import java.util.ArrayList;
47import java.util.HashMap;
48
49/**
50 */
51public class MailService extends Service {
52    private static final String ACTION_CHECK_MAIL = "com.android.email.intent.action.MAIL_SERVICE_WAKEUP";
53    private static final String ACTION_RESCHEDULE = "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE";
54    private static final String ACTION_CANCEL = "com.android.email.intent.action.MAIL_SERVICE_CANCEL";
55
56    private static final String EXTRA_CHECK_ACCOUNT = "com.android.email.intent.extra.ACCOUNT";
57
58    private Listener mListener = new Listener();
59
60    private int mStartId;
61
62    public static void actionReschedule(Context context) {
63        Intent i = new Intent();
64        i.setClass(context, MailService.class);
65        i.setAction(MailService.ACTION_RESCHEDULE);
66        context.startService(i);
67    }
68
69    public static void actionCancel(Context context)  {
70        Intent i = new Intent();
71        i.setClass(context, MailService.class);
72        i.setAction(MailService.ACTION_CANCEL);
73        context.startService(i);
74    }
75
76    /**
77     * Entry point for asynchronous message services (e.g. push mode) to post notifications of new
78     * messages.  Note:  Although this is not a blocking call, it will start the MessagingController
79     * which will attempt to load the new messages.  So the Store should expect to be opened and
80     * fetched from shortly after making this call.
81     *
82     * @param storeUri the Uri of the store that is reporting new messages
83     */
84    public static void actionNotifyNewMessages(Context context, String storeUri) {
85        Intent i = new Intent(ACTION_CHECK_MAIL);
86        i.setClass(context, MailService.class);
87        i.putExtra(EXTRA_CHECK_ACCOUNT, storeUri);
88        context.startService(i);
89    }
90
91    @Override
92    public void onStart(Intent intent, int startId) {
93        super.onStart(intent, startId);
94        this.mStartId = startId;
95
96        MessagingController controller = MessagingController.getInstance(getApplication());
97        controller.addListener(mListener);
98        if (ACTION_CHECK_MAIL.equals(intent.getAction())) {
99            if (Config.LOGD && Email.DEBUG) {
100                Log.d(Email.LOG_TAG, "*** MailService: checking mail");
101            }
102            // Only check mail for accounts that have enabled automatic checking.  There is still
103            // a bug here in that we check every enabled account, on every refresh - irrespective
104            // of that account's refresh frequency - but this fixes the worst case of checking
105            // accounts that should not have been checked at all.
106            // Also note:  Due to the organization of this service, you must gather the accounts
107            // and make a single call to controller.checkMail().
108
109            // TODO: Notification for single push account will fire up checks on all other
110            // accounts.  This needs to be cleaned up for better efficiency.
111            String specificStoreUri = intent.getStringExtra(EXTRA_CHECK_ACCOUNT);
112
113            ArrayList<EmailContent.Account> accountsToCheck = new ArrayList<EmailContent.Account>();
114
115            Cursor c = null;
116            try {
117                c = this.getContentResolver().query(
118                        EmailContent.Account.CONTENT_URI,
119                        EmailContent.Account.CONTENT_PROJECTION,
120                        null, null, null);
121                while (c.moveToNext()) {
122                    EmailContent.Account account = EmailContent.getContent(c,
123                            EmailContent.Account.class);
124                    int interval = account.getAutomaticCheckIntervalMinutes();
125                    String storeUri = account.getStoreUri(this);
126                    if (interval > 0 || (storeUri != null && storeUri.equals(specificStoreUri))) {
127                        accountsToCheck.add(account);
128                    }
129
130                    // For each account, switch pushmail on or off
131                    enablePushMail(account, interval == EmailContent.Account.CHECK_INTERVAL_PUSH);
132                }
133            } finally {
134                if (c != null) {
135                    c.close();
136                }
137            }
138
139            EmailContent.Account[] accounts = accountsToCheck.toArray(
140                    new EmailContent.Account[accountsToCheck.size()]);
141            controller.checkMail(this, accounts, mListener);
142        }
143        else if (ACTION_CANCEL.equals(intent.getAction())) {
144            if (Config.LOGD && Email.DEBUG) {
145                Log.d(Email.LOG_TAG, "*** MailService: cancel");
146            }
147            cancel();
148            stopSelf(startId);
149        }
150        else if (ACTION_RESCHEDULE.equals(intent.getAction())) {
151            if (Config.LOGD && Email.DEBUG) {
152                Log.d(Email.LOG_TAG, "*** MailService: reschedule");
153            }
154            reschedule();
155            stopSelf(startId);
156        }
157    }
158
159    @Override
160    public void onDestroy() {
161        super.onDestroy();
162        MessagingController.getInstance(getApplication()).removeListener(mListener);
163    }
164
165    private void cancel() {
166        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
167        Intent i = new Intent();
168        i.setClassName("com.android.email", "com.android.email.service.MailService");
169        i.setAction(ACTION_CHECK_MAIL);
170        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
171        alarmMgr.cancel(pi);
172    }
173
174    private void reschedule() {
175        AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
176        Intent i = new Intent();
177        i.setClassName("com.android.email", "com.android.email.service.MailService");
178        i.setAction(ACTION_CHECK_MAIL);
179        PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
180
181        int shortestInterval = -1;
182        Cursor c = null;
183        try {
184            c = this.getContentResolver().query(
185                    EmailContent.Account.CONTENT_URI,
186                    EmailContent.Account.CONTENT_PROJECTION,
187                    null, null, null);
188            while (c.moveToNext()) {
189                EmailContent.Account account = EmailContent.getContent(c,
190                        EmailContent.Account.class);
191                int interval = account.getAutomaticCheckIntervalMinutes();
192                if (interval > 0 && (interval < shortestInterval || shortestInterval == -1)) {
193                    shortestInterval = interval;
194                }
195                enablePushMail(account, interval == Account.CHECK_INTERVAL_PUSH);
196            }
197        } finally {
198            if (c != null) {
199                c.close();
200            }
201        }
202
203        if (shortestInterval == -1) {
204            alarmMgr.cancel(pi);
205        }
206        else {
207            alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()
208                    + (shortestInterval * (60 * 1000)), pi);
209        }
210    }
211
212    public IBinder onBind(Intent intent) {
213        return null;
214    }
215
216    class Listener extends MessagingListener {
217        HashMap<EmailContent.Account, Integer> accountsWithNewMail =
218            new HashMap<EmailContent.Account, Integer>();
219
220        // TODO this should be redone because account is usually null, not very interesting.
221        // I think it would make more sense to pass Account[] here in case anyone uses it
222        // In any case, it should be noticed that this is called once per cycle
223        @Override
224        public void checkMailStarted(Context context, EmailContent.Account account) {
225            accountsWithNewMail.clear();
226        }
227
228        // Called once per checked account
229        @Override
230        public void checkMailFailed(Context context, EmailContent.Account account, String reason) {
231            if (Config.LOGD && Email.DEBUG) {
232                Log.d(Email.LOG_TAG, "*** MailService: checkMailFailed: " + reason);
233            }
234            reschedule();
235            stopSelf(mStartId);
236        }
237
238        // Called once per checked account
239        @Override
240        public void synchronizeMailboxFinished(EmailContent.Account account,
241                EmailContent.Mailbox folder, int totalMessagesInMailbox, int numNewMessages) {
242            if (Config.LOGD && Email.DEBUG) {
243                Log.d(Email.LOG_TAG, "*** MailService: synchronizeMailboxFinished: total=" +
244                        totalMessagesInMailbox + " new=" + numNewMessages);
245            }
246            if (numNewMessages > 0 &&
247                    ((account.getFlags() & EmailContent.Account.FLAGS_NOTIFY_NEW_MAIL) != 0)) {
248                accountsWithNewMail.put(account, numNewMessages);
249            }
250        }
251
252        // TODO this should be redone because account is usually null, not very interesting.
253        // I think it would make more sense to pass Account[] here in case anyone uses it
254        // In any case, it should be noticed that this is called once per cycle
255        @Override
256        public void checkMailFinished(Context context, EmailContent.Account account) {
257            if (Config.LOGD && Email.DEBUG) {
258                Log.d(Email.LOG_TAG, "*** MailService: checkMailFinished");
259            }
260            NotificationManager notifMgr = (NotificationManager)context
261                    .getSystemService(Context.NOTIFICATION_SERVICE);
262
263            if (accountsWithNewMail.size() > 0) {
264                Notification notif = new Notification(R.drawable.stat_notify_email_generic,
265                        getString(R.string.notification_new_title), System.currentTimeMillis());
266                boolean vibrate = false;
267                String ringtone = null;
268                if (accountsWithNewMail.size() > 1) {
269                    for (EmailContent.Account account1 : accountsWithNewMail.keySet()) {
270                        if ((account1.getFlags() & EmailContent.Account.FLAGS_VIBRATE) != 0) {
271                            vibrate = true;
272                        }
273                        ringtone = account1.getRingtone();
274                    }
275                    Intent i = new Intent(context, AccountFolderList.class);
276                    PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
277                    notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
278                            getResources().
279                                getQuantityString(R.plurals.notification_new_multi_account_fmt,
280                                    accountsWithNewMail.size(),
281                                    accountsWithNewMail.size()), pi);
282                } else {
283                    EmailContent.Account account1 = accountsWithNewMail.keySet().iterator().next();
284                    int totalNewMails = accountsWithNewMail.get(account1);
285                    Intent i = FolderMessageList.actionHandleAccountIntent(context,
286                            account1.mId, Email.INBOX);
287                    PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
288                    notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
289                            getResources().
290                                getQuantityString(R.plurals.notification_new_one_account_fmt,
291                                    totalNewMails, totalNewMails,
292                                    account1.getDescription()), pi);
293                    vibrate = ((account1.getFlags() & EmailContent.Account.FLAGS_VIBRATE) != 0);
294                    ringtone = account1.getRingtone();
295                }
296                notif.defaults = Notification.DEFAULT_LIGHTS;
297                notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone);
298                if (vibrate) {
299                    notif.defaults |= Notification.DEFAULT_VIBRATE;
300                }
301                notifMgr.notify(1, notif);
302            }
303
304            reschedule();
305            stopSelf(mStartId);
306        }
307    }
308
309    /**
310     * For any account that wants push mail, get its Store and start the pushmail service.
311     * This function makes no attempt to optimize, so accounts may have push enabled (or disabled)
312     * repeatedly, and should handle this appropriately.
313     *
314     * @param account the account that needs push delivery enabled
315     */
316    private void enablePushMail(EmailContent.Account account, boolean enable) {
317        try {
318            String localUri = account.getLocalStoreUri(this);
319            String storeUri = account.getStoreUri(this);
320            if (localUri != null && storeUri != null) {
321                LocalStore localStore = (LocalStore) Store.getInstance(
322                        localUri, this.getBaseContext(), null);
323                Store store = Store.getInstance(storeUri, this.getBaseContext(),
324                        localStore.getPersistentCallbacks());
325                if (store != null) {
326                    store.enablePushModeDelivery(enable);
327                }
328            }
329        } catch (MessagingException me) {
330            if (Config.LOGD && Email.DEBUG) {
331                Log.d(Email.LOG_TAG, "Failed to enable push mail for account" + account.getName() +
332                        " with exception " + me.toString());
333            }
334        }
335    }
336}
337