1899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki/*
2899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * Copyright (C) 2010 The Android Open Source Project
3899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki *
4899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * Licensed under the Apache License, Version 2.0 (the "License");
5899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * you may not use this file except in compliance with the License.
6899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * You may obtain a copy of the License at
7899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki *
8899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki *      http://www.apache.org/licenses/LICENSE-2.0
9899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki *
10899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * Unless required by applicable law or agreed to in writing, software
11899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * distributed under the License is distributed on an "AS IS" BASIS,
12899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * See the License for the specific language governing permissions and
14899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * limitations under the License.
15899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki */
16899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
17899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukipackage com.android.email;
18899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
19899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.app.Notification;
20dfdc8b6da3cc074c97d53a597855aeaa3f039a68Todd Kennedyimport android.app.Notification.Builder;
21899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.app.NotificationManager;
22899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.app.PendingIntent;
23c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedyimport android.content.ContentResolver;
246e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport android.content.ContentUris;
25aca94265813e72e692eace527f43eb4c02b09c76Marc Blankimport android.content.ContentValues;
26899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.content.Context;
27d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blankimport android.content.Intent;
28f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.content.res.Resources;
29c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedyimport android.database.ContentObserver;
3083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedyimport android.database.Cursor;
31899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.graphics.Bitmap;
3274e094834c6efd3260c717090a89232a892c411bMakoto Onukiimport android.graphics.BitmapFactory;
33899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.media.AudioManager;
34899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.net.Uri;
35bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport android.os.Build;
36c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedyimport android.os.Handler;
3783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedyimport android.os.Looper;
3883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedyimport android.os.Process;
3974e094834c6efd3260c717090a89232a892c411bMakoto Onukiimport android.text.SpannableString;
40899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukiimport android.text.TextUtils;
41f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blankimport android.text.style.TextAppearanceSpan;
4283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedyimport android.util.Log;
43899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
446e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.email.activity.ContactStatusLoader;
45bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport com.android.email.activity.Welcome;
466e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.email.activity.setup.AccountSecurity;
476e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.email.activity.setup.AccountSettings;
486e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.Logging;
496e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.mail.Address;
506e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.Account;
516e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.EmailContent;
52bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport com.android.emailcommon.provider.EmailContent.AccountColumns;
536e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.EmailContent.Attachment;
546e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.EmailContent.MailboxColumns;
556e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.EmailContent.Message;
56bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrookimport com.android.emailcommon.provider.EmailContent.MessageColumns;
576e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.provider.Mailbox;
586e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.android.emailcommon.utility.Utility;
596e418aa41a17136be0dddb816d843428a0a1e722Marc Blankimport com.google.common.annotations.VisibleForTesting;
606e418aa41a17136be0dddb816d843428a0a1e722Marc Blank
61c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedyimport java.util.HashMap;
62e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedyimport java.util.HashSet;
63c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
64899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki/**
65899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki * Class that manages notifications.
66899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki */
67899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onukipublic class NotificationController {
68bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private static final int NOTIFICATION_ID_SECURITY_NEEDED = 1;
69958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    /** Reserved for {@link com.android.exchange.CalendarSyncEnabler} */
70958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @SuppressWarnings("unused")
71958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    private static final int NOTIFICATION_ID_EXCHANGE_CALENDAR_ADDED = 2;
72308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private static final int NOTIFICATION_ID_ATTACHMENT_WARNING = 3;
73308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private static final int NOTIFICATION_ID_PASSWORD_EXPIRING = 4;
74308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    private static final int NOTIFICATION_ID_PASSWORD_EXPIRED = 5;
75d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank
76d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    private static final int NOTIFICATION_ID_BASE_NEW_MESSAGES = 0x10000000;
77d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    private static final int NOTIFICATION_ID_BASE_LOGIN_WARNING = 0x20000000;
78899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
7983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    /** Selection to retrieve accounts that should we notify user for changes */
8083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    private final static String NOTIFIED_ACCOUNT_SELECTION =
8183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        Account.FLAGS + "&" + Account.FLAGS_NOTIFY_NEW_MAIL + " != 0";
8283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
83e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    private static NotificationThread sNotificationThread;
84e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    private static Handler sNotificationHandler;
85899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    private static NotificationController sInstance;
86899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    private final Context mContext;
87899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    private final NotificationManager mNotificationManager;
88899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    private final AudioManager mAudioManager;
89c1c3b6f21ecddbb237195caedc36af442ebf00e8Andy Stadler    private final Bitmap mGenericSenderIcon;
90f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo    private final Bitmap mGenericMultipleSenderIcon;
9174e094834c6efd3260c717090a89232a892c411bMakoto Onuki    private final Clock mClock;
92bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    // TODO We're maintaining all of our structures based upon the account ID. This is fine
93bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    // for now since the assumption is that we only ever look for changes in an account's
94bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    // INBOX. We should adjust our logic to use the mailbox ID instead.
95bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    /** Maps account id to the message data */
96aca94265813e72e692eace527f43eb4c02b09c76Marc Blank    private final HashMap<Long, ContentObserver> mNotificationMap;
97e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    private ContentObserver mAccountObserver;
98bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    /**
99bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Suspend notifications for this account. If {@link Account#NO_ACCOUNT}, no
100bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * account notifications are suspended. If {@link Account#ACCOUNT_ID_COMBINED_VIEW},
101bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * notifications for all accounts are suspended.
102bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     */
103bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private long mSuspendAccountId = Account.NO_ACCOUNT;
104899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
10523a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo    /**
10623a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * Timestamp indicating when the last message notification sound was played.
10723a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * Used for throttling.
10823a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     */
10923a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo    private long mLastMessageNotifyTime;
11023a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo
11123a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo    /**
11223a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * Minimum interval between notification sounds.
11323a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * Since a long sync (either on account setup or after a long period of being offline) can cause
11423a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * several notifications consecutively, it can be pretty overwhelming to get a barrage of
11523a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     * notification sounds. Throttle them using this value.
11623a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo     */
11748a3a1c51c11e3a752d7178e5c12c6caec842526Ben Komalo    private static final long MIN_SOUND_INTERVAL_MS = 15 * 1000; // 15 seconds
11823a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo
119bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private static boolean isRunningJellybeanOrLater() {
120bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
121bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    }
122bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
123899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    /** Constructor */
124958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @VisibleForTesting
125958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    NotificationController(Context context, Clock clock) {
126899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        mContext = context.getApplicationContext();
127899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        mNotificationManager = (NotificationManager) context.getSystemService(
128899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki                Context.NOTIFICATION_SERVICE);
129899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
130c1c3b6f21ecddbb237195caedc36af442ebf00e8Andy Stadler        mGenericSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
131c1c3b6f21ecddbb237195caedc36af442ebf00e8Andy Stadler                R.drawable.ic_contact_picture);
132f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo        mGenericMultipleSenderIcon = BitmapFactory.decodeResource(mContext.getResources(),
133f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo                R.drawable.ic_notification_multiple_mail_holo_dark);
13474e094834c6efd3260c717090a89232a892c411bMakoto Onuki        mClock = clock;
135aca94265813e72e692eace527f43eb4c02b09c76Marc Blank        mNotificationMap = new HashMap<Long, ContentObserver>();
136899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
137899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
138899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    /** Singleton access */
139899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    public static synchronized NotificationController getInstance(Context context) {
140899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        if (sInstance == null) {
14174e094834c6efd3260c717090a89232a892c411bMakoto Onuki            sInstance = new NotificationController(context, Clock.INSTANCE);
142899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
143899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        return sInstance;
144899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
145899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
146899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    /**
147d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank     * Return whether or not a notification, based on the passed-in id, needs to be "ongoing"
148d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank     * @param notificationId the notification id to check
149d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank     * @return whether or not the notification must be "ongoing"
150d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank     */
151d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank    private boolean needsOngoingNotification(int notificationId) {
152d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank        // "Security needed" must be ongoing so that the user doesn't close it; otherwise, sync will
153d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank        // be prevented until a reboot.  Consider also doing this for password expired.
154bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        return notificationId == NOTIFICATION_ID_SECURITY_NEEDED;
155d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank    }
156d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank
157d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank    /**
158bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Returns a {@link Notification.Builder} for an event with the given account. The account
159f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * contains specific rules on ring tone usage and these will be used to modify the notification
160958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * behaviour.
1611ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler     *
162bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * @param account The account this notification is being built for.
163958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
164958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param title The first line of text. May NOT be {@code null}.
165958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param contentText The second line of text. May NOT be {@code null}.
166958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param intent The intent to start if the user clicks on the notification.
167958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param largeIcon A large icon. May be {@code null}
168dfdc8b6da3cc074c97d53a597855aeaa3f039a68Todd Kennedy     * @param number A number to display using {@link Builder#setNumber(int)}. May
169958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     *        be {@code null}.
170c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy     * @param enableAudio If {@code false}, do not play any sound. Otherwise, play sound according
171c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy     *        to the settings for the given account.
172958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @return A {@link Notification} that can be sent to the notification service.
1731ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler     */
174bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private Notification.Builder createBaseAccountNotificationBuilder(Account account,
175bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            String ticker, CharSequence title, String contentText, Intent intent, Bitmap largeIcon,
176d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank            Integer number, boolean enableAudio, boolean ongoing) {
1771ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler        // Pending Intent
178c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler        PendingIntent pending = null;
179c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler        if (intent != null) {
180958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy            pending = PendingIntent.getActivity(
181958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                    mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
182c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler        }
1831ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler
184958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        // NOTE: the ticker is not shown for notifications in the Holo UX
185f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final Notification.Builder builder = new Notification.Builder(mContext)
186958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setContentTitle(title)
187958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setContentText(contentText)
188958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setContentIntent(pending)
189958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setLargeIcon(largeIcon)
190958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setNumber(number == null ? 0 : number)
191958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setSmallIcon(R.drawable.stat_notify_email_generic)
192958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                .setWhen(mClock.getTime())
193d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank                .setTicker(ticker)
194d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank                .setOngoing(ongoing);
195c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
196c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        if (enableAudio) {
197c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            setupSoundAndVibration(builder, account);
198c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        }
1991ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler
200f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        return builder;
201958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    }
2021ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler
203958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    /**
204958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Generic notifier for any account.  Uses notification rules from account.
205958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     *
206bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * @param account The account this notification is being built for.
207958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param ticker Text displayed when the notification is first shown. May be {@code null}.
208958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param title The first line of text. May NOT be {@code null}.
209958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param contentText The second line of text. May NOT be {@code null}.
210958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param intent The intent to start if the user clicks on the notification.
211958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * @param notificationId The ID of the notification to register with the service.
212958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     */
213bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private void showAccountNotification(Account account, String ticker, String title,
214958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy            String contentText, Intent intent, int notificationId) {
215bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        Notification.Builder builder = createBaseAccountNotificationBuilder(account, ticker, title,
216bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                contentText, intent, null, null, true, needsOngoingNotification(notificationId));
217f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        mNotificationManager.notify(notificationId, builder.getNotification());
2181ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler    }
2191ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler
2201ca111c19c83d54ad23bd8615d9c648e09ec3366Andy Stadler    /**
221958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Returns a notification ID for new message notifications for the given account.
222899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     */
223bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    private int getNewMessageNotificationId(long accountId) {
224958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        // We assume accountId will always be less than 0x0FFFFFFF; is there a better way?
225bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        return (int) (NOTIFICATION_ID_BASE_NEW_MESSAGES + accountId);
226899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
227899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
228899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    /**
22983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * Tells the notification controller if it should be watching for changes to the message table.
23083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * This is the main life cycle method for message notifications. When we stop observing
23183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * database changes, we save the state [e.g. message ID and count] of the most recent
23283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * notification shown to the user. And, when we start observing database changes, we restore
23383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * the saved state.
23483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * @param watch If {@code true}, we register observers for all accounts whose settings have
2355701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     *              notifications enabled. Otherwise, all observers are unregistered.
236899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     */
23783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    public void watchForMessages(final boolean watch) {
238bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (Email.DEBUG) {
239bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            Log.i(Logging.LOG_TAG, "Notifications being toggled: " + watch);
240eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo        }
24183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        // Don't create the thread if we're only going to stop watching
2425701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        if (!watch && sNotificationThread == null) return;
24383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
2445701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        ensureHandlerExists();
24583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        // Run this on the message notification handler
246e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        sNotificationHandler.post(new Runnable() {
24783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            @Override
24883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            public void run() {
24983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                ContentResolver resolver = mContext.getContentResolver();
25083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                if (!watch) {
2515701e0a555a5c263862156c1291aa13b06850425Todd Kennedy                    unregisterMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
252e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    if (mAccountObserver != null) {
253e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                        resolver.unregisterContentObserver(mAccountObserver);
254e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                        mAccountObserver = null;
255e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    }
25683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
25783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    // tear down the event loop
258e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    sNotificationThread.quit();
259e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    sNotificationThread = null;
26083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    return;
261c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy                }
26283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
26383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                // otherwise, start new observers for all notified accounts
2645701e0a555a5c263862156c1291aa13b06850425Todd Kennedy                registerMessageNotification(Account.ACCOUNT_ID_COMBINED_VIEW);
265e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                // If we're already observing account changes, don't do anything else
266e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                if (mAccountObserver == null) {
267bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    if (Email.DEBUG) {
268eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                        Log.i(Logging.LOG_TAG, "Observing account changes for notifications");
269eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                    }
270e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    mAccountObserver = new AccountContentObserver(sNotificationHandler, mContext);
271e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    resolver.registerContentObserver(Account.NOTIFIER_URI, true, mAccountObserver);
27283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                }
27383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
27483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        });
275899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
276899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
277899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    /**
278bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Temporarily suspend a single account from receiving notifications. NOTE: only a single
279bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * account may ever be suspended at a time. So, if this method is invoked a second time,
280bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * notifications for the previously suspended account will automatically be re-activated.
281bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * @param suspend If {@code true}, suspend notifications for the given account. Otherwise,
282bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     *              re-activate notifications for the previously suspended account.
283bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * @param accountId The ID of the account. If this is the special account ID
284bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     *              {@link Account#ACCOUNT_ID_COMBINED_VIEW},  notifications for all accounts are
285bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     *              suspended. If {@code suspend} is {@code false}, the account ID is ignored.
286bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     */
287bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    public void suspendMessageNotification(boolean suspend, long accountId) {
288bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (mSuspendAccountId != Account.NO_ACCOUNT) {
289bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            // we're already suspending an account; un-suspend it
290bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            mSuspendAccountId = Account.NO_ACCOUNT;
291bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        }
292bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (suspend && accountId != Account.NO_ACCOUNT && accountId > 0L) {
293bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            mSuspendAccountId = accountId;
294bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
295bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // Only go onto the notification handler if we really, absolutely need to
296bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                ensureHandlerExists();
297bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                sNotificationHandler.post(new Runnable() {
298bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    @Override
299bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    public void run() {
300bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        for (long accountId : mNotificationMap.keySet()) {
301bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            mNotificationManager.cancel(getNewMessageNotificationId(accountId));
302bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        }
303bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    }
304bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                });
305bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            } else {
306bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                mNotificationManager.cancel(getNewMessageNotificationId(accountId));
307bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
308bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        }
309bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    }
310bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
311bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    /**
3125701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     * Ensures the notification handler exists and is ready to handle requests.
3135701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     */
3145701e0a555a5c263862156c1291aa13b06850425Todd Kennedy    private static synchronized void ensureHandlerExists() {
3155701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        if (sNotificationThread == null) {
3165701e0a555a5c263862156c1291aa13b06850425Todd Kennedy            sNotificationThread = new NotificationThread();
3175701e0a555a5c263862156c1291aa13b06850425Todd Kennedy            sNotificationHandler = new Handler(sNotificationThread.getLooper());
3185701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        }
3195701e0a555a5c263862156c1291aa13b06850425Todd Kennedy    }
3205701e0a555a5c263862156c1291aa13b06850425Todd Kennedy
3215701e0a555a5c263862156c1291aa13b06850425Todd Kennedy    /**
322bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Registers an observer for changes to the INBOX for the given account. Since accounts
323bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * may only have a single INBOX, we will never have more than one observer for an account.
32483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * NOTE: This must be called on the notification handler thread.
32583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * @param accountId The ID of the account to register the observer for. May be
3265701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     *                  {@link Account#ACCOUNT_ID_COMBINED_VIEW} to register observers for all
3275701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     *                  accounts that allow for user notification.
328899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     */
32983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    private void registerMessageNotification(long accountId) {
33083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        ContentResolver resolver = mContext.getContentResolver();
3315701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
33283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            Cursor c = resolver.query(
33383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    Account.CONTENT_URI, EmailContent.ID_PROJECTION,
33483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    NOTIFIED_ACCOUNT_SELECTION, null, null);
33583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            try {
33683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                while (c.moveToNext()) {
33783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    long id = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
33883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    registerMessageNotification(id);
33983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                }
34083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            } finally {
34183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                c.close();
34283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
34383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        } else {
344aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            ContentObserver obs = mNotificationMap.get(accountId);
345aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            if (obs != null) return;  // we're already observing; nothing to do
346bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
347bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            Mailbox mailbox = Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX);
348bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (mailbox == null) {
349bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Log.w(Logging.LOG_TAG, "Could not load INBOX for account id: " + accountId);
350bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                return;
351bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
352bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (Email.DEBUG) {
353eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                Log.i(Logging.LOG_TAG, "Registering for notifications for account " + accountId);
354eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo            }
35583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            ContentObserver observer = new MessageContentObserver(
356bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    sNotificationHandler, mContext, mailbox.mId, accountId);
35783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            resolver.registerContentObserver(Message.NOTIFIER_URI, true, observer);
358aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            mNotificationMap.put(accountId, observer);
359e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // Now, ping the observer for any initial notifications
360aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            observer.onChange(true);
361c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        }
36283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    }
36383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
36483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    /**
36583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * Unregisters the observer for the given account. If the specified account does not have
366e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy     * a registered observer, no action is performed. This will not clear any existing notification
367e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy     * for the specified account. Use {@link NotificationManager#cancel(int)}.
36883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * NOTE: This must be called on the notification handler thread.
3695701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     * @param accountId The ID of the account to unregister from. To unregister all accounts that
3705701e0a555a5c263862156c1291aa13b06850425Todd Kennedy     *                  have observers, specify an ID of {@link Account#ACCOUNT_ID_COMBINED_VIEW}.
37183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     */
37283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    private void unregisterMessageNotification(long accountId) {
37383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        ContentResolver resolver = mContext.getContentResolver();
3745701e0a555a5c263862156c1291aa13b06850425Todd Kennedy        if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
375bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (Email.DEBUG) {
376eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                Log.i(Logging.LOG_TAG, "Unregistering notifications for all accounts");
377eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo            }
37883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            // cancel all existing message observers
379aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            for (ContentObserver observer : mNotificationMap.values()) {
38083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                resolver.unregisterContentObserver(observer);
38183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
38283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            mNotificationMap.clear();
383c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        } else {
384bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (Email.DEBUG) {
385eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                Log.i(Logging.LOG_TAG, "Unregistering notifications for account " + accountId);
386eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo            }
387aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            ContentObserver observer = mNotificationMap.remove(accountId);
388aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            if (observer != null) {
38983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                resolver.unregisterContentObserver(observer);
39083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
39183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        }
39283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    }
39383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
39483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    /**
395958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Returns a picture of the sender of the given message. If no picture is available, returns
396958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * {@code null}.
397899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     *
398958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
399899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     */
400899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    private Bitmap getSenderPhoto(Message message) {
401899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        Address sender = Address.unpackFirst(message.mFrom);
402899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        if (sender == null) {
403899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki            return null;
404899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
405899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        String email = sender.getAddress();
406899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        if (TextUtils.isEmpty(email)) {
407899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki            return null;
408899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
409f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        Bitmap photo = ContactStatusLoader.getContactInfo(mContext, email).mPhoto;
410f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
411f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        if (photo != null) {
412f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final Resources res = mContext.getResources();
413f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final int idealIconHeight =
414f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
415f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final int idealIconWidth =
416f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
417f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
418f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            if (photo.getHeight() < idealIconHeight) {
419f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // We should scale this image to fit the intended size
420f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                photo = Bitmap.createScaledBitmap(
421f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                        photo, idealIconWidth, idealIconHeight, true);
422f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            }
423f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
424f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        return photo;
425899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
426899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
427dd6e6b850462fd8617a73dd40b5e7d32f0aaf5edMakoto Onuki    /**
428958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Returns a "new message" notification for the given account.
429899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     *
430958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
431899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki     */
432958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @VisibleForTesting
433bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook    Notification createNewMessageNotification(long accountId, long mailboxId, Cursor messageCursor,
434bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            long newestMessageId, int unseenMessageCount, int unreadCount) {
435bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        final Account account = Account.restoreAccountWithId(mContext, accountId);
436899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        if (account == null) {
437899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki            return null;
438899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
439899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        // Get the latest message
440bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        final Message message = Message.restoreMessageWithId(mContext, newestMessageId);
441899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        if (message == null) {
442899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki            return null; // no message found???
443899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
444899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
44543c455eb266125a8e09a98e38d7639a865f52d42Makoto Onuki        String senderName = Address.toFriendly(Address.unpack(message.mFrom));
44643c455eb266125a8e09a98e38d7639a865f52d42Makoto Onuki        if (senderName == null) {
44743c455eb266125a8e09a98e38d7639a865f52d42Makoto Onuki            senderName = ""; // Happens when a message has no from.
44843c455eb266125a8e09a98e38d7639a865f52d42Makoto Onuki        }
4496f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        final boolean multipleUnseen = unseenMessageCount > 1;
450f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo        final Bitmap senderPhoto = multipleUnseen
451f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo                ? mGenericMultipleSenderIcon
452f13fee5d78e8975e05a7379eb7972282242c68b7Ben Komalo                : getSenderPhoto(message);
4536f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        final SpannableString title = getNewMessageTitle(senderName, unseenMessageCount);
4546f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        // TODO: add in display name on the second line for the text, once framework supports
4556f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        // multiline texts.
4566f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        final String text = multipleUnseen
457bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                ? account.mDisplayName
4586f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo                : message.mSubject;
459958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        final Bitmap largeIcon = senderPhoto != null ? senderPhoto : mGenericSenderIcon;
4606f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        final Integer number = unreadCount > 1 ? unreadCount : null;
461986881efe6029ad4382371e2da4e597312eda51cPaul Westbrook        final Intent intent;
462bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (unseenMessageCount > 1) {
463bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            intent = Welcome.createOpenAccountInboxIntent(mContext, accountId);
464986881efe6029ad4382371e2da4e597312eda51cPaul Westbrook        } else {
465bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            intent = Welcome.createOpenMessageIntent(
466d5f267ed250e7c3c97ca5e6ac1902340771d168cMark Wei                    mContext, accountId, mailboxId, newestMessageId);
467f4378923488abcb4422253cf2e7a9c9d244d1b96Todd Kennedy        }
46865432baf25d06b58a9b50d3df9952c4eedc9b8a5Paul Westbrook        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
46965432baf25d06b58a9b50d3df9952c4eedc9b8a5Paul Westbrook                Intent.FLAG_ACTIVITY_TASK_ON_HOME);
47023a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo        long now = mClock.getTime();
47148a3a1c51c11e3a752d7178e5c12c6caec842526Ben Komalo        boolean enableAudio = (now - mLastMessageNotifyTime) > MIN_SOUND_INTERVAL_MS;
472f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final Notification.Builder builder = createBaseAccountNotificationBuilder(
473bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                account, title.toString(), title, text,
474d9b2a8f237492951f71abeb8f2ce359311862f21Marc Blank                intent, largeIcon, number, enableAudio, false);
475bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (isRunningJellybeanOrLater()) {
476f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // For a new-style notification
477f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            if (multipleUnseen) {
478bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                if (messageCursor != null) {
479bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    final int maxNumDigestItems = mContext.getResources().getInteger(
480bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            R.integer.max_num_notification_digest_items);
481bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // The body of the notification is the account name, or the label name.
482bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    builder.setSubText(text);
483bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
484bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    Notification.InboxStyle digest = new Notification.InboxStyle(builder);
485bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
486bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    digest.setBigContentTitle(title);
487bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
488bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    int numDigestItems = 0;
489bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // We can assume that the current position of the cursor is on the
490bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // newest message
491bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    do {
492bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        final long messageId =
493bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                                messageCursor.getLong(EmailContent.ID_PROJECTION_COLUMN);
494bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
495bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        // Get the latest message
496bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        final Message digestMessage =
497bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                                Message.restoreMessageWithId(mContext, messageId);
498bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        if (digestMessage != null) {
499bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            final CharSequence digestLine =
500bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                                    getSingleMessageInboxLine(mContext, digestMessage);
501bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            digest.addLine(digestLine);
502bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            numDigestItems++;
503bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        }
504bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    } while (numDigestItems <= maxNumDigestItems && messageCursor.moveToNext());
505bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
506bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // We want to clear the content text in this case. The content text would have
507bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // been set in createBaseAccountNotificationBuilder, but since the same string
508bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // was set in as the subtext, we don't want to show a duplicate string.
509bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    builder.setContentText(null);
510f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                }
511f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            } else {
512f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // The notification content will be the subject of the conversation.
513f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                builder.setContentText(getSingleMessageLittleText(mContext, message.mSubject));
514f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
515f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // The notification subtext will be the subject of the conversation for inbox
516f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                // notifications, or will based on the the label name for user label notifications.
517f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                builder.setSubText(account.mDisplayName);
518f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
519f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                final Notification.BigTextStyle bigText = new Notification.BigTextStyle(builder);
520f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                bigText.bigText(getSingleMessageBigText(mContext, message));
521f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            }
522f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
523f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
52423a4b15e08bfe57be3242790d1a92db8dd4b9980Ben Komalo        mLastMessageNotifyTime = now;
525f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        return builder.getNotification();
526f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
527f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
528f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    /**
529f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * Sets the bigtext for a notification for a single new conversation
530f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param context
531f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param message New message that triggered the notification.
532f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @return a {@link CharSequence} suitable for use in {@link Notification.BigTextStyle}
533f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     */
534f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    private static CharSequence getSingleMessageInboxLine(Context context, Message message) {
535f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String subject = message.mSubject;
536f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String snippet = message.mSnippet;
537f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String senders = Address.toFriendly(Address.unpack(message.mFrom));
538f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
539f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String subjectSnippet = !TextUtils.isEmpty(subject) ? subject : snippet;
540f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
541f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final TextAppearanceSpan notificationPrimarySpan =
542f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                new TextAppearanceSpan(context, R.style.NotificationPrimaryText);
543f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
544f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        if (TextUtils.isEmpty(senders)) {
545f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // If the senders are empty, just use the subject/snippet.
546f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return subjectSnippet;
547f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
548f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        else if (TextUtils.isEmpty(subjectSnippet)) {
549f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // If the subject/snippet is empty, just use the senders.
550f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final SpannableString spannableString = new SpannableString(senders);
551f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            spannableString.setSpan(notificationPrimarySpan, 0, senders.length(), 0);
552f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
553f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return spannableString;
554f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        } else {
555f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final String formatString = context.getResources().getString(
556f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    R.string.multiple_new_message_notification_item);
557f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final TextAppearanceSpan notificationSecondarySpan =
558f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    new TextAppearanceSpan(context, R.style.NotificationSecondaryText);
559f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
560f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final String instantiatedString = String.format(formatString, senders, subjectSnippet);
561f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
562f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final SpannableString spannableString = new SpannableString(instantiatedString);
563f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
564f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final boolean isOrderReversed = formatString.indexOf("%2$s") <
565f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    formatString.indexOf("%1$s");
566f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final int primaryOffset =
567f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    (isOrderReversed ? instantiatedString.lastIndexOf(senders) :
568f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                     instantiatedString.indexOf(senders));
569f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final int secondaryOffset =
570f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    (isOrderReversed ? instantiatedString.lastIndexOf(subjectSnippet) :
571f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                     instantiatedString.indexOf(subjectSnippet));
572f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            spannableString.setSpan(notificationPrimarySpan,
573f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    primaryOffset, primaryOffset + senders.length(), 0);
574f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            spannableString.setSpan(notificationSecondarySpan,
575f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    secondaryOffset, secondaryOffset + subjectSnippet.length(), 0);
576f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return spannableString;
577f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
578f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
579f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
580f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    /**
581f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * Sets the bigtext for a notification for a single new conversation
582f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param context
583f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param subject Subject of the new message that triggered the notification
584f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @return a {@link CharSequence} suitable for use in {@link Notification.ContentText}
585f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     */
586f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    private static CharSequence getSingleMessageLittleText(Context context, String subject) {
587f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        if (subject == null) {
588f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return null;
589f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
590f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
591f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                context, R.style.NotificationPrimaryText);
592f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
593f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final SpannableString spannableString = new SpannableString(subject);
594f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
595f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
596f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        return spannableString;
597f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
598f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
599f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
600f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    /**
601f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * Sets the bigtext for a notification for a single new conversation
602f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param context
603f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @param message New message that triggered the notification
604f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     * @return a {@link CharSequence} suitable for use in {@link Notification.BigTextStyle}
605f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank     */
606f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    private static CharSequence getSingleMessageBigText(Context context, Message message) {
607f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final TextAppearanceSpan notificationSubjectSpan = new TextAppearanceSpan(
608f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                context, R.style.NotificationPrimaryText);
609f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
610f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String subject = message.mSubject;
611f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        final String snippet = message.mSnippet;
612f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
613f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        if (TextUtils.isEmpty(subject)) {
614f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // If the subject is empty, just use the snippet.
615f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return snippet;
616f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
617f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        else if (TextUtils.isEmpty(snippet)) {
618f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // If the snippet is empty, just use the subject.
619f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final SpannableString spannableString = new SpannableString(subject);
620f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            spannableString.setSpan(notificationSubjectSpan, 0, subject.length(), 0);
621f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
622f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return spannableString;
623f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        } else {
624f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final String notificationBigTextFormat = context.getResources().getString(
625f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    R.string.single_new_message_notification_big_text);
626f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
627f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // Localizers may change the order of the parameters, look at how the format
628f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            // string is structured.
629f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final boolean isSubjectFirst = notificationBigTextFormat.indexOf("%2$s") >
630f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    notificationBigTextFormat.indexOf("%1$s");
631f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final String bigText = String.format(notificationBigTextFormat, subject, snippet);
632f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final SpannableString spannableString = new SpannableString(bigText);
633f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
634f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            final int subjectOffset =
635f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    (isSubjectFirst ? bigText.indexOf(subject) : bigText.lastIndexOf(subject));
636f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            spannableString.setSpan(notificationSubjectSpan,
637f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank                    subjectOffset, subjectOffset + subject.length(), 0);
638f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
639f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank            return spannableString;
640f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank        }
641899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
642899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
64374e094834c6efd3260c717090a89232a892c411bMakoto Onuki    /**
6446f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo     * Creates a notification title for a new message. If there is only a single message,
6456f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo     * show the sender name. Otherwise, show "X new messages".
64674e094834c6efd3260c717090a89232a892c411bMakoto Onuki     */
647958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @VisibleForTesting
6486f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo    SpannableString getNewMessageTitle(String sender, int unseenCount) {
6496f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        String title;
6506f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        if (unseenCount > 1) {
6516f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo            title = String.format(
6526f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo                    mContext.getString(R.string.notification_multiple_new_messages_fmt),
6536f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo                    unseenCount);
65474e094834c6efd3260c717090a89232a892c411bMakoto Onuki        } else {
6556f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo            title = sender;
65674e094834c6efd3260c717090a89232a892c411bMakoto Onuki        }
6576f93d2edca7e699503089def8935042cedbbcdf4Ben Komalo        return new SpannableString(title);
65874e094834c6efd3260c717090a89232a892c411bMakoto Onuki    }
65974e094834c6efd3260c717090a89232a892c411bMakoto Onuki
660958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    /** Returns the system's current ringer mode */
661958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @VisibleForTesting
662958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    int getRingerMode() {
66374e094834c6efd3260c717090a89232a892c411bMakoto Onuki        return mAudioManager.getRingerMode();
66474e094834c6efd3260c717090a89232a892c411bMakoto Onuki    }
66574e094834c6efd3260c717090a89232a892c411bMakoto Onuki
666958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    /** Sets up the notification's sound and vibration based upon account details. */
667958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    @VisibleForTesting
668958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    void setupSoundAndVibration(Notification.Builder builder, Account account) {
669899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        final int flags = account.mFlags;
670899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        final String ringtoneUri = account.mRingtoneUri;
6713c0b8eeaeb194de986f8adf2fd882080938afd37Scott Kennedy        final boolean vibrate = (flags & Account.FLAGS_VIBRATE) != 0;
672899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
673958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        int defaults = Notification.DEFAULT_LIGHTS;
6743c0b8eeaeb194de986f8adf2fd882080938afd37Scott Kennedy        if (vibrate) {
675958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy            defaults |= Notification.DEFAULT_VIBRATE;
676899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki        }
677899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki
678978959e6199410419792e89fcea16413f56aeb12Scott Kennedy        builder.setSound(TextUtils.isEmpty(ringtoneUri) ? null : Uri.parse(ringtoneUri))
679958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy            .setDefaults(defaults);
680899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki    }
681d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank
682d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    /**
683958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Show (or update) a notification that the given attachment could not be forwarded. This
684958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * is a very unusual case, and perhaps we shouldn't even send a notification. For now,
685958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * it's helpful for debugging.
686958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     *
687c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
688d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank     */
689c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler    public void showDownloadForwardFailedNotification(Attachment attachment) {
690bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        final Account account = Account.restoreAccountWithId(mContext, attachment.mAccountKey);
691bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        if (account == null) return;
692bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        showAccountNotification(account,
693d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank                mContext.getString(R.string.forward_download_failed_ticker),
694c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                mContext.getString(R.string.forward_download_failed_title),
695c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                attachment.mFileName,
696c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                null,
697c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                NOTIFICATION_ID_ATTACHMENT_WARNING);
698d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    }
699d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank
700d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    /**
701958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Returns a notification ID for login failed notifications for the given account account.
702d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank     */
703d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    private int getLoginFailedNotificationId(long accountId) {
704d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank        return NOTIFICATION_ID_BASE_LOGIN_WARNING + (int)accountId;
705d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    }
706d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank
707c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler    /**
708958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Show (or update) a notification that there was a login failure for the given account.
709958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     *
710c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
711c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler     */
712d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    public void showLoginFailedNotification(long accountId) {
713d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank        final Account account = Account.restoreAccountWithId(mContext, accountId);
714d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank        if (account == null) return;
715bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        showAccountNotification(account,
716d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank                mContext.getString(R.string.login_failed_ticker, account.mDisplayName),
717c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                mContext.getString(R.string.login_failed_title),
718c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                account.getDisplayName(),
7192866284a6d4ee4b368fcaf412c540b21d457e065Ben Komalo                AccountSettings.createAccountSettingsIntent(mContext, accountId,
720bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        account.mDisplayName),
721c6d344ad2aab6bc46a87033af53d5a19a080e5f4Andy Stadler                getLoginFailedNotificationId(accountId));
722d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    }
723d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank
724958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy    /**
725958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Cancels the login failed notification for the given account.
726958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     */
727d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    public void cancelLoginFailedNotification(long accountId) {
728d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank        mNotificationManager.cancel(getLoginFailedNotificationId(accountId));
729d3e4f3ca7e43fb7ebaa140f93a44a1fb96a0577eMarc Blank    }
730308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
731308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    /**
732958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Show (or update) a notification that the user's password is expiring. The given account
733958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * is used to update the display text, but, all accounts share the same notification ID.
734308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     *
735958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
736308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     */
737308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    public void showPasswordExpiringNotification(long accountId) {
738308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        Account account = Account.restoreAccountWithId(mContext, accountId);
739308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        if (account == null) return;
740958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy
741308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
742308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki                accountId, false);
743958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String accountName = account.getDisplayName();
744958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String ticker =
745958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy            mContext.getString(R.string.password_expire_warning_ticker_fmt, accountName);
746958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String title = mContext.getString(R.string.password_expire_warning_content_title);
747bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        showAccountNotification(account, ticker, title, accountName, intent,
748308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki                NOTIFICATION_ID_PASSWORD_EXPIRING);
749308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    }
750308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
751308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    /**
752958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Show (or update) a notification that the user's password has expired. The given account
753958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * is used to update the display text, but, all accounts share the same notification ID.
754308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     *
755958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * NOTE: DO NOT CALL THIS METHOD FROM THE UI THREAD (DATABASE ACCESS)
756308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     */
757308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    public void showPasswordExpiredNotification(long accountId) {
758308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        Account account = Account.restoreAccountWithId(mContext, accountId);
759308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        if (account == null) return;
760958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy
761308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        Intent intent = AccountSecurity.actionDevicePasswordExpirationIntent(mContext,
762308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki                accountId, true);
763958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String accountName = account.getDisplayName();
764308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        String ticker = mContext.getString(R.string.password_expired_ticker);
765958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String title = mContext.getString(R.string.password_expired_content_title);
766bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        showAccountNotification(account, ticker, title, accountName, intent,
767958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy                NOTIFICATION_ID_PASSWORD_EXPIRED);
768308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    }
769308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
770308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    /**
771958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy     * Cancels any password expire notifications [both expired & expiring].
772308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     */
773308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    public void cancelPasswordExpirationNotifications() {
77483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRING);
77583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        mNotificationManager.cancel(NOTIFICATION_ID_PASSWORD_EXPIRED);
776308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    }
777308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki
778308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    /**
779bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Show (or update) a security needed notification. The given account is used to update
780bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * the display text, but, all accounts share the same notification ID.
781308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     */
782308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    public void showSecurityNeededNotification(Account account) {
783308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki        Intent intent = AccountSecurity.actionUpdateSecurityIntent(mContext, account.mId, true);
784958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String accountName = account.getDisplayName();
785958b15e8f30fd4e9eae1b05d48cb9a817326be6dTodd Kennedy        String ticker =
786bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            mContext.getString(R.string.security_notification_ticker_fmt, accountName);
787bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        String title = mContext.getString(R.string.security_notification_content_title);
788bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        showAccountNotification(account, ticker, title, accountName, intent,
789bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                NOTIFICATION_ID_SECURITY_NEEDED);
790f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    }
791f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank
792f419287f22ae44f25e1ba1f757ec33c7941bbfa8Marc Blank    /**
793bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook     * Cancels the security needed notification.
794308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki     */
795308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    public void cancelSecurityNeededNotification() {
796bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        mNotificationManager.cancel(NOTIFICATION_ID_SECURITY_NEEDED);
797308ce9284793b597797994dfb1fb25155cbe0b20Makoto Onuki    }
798c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
799c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy    /**
800c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy     * Observer invoked whenever a message we're notifying the user about changes.
801c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy     */
802c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy    private static class MessageContentObserver extends ContentObserver {
803bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        /** A selection to get messages the user hasn't seen before */
804bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        private final static String MESSAGE_SELECTION =
805bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                MessageColumns.MAILBOX_KEY + "=? AND "
806bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                + MessageColumns.ID + ">? AND "
807bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                + MessageColumns.FLAG_READ + "=0 AND "
808bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                + Message.FLAG_LOADED_SELECTION;
809c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        private final Context mContext;
810bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook        private final long mMailboxId;
81183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        private final long mAccountId;
812c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
81383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        public MessageContentObserver(
814bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Handler handler, Context context, long mailboxId, long accountId) {
81583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            super(handler);
816c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            mContext = context;
817bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            mMailboxId = mailboxId;
818c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            mAccountId = accountId;
819c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        }
820c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
821c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        @Override
822c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        public void onChange(boolean selfChange) {
823bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (mAccountId == sInstance.mSuspendAccountId
824bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    || sInstance.mSuspendAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
825bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                return;
826bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
827bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
828aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            ContentObserver observer = sInstance.mNotificationMap.get(mAccountId);
829bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (observer == null) {
830bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // Notification for a mailbox that we aren't observing; account is probably
831bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // being deleted.
832bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Log.w(Logging.LOG_TAG, "Received notification when observer data was null");
833bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                return;
834bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
835aca94265813e72e692eace527f43eb4c02b09c76Marc Blank            Account account = Account.restoreAccountWithId(mContext, mAccountId);
836bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (account == null) {
837eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                Log.w(Logging.LOG_TAG, "Couldn't find account for changed message notification");
838eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo                return;
839eb9dcfaf18fcc8c1bf0a51238fc6e13873e8334eBen Komalo            }
840bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            long oldMessageId = account.mNotifiedMessageId;
841bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            int oldMessageCount = account.mNotifiedMessageCount;
842c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
843c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            ContentResolver resolver = mContext.getContentResolver();
844bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            Long lastSeenMessageId = Utility.getFirstRowLong(
845bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
846bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    new String[] { MailboxColumns.LAST_SEEN_MESSAGE_KEY },
847bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    null, null, null, 0);
848bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (lastSeenMessageId == null) {
849bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // Mailbox got nuked. Could be that the account is in the process of being deleted
850bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Log.w(Logging.LOG_TAG, "Couldn't find mailbox for changed message notification");
851bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                return;
852bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
853bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
854bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            Cursor c = resolver.query(
855bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    Message.CONTENT_URI, EmailContent.ID_PROJECTION,
856bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    MESSAGE_SELECTION,
857bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    new String[] { Long.toString(mMailboxId), Long.toString(lastSeenMessageId) },
858bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    MessageColumns.ID + " DESC");
859bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            if (c == null) {
860bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // Couldn't find message info - things may be getting deleted in bulk.
861bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Log.w(Logging.LOG_TAG, "#onChange(); NULL response for message id query");
862bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                return;
863bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            }
864c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            try {
865bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                int newMessageCount = c.getCount();
866bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                long newMessageId = 0L;
867bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                if (c.moveToNext()) {
868bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    newMessageId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
869bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                }
870bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
871bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                if (newMessageCount == 0) {
872bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // No messages to notify for; clear the notification
873bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    int notificationId = sInstance.getNewMessageNotificationId(mAccountId);
874bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    sInstance.mNotificationManager.cancel(notificationId);
875bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                } else if (newMessageCount != oldMessageCount
876bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        || (newMessageId != 0 && newMessageId != oldMessageId)) {
877bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    // Either the count or last message has changed; update the notification
878bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    Integer unreadCount = Utility.getFirstRowInt(
879bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            mContext, ContentUris.withAppendedId(Mailbox.CONTENT_URI, mMailboxId),
880bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            new String[] { MailboxColumns.UNREAD_COUNT },
881bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            null, null, null, 0);
882bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    if (unreadCount == null) {
883bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        Log.w(Logging.LOG_TAG, "Couldn't find unread count for mailbox");
884bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        return;
885bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    }
886bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook
887bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    Notification n = sInstance.createNewMessageNotification(
888bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            mAccountId, mMailboxId, c, newMessageId,
889bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                            newMessageCount, unreadCount);
890bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    if (n != null) {
891bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        // Make the notification visible
892bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        sInstance.mNotificationManager.notify(
893bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                                sInstance.getNewMessageNotificationId(mAccountId), n);
894bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                    }
89583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                }
896bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                // Save away the new values
897bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                ContentValues cv = new ContentValues();
898bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                cv.put(AccountColumns.NOTIFIED_MESSAGE_ID, newMessageId);
899bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                cv.put(AccountColumns.NOTIFIED_MESSAGE_COUNT, newMessageCount);
900bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                resolver.update(ContentUris.withAppendedId(Account.CONTENT_URI, mAccountId), cv,
901bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                        null, null);
90283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            } finally {
90383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                c.close();
904c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy            }
905c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy        }
906c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy    }
907c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy
908e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    /**
909e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy     * Observer invoked whenever an account is modified. This could mean the user changed the
910e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy     * notification settings.
911e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy     */
912e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    private static class AccountContentObserver extends ContentObserver {
913e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        private final Context mContext;
914e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        public AccountContentObserver(Handler handler, Context context) {
915e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            super(handler);
916e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            mContext = context;
917e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        }
918e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy
919e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        @Override
920e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        public void onChange(boolean selfChange) {
921e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            final ContentResolver resolver = mContext.getContentResolver();
922bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook            final Cursor c = resolver.query(
923bc47398187c6ffd132435e51d8d61e6ec79a79dbPaul Westbrook                Account.CONTENT_URI, EmailContent.ID_PROJECTION,
924e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                NOTIFIED_ACCOUNT_SELECTION, null, null);
925e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            final HashSet<Long> newAccountList = new HashSet<Long>();
926e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            final HashSet<Long> removedAccountList = new HashSet<Long>();
927e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            if (c == null) {
928e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                // Suspender time ... theoretically, this will never happen
929e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                Log.wtf(Logging.LOG_TAG, "#onChange(); NULL response for account id query");
930e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                return;
931e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            }
932e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            try {
933e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                while (c.moveToNext()) {
934e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN);
935e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    newAccountList.add(accountId);
936e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                }
937e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            } finally {
938e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                if (c != null) {
939e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    c.close();
940e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                }
941e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            }
942e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // NOTE: Looping over three lists is not necessarily the most efficient. However, the
943e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // account lists are going to be very small, so, this will not be necessarily bad.
944e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // Cycle through existing notification list and adjust as necessary
945e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            for (long accountId : sInstance.mNotificationMap.keySet()) {
946e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                if (!newAccountList.remove(accountId)) {
947e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    // account id not in the current set of notifiable accounts
948e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                    removedAccountList.add(accountId);
949e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                }
950e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            }
951e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // A new account was added to the notification list
952e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            for (long accountId : newAccountList) {
953e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                sInstance.registerMessageNotification(accountId);
954e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            }
955e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            // An account was removed from the notification list
956e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            for (long accountId : removedAccountList) {
957e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                sInstance.unregisterMessageNotification(accountId);
958e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                int notificationId = sInstance.getNewMessageNotificationId(accountId);
959e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy                sInstance.mNotificationManager.cancel(notificationId);
960e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy            }
961e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy        }
962e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy    }
963e7fb4ac9e3b098ece98d004403a89652f88bbe7aTodd Kennedy
96483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    /**
96583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     * Thread to handle all notification actions through its own {@link Looper}.
96683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy     */
96783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy    private static class NotificationThread implements Runnable {
96883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        /** Lock to ensure proper initialization */
96983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        private final Object mLock = new Object();
97083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        /** The {@link Looper} that handles messages for this thread */
97183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        private Looper mLooper;
97283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
97383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        NotificationThread() {
97483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            new Thread(null, this, "EmailNotification").start();
97583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            synchronized (mLock) {
97683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                while (mLooper == null) {
97783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    try {
97883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                        mLock.wait();
97983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    } catch (InterruptedException ex) {
98083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                    }
98183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                }
98283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
98383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        }
98483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy
98583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        @Override
98683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        public void run() {
98783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            synchronized (mLock) {
98883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                Looper.prepare();
98983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                mLooper = Looper.myLooper();
99083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy                mLock.notifyAll();
99183693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            }
99283693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
99383693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            Looper.loop();
99483693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        }
99583693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        void quit() {
99683693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            mLooper.quit();
99783693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        }
99883693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        Looper getLooper() {
99983693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy            return mLooper;
100083693a6acaffd219f65e04054bc04530e9cc132fTodd Kennedy        }
1001c4cdb11d24c19428dd39f986b00c1a29e75e1505Todd Kennedy    }
1002899c5b866192a4c4a12413446d10e5d98dbf94faMakoto Onuki}
1003