NotificationController.java revision 8d8537cd2e39268e0fdcd019bc8b6c4572b7c520
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;
18
19import com.android.email.activity.ContactStatusLoader;
20import com.android.email.activity.Welcome;
21import com.android.email.activity.setup.AccountSettingsXL;
22import com.android.email.mail.Address;
23import com.android.email.provider.EmailContent;
24import com.android.email.provider.EmailContent.Account;
25import com.android.email.provider.EmailContent.Attachment;
26import com.android.email.provider.EmailContent.Message;
27
28import android.app.Notification;
29import android.app.Notification.Builder;
30import android.app.NotificationManager;
31import android.app.PendingIntent;
32import android.content.Context;
33import android.content.Intent;
34import android.graphics.Bitmap;
35import android.graphics.BitmapFactory;
36import android.media.AudioManager;
37import android.net.Uri;
38import android.text.SpannableString;
39import android.text.TextUtils;
40import android.text.style.TextAppearanceSpan;
41
42/**
43 * Class that manages notifications.
44 *
45 * TODO Gather all notification related code here
46 */
47public class NotificationController {
48    public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
49    public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
50    public static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
51    public static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
52    public static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
53
54    private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
55    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
56
57    private static NotificationController sInstance;
58    private final Context mContext;
59    private final NotificationManager mNotificationManager;
60    private final AudioManager mAudioManager;
61    private final Bitmap mGenericSenderIcon;
62    private final Clock mClock;
63
64    /** Constructor */
65    /* package */ NotificationController(Context context, Clock clock) {
66        mContext = context.getApplicationContext();
67        mNotificationManager = (NotificationManager) context.getSystemService(
68                Context.NOTIFICATION_SERVICE);
69        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
70        mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
71                R.drawable.ic_contact_picture);
72        mClock = clock;
73    }
74
75    /** Singleton access */
76    public static synchronized NotificationController getInstance(Context context) {
77        if (sInstance == null) {
78            sInstance = new NotificationController(context, Clock.INSTANCE);
79        }
80        return sInstance;
81    }
82
83    /**
84     * Generic notifier for any account.  Uses notification rules from account.
85     *
86     * @param account The account for which the notification is posted
87     * @param ticker String for ticker
88     * @param contentTitle String for notification content title
89     * @param contentText String for notification content text
90     * @param intent The intent to launch from the notification
91     * @param notificationId The notification id
92     */
93    public void postAccountNotification(Account account, String ticker, String contentTitle,
94            String contentText, Intent intent, int notificationId) {
95
96        // Pending Intent
97        PendingIntent pending =
98            PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
99
100        // Ringtone & Vibration
101        String ringtoneString = account.getRingtone();
102        Uri ringTone = (ringtoneString == null) ? null : Uri.parse(ringtoneString);
103        boolean vibrate = 0 != (account.mFlags & Account.FLAGS_VIBRATE_ALWAYS);
104        boolean vibrateWhenSilent = 0 != (account.mFlags & Account.FLAGS_VIBRATE_WHEN_SILENT);
105
106        // Use the account's notification rules for sound & vibrate (but always notify)
107        boolean nowSilent =
108            mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE;
109
110        int defaults = Notification.DEFAULT_LIGHTS;
111        if (vibrate || (vibrateWhenSilent && nowSilent)) {
112            defaults |= Notification.DEFAULT_VIBRATE;
113        }
114
115        // Notification
116        Notification.Builder nb = new Notification.Builder(mContext);
117        nb.setSmallIcon(R.drawable.stat_notify_email_generic);
118        nb.setTicker(ticker);
119        nb.setContentTitle(contentTitle);
120        nb.setContentText(contentText);
121        nb.setContentIntent(pending);
122        nb.setSound(ringTone);
123        nb.setDefaults(defaults);
124        Notification notification = nb.getNotification();
125
126        mNotificationManager.notify(notificationId, notification);
127    }
128
129    /**
130     * Generic notification canceler.
131     * @param notificationId The notification id
132     */
133    public void cancelNotification(int notificationId) {
134        mNotificationManager.cancel(notificationId);
135    }
136
137    /**
138     * @return the "new message" notification ID for an account. It just assumes
139     *         accountID won't be too huge. Any other smarter/cleaner way?
140     */
141    private int getNewMessageNotificationId(long accountId) {
142        return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
143    }
144
145    /**
146     * Dismiss new message notification
147     *
148     * @param accountId ID of the target account, or -1 for all accounts.
149     */
150    public void cancelNewMessageNotification(long accountId) {
151        if (accountId == -1) {
152            new Utility.ForEachAccount(mContext) {
153                @Override
154                protected void performAction(long accountId) {
155                    cancelNewMessageNotification(accountId);
156                }
157            }.execute();
158        } else {
159            mNotificationManager.cancel(getNewMessageNotificationId(accountId));
160        }
161    }
162
163    /**
164     * Show (or update) the "new message" notification.
165     */
166    public void showNewMessageNotification(final long accountId, final int unseenMessageCount,
167            final int justFetchedCount) {
168        Utility.runAsync(new Runnable() {
169            @Override
170            public void run() {
171                Notification n = createNewMessageNotification(accountId, unseenMessageCount);
172                if (n == null) {
173                    return;
174                }
175                mNotificationManager.notify(getNewMessageNotificationId(accountId), n);
176            }
177        });
178    }
179
180    /**
181     * @return The sender's photo, if available, or null.
182     *
183     * Don't call it on the UI thread.
184     */
185    private Bitmap getSenderPhoto(Message message) {
186        Address sender = Address.unpackFirst(message.mFrom);
187        if (sender == null) {
188            return null;
189        }
190        String email = sender.getAddress();
191        if (TextUtils.isEmpty(email)) {
192            return null;
193        }
194        return ContactStatusLoader.load(mContext, email).mPhoto;
195    }
196
197    /**
198     * Create a notification
199     *
200     * Don't call it on the UI thread.
201     */
202    /* package */ Notification createNewMessageNotification(long accountId,
203            int unseenMessageCount) {
204        final Account account = Account.restoreAccountWithId(mContext, accountId);
205        if (account == null) {
206            return null;
207        }
208        // Get the latest message
209        final Message message = Message.getLatestIncomingMessage(mContext, accountId);
210        if (message == null) {
211            return null; // no message found???
212        }
213
214        final String senderName = Address.toFriendly(Address.unpack(message.mFrom));
215        final String subject = message.mSubject;
216        final Bitmap senderPhoto = getSenderPhoto(message);
217
218        // Intent to open inbox
219        PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0,
220                Welcome.createOpenAccountInboxIntent(mContext, accountId),
221                PendingIntent.FLAG_UPDATE_CURRENT);
222
223        Notification.Builder builder = new Notification.Builder(mContext)
224                .setSmallIcon(R.drawable.stat_notify_email_generic)
225                .setWhen(mClock.getTime())
226                .setLargeIcon(senderPhoto != null ? senderPhoto : mGenericSenderIcon)
227                .setContentTitle(getNotificationTitle(senderName, account.mDisplayName))
228                .setContentText(subject)
229                .setContentIntent(contentIntent);
230        if (unseenMessageCount > 1) {
231            builder.setNumber(unseenMessageCount);
232        }
233
234        Notification notification = builder.getNotification();
235
236        setupNotificationSoundAndVibrationFromAccount(notification, account);
237        return notification;
238    }
239
240    /**
241     * Creates the notification title.
242     *
243     * If only 1 account, just show the sender name.
244     * If 2+ accounts, make it "SENDER_NAME to RECEIVER_NAME", and gray out the "to RECEIVER_NAME"
245     * part.
246     */
247    /* package */ SpannableString getNotificationTitle(String sender, String receiverDisplayName) {
248        final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
249        if (numAccounts == 1) {
250            return new SpannableString(sender);
251        } else {
252            // "to [account name]"
253            String toAcccount = mContext.getResources().getString(R.string.notification_to_account,
254                    receiverDisplayName);
255            // "[Sender] to [account name]"
256            SpannableString senderToAccount = new SpannableString(sender + " " + toAcccount);
257
258            // "[Sender] to [account name]"
259            //           ^^^^^^^^^^^^^^^^^ <- Make this part gray
260            TextAppearanceSpan secondarySpan = new TextAppearanceSpan(
261                    mContext, R.style.notification_secondary_text);
262            senderToAccount.setSpan(secondarySpan, sender.length() + 1, senderToAccount.length(),
263                    0);
264            return senderToAccount;
265        }
266    }
267
268    // Overridden for testing (AudioManager can't be mocked out.)
269    /* package */ int getRingerMode() {
270        return mAudioManager.getRingerMode();
271    }
272
273    /* package */ boolean isRingerModeSilent() {
274        return getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
275    }
276
277    /* package */ void setupNotificationSoundAndVibrationFromAccount(Notification notification,
278            Account account) {
279        final int flags = account.mFlags;
280        final String ringtoneUri = account.mRingtoneUri;
281        final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
282        final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
283
284        notification.sound = (ringtoneUri == null) ? null : Uri.parse(ringtoneUri);
285
286        if (vibrate || (vibrateWhenSilent && isRingerModeSilent())) {
287            notification.defaults |= Notification.DEFAULT_VIBRATE;
288        }
289
290        // This code is identical to that used by Gmail and GTalk for notifications
291        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
292        notification.defaults |= Notification.DEFAULT_LIGHTS;
293    }
294
295    /**
296     * Generic warning notification
297     */
298    public void showWarningNotification(int id, String tickerText, String notificationText,
299            Intent intent) {
300        PendingIntent pendingIntent = null;
301        if (intent != null) {
302            pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
303                    PendingIntent.FLAG_UPDATE_CURRENT);
304        }
305        Builder b = new Builder(mContext);
306        b.setSmallIcon(android.R.drawable.stat_notify_error)
307            .setTicker(tickerText)
308            .setWhen(mClock.getTime())
309            .setContentTitle(tickerText)
310            .setContentText(notificationText)
311            .setContentIntent(pendingIntent)
312            .setAutoCancel(true);
313        Notification n = b.getNotification();
314        mNotificationManager.notify(id, n);
315    }
316
317    /**
318     * Alert the user that an attachment couldn't be forwarded.  This is a very unusual case, and
319     * perhaps we shouldn't even send a notification. For now, it's helpful for debugging.
320     */
321    public void showDownloadForwardFailedNotification(Attachment att) {
322        showWarningNotification(NOTIFICATION_ID_ATTACHMENT_WARNING,
323                mContext.getString(R.string.forward_download_failed_ticker),
324                mContext.getString(R.string.forward_download_failed_notification,
325                        att.mFileName), null);
326    }
327
328    /**
329     * Alert the user that login failed for the specified account
330     */
331    private int getLoginFailedNotificationId(long accountId) {
332        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
333    }
334
335    // NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
336    public void showLoginFailedNotification(long accountId) {
337        final Account account = Account.restoreAccountWithId(mContext, accountId);
338        if (account == null) return;
339        showWarningNotification(getLoginFailedNotificationId(accountId),
340                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
341                mContext.getString(R.string.login_failed_notification),
342                AccountSettingsXL.createAccountSettingsIntent(mContext, accountId));
343    }
344
345    public void cancelLoginFailedNotification(long accountId) {
346        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
347    }
348}
349