NotificationController.java revision 7126e5ae53082e1f16eb0624208b6ab7edebd392
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.NotificationManager;
30import android.app.PendingIntent;
31import android.content.Context;
32import android.content.Intent;
33import android.graphics.Bitmap;
34import android.graphics.BitmapFactory;
35import android.media.AudioManager;
36import android.net.Uri;
37import android.text.TextUtils;
38
39/**
40 * Class that manages notifications.
41 *
42 * TODO Gather all notification related code here
43 */
44public class NotificationController {
45    public static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
46    public static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
47    public static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
48
49    private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
50    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
51
52    private static NotificationController sInstance;
53    private final Context mContext;
54    private final NotificationManager mNotificationManager;
55    private final AudioManager mAudioManager;
56
57    /** Constructor */
58    private NotificationController(Context context) {
59        mContext = context.getApplicationContext();
60        mNotificationManager = (NotificationManager) context.getSystemService(
61                Context.NOTIFICATION_SERVICE);
62        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
63    }
64
65    /** Singleton access */
66    public static synchronized NotificationController getInstance(Context context) {
67        if (sInstance == null) {
68            sInstance = new NotificationController(context);
69        }
70        return sInstance;
71    }
72
73    /**
74     * @return the "new message" notification ID for an account. It just assumes
75     *         accountID won't be too huge. Any other smarter/cleaner way?
76     */
77    private int getNewMessageNotificationId(long accountId) {
78        return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
79    }
80
81    /**
82     * Dismiss new message notification
83     *
84     * @param accountId ID of the target account, or -1 for all accounts.
85     */
86    public void cancelNewMessageNotification(long accountId) {
87        if (accountId == -1) {
88            new Utility.ForEachAccount(mContext) {
89                @Override
90                protected void performAction(long accountId) {
91                    cancelNewMessageNotification(accountId);
92                }
93            }.execute();
94        } else {
95            mNotificationManager.cancel(getNewMessageNotificationId(accountId));
96        }
97    }
98
99    /**
100     * Show (or update) the "new message" notification.
101     */
102    public void showNewMessageNotification(final long accountId, final int unseenMessageCount,
103            final int justFetchedCount) {
104        Utility.runAsync(new Runnable() {
105            @Override
106            public void run() {
107                Notification n = createNewMessageNotification(accountId, unseenMessageCount,
108                        justFetchedCount);
109                if (n == null) {
110                    return;
111                }
112                mNotificationManager.notify(getNewMessageNotificationId(accountId), n);
113            }
114        });
115    }
116
117    /**
118     * @return The sender's photo, if available, or null.
119     *
120     * Don't call it on the UI thread.
121     */
122    private Bitmap getSenderPhoto(Message message) {
123        Address sender = Address.unpackFirst(message.mFrom);
124        if (sender == null) {
125            return null;
126        }
127        String email = sender.getAddress();
128        if (TextUtils.isEmpty(email)) {
129            return null;
130        }
131        return ContactStatusLoader.load(mContext, email).mPhoto;
132    }
133
134    private Bitmap[] getNotificationBitmaps(Bitmap senderPhoto) {
135        // TODO Should we cache these objects?  (bitmaps and arrays)
136        // They don't have to be on this process's memory once we post a notification request to
137        // the system, and decodeResource() seems to be reasonably fast.  We don't want them to
138        // take up memory when not necessary.
139        Bitmap appIcon = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.icon);
140        if (senderPhoto == null) {
141            return new Bitmap[] {appIcon};
142        } else {
143            return new Bitmap[] {senderPhoto, appIcon};
144        }
145    }
146
147    /**
148     * Create a notification
149     *
150     * Don't call it on the UI thread.
151     *
152     * TODO Test it when the UI is settled.
153     */
154    private Notification createNewMessageNotification(long accountId, int unseenMessageCount,
155            int justFetchedCount) {
156        final Account account = Account.restoreAccountWithId(mContext, accountId);
157        if (account == null) {
158            return null;
159        }
160        // Get the latest message
161        final Message message = Message.getLatestMessage(mContext, accountId);
162        if (message == null) {
163            return null; // no message found???
164        }
165
166        final String senderName = Address.toFriendly(Address.unpack(message.mFrom));
167        final String subject = message.mSubject;
168        final Bitmap senderPhoto = getSenderPhoto(message);
169
170        // Intent to open inbox
171        PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0,
172                Welcome.createOpenAccountInboxIntent(mContext, accountId),
173                PendingIntent.FLAG_UPDATE_CURRENT);
174
175        final String notificationTitle;
176        if (justFetchedCount == 1) {
177            notificationTitle = senderName;
178        } else {
179            notificationTitle = mContext.getResources().getQuantityString(
180                    R.plurals.notification_sender_name_multi_messages, justFetchedCount - 1,
181                    senderName, justFetchedCount - 1);
182        }
183        final String content = subject;
184        final String numNewMessages;
185        final int numAccounts = EmailContent.count(mContext, Account.CONTENT_URI);
186        if (numAccounts == 1) {
187            numNewMessages = mContext.getResources().getQuantityString(
188                    R.plurals.notification_num_new_messages_single_account, unseenMessageCount,
189                    unseenMessageCount, account.mDisplayName);
190        } else {
191            numNewMessages = mContext.getResources().getQuantityString(
192                    R.plurals.notification_num_new_messages_multi_account, unseenMessageCount,
193                    unseenMessageCount, account.mDisplayName);
194        }
195
196        Notification notification = new Notification(R.drawable.stat_notify_email_generic,
197                mContext.getString(R.string.notification_new_title), System.currentTimeMillis());
198        notification.setLatestEventInfo(mContext, notificationTitle, subject, contentIntent);
199
200        notification.tickerTitle = notificationTitle;
201        // STOPSHIPO numNewMessages should be the 3rd line on expanded notification.  But it's not
202        // clear how to do that yet.
203        // For now we just append it to subject.
204        notification.tickerSubtitle = subject + "  " + numNewMessages;
205        notification.tickerIcons = getNotificationBitmaps(senderPhoto);
206
207        setupNotificationSoundAndVibrationFromAccount(notification, account);
208        return notification;
209    }
210
211    private boolean isRingerModeSilent() {
212        return mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
213    }
214
215    private void setupNotificationSoundAndVibrationFromAccount(Notification notification,
216            Account account) {
217        final int flags = account.mFlags;
218        final String ringtoneUri = account.mRingtoneUri;
219        final boolean vibrate = (flags & Account.FLAGS_VIBRATE_ALWAYS) != 0;
220        final boolean vibrateWhenSilent = (flags & Account.FLAGS_VIBRATE_WHEN_SILENT) != 0;
221
222        notification.sound = (ringtoneUri == null) ? null : Uri.parse(ringtoneUri);
223
224        if (vibrate || (vibrateWhenSilent && isRingerModeSilent())) {
225            notification.defaults |= Notification.DEFAULT_VIBRATE;
226        }
227
228        // This code is identical to that used by Gmail and GTalk for notifications
229        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
230        notification.defaults |= Notification.DEFAULT_LIGHTS;
231    }
232
233    /**
234     * Generic warning notification
235     */
236    public void showWarningNotification(int id, String tickerText, String notificationText,
237            Intent intent) {
238        PendingIntent pendingIntent = null;
239        if (intent != null) {
240            pendingIntent = PendingIntent.getActivity(mContext, 0, intent,
241                    PendingIntent.FLAG_UPDATE_CURRENT);
242        }
243        Notification n = new Notification(android.R.drawable.stat_notify_error, tickerText,
244                System.currentTimeMillis());
245        n.setLatestEventInfo(mContext, tickerText, notificationText, pendingIntent);
246        n.flags = Notification.FLAG_AUTO_CANCEL;
247        mNotificationManager.notify(id, n);
248    }
249
250    /**
251     * Alert the user that an attachment couldn't be forwarded.  This is a very unusual case, and
252     * perhaps we shouldn't even send a notification. For now, it's helpful for debugging.
253     */
254    public void showDownloadForwardFailedNotification(Attachment att) {
255        showWarningNotification(NOTIFICATION_ID_ATTACHMENT_WARNING,
256                mContext.getString(R.string.forward_download_failed_ticker),
257                mContext.getString(R.string.forward_download_failed_notification,
258                        att.mFileName), null);
259    }
260
261    /**
262     * Alert the user that login failed for the specified account
263     */
264    private int getLoginFailedNotificationId(long accountId) {
265        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
266    }
267
268    // NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
269    public void showLoginFailedNotification(long accountId) {
270        final Account account = Account.restoreAccountWithId(mContext, accountId);
271        if (account == null) return;
272        showWarningNotification(getLoginFailedNotificationId(accountId),
273                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
274                mContext.getString(R.string.login_failed_notification),
275                AccountSettingsXL.createAccountSettingsIntent(mContext, accountId));
276    }
277
278    public void cancelLoginFailedNotification(long accountId) {
279        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
280    }
281}
282