DefaultVoicemailNotifier.java revision 9554500572ba82fbd7adb0a1637206ef870ef09e
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.dialer.calllog;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.content.ContentResolver;
23import android.content.ContentUris;
24import android.content.Context;
25import android.content.Intent;
26import android.content.res.Resources;
27import android.database.Cursor;
28import android.net.Uri;
29import android.provider.CallLog.Calls;
30import android.provider.ContactsContract.PhoneLookup;
31import android.text.TextUtils;
32import android.util.Log;
33
34import com.android.common.io.MoreCloseables;
35import com.android.dialer.CallDetailActivity;
36import com.android.dialer.R;
37import com.google.common.collect.Maps;
38
39import java.util.Map;
40
41/**
42 * Implementation of {@link VoicemailNotifier} that shows a notification in the
43 * status bar.
44 */
45public class DefaultVoicemailNotifier implements VoicemailNotifier {
46    public static final String TAG = "DefaultVoicemailNotifier";
47
48    /** The tag used to identify notifications from this class. */
49    private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
50    /** The identifier of the notification of new voicemails. */
51    private static final int NOTIFICATION_ID = 1;
52
53    /** The singleton instance of {@link DefaultVoicemailNotifier}. */
54    private static DefaultVoicemailNotifier sInstance;
55
56    private final Context mContext;
57    private final NotificationManager mNotificationManager;
58    private final NewCallsQuery mNewCallsQuery;
59    private final NameLookupQuery mNameLookupQuery;
60    private final PhoneNumberHelper mPhoneNumberHelper;
61
62    /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
63    public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
64        if (sInstance == null) {
65            NotificationManager notificationManager =
66                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
67            ContentResolver contentResolver = context.getContentResolver();
68            sInstance = new DefaultVoicemailNotifier(context, notificationManager,
69                    createNewCallsQuery(contentResolver),
70                    createNameLookupQuery(contentResolver),
71                    createPhoneNumberHelper(context));
72        }
73        return sInstance;
74    }
75
76    private DefaultVoicemailNotifier(Context context,
77            NotificationManager notificationManager, NewCallsQuery newCallsQuery,
78            NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper) {
79        mContext = context;
80        mNotificationManager = notificationManager;
81        mNewCallsQuery = newCallsQuery;
82        mNameLookupQuery = nameLookupQuery;
83        mPhoneNumberHelper = phoneNumberHelper;
84    }
85
86    /** Updates the notification and notifies of the call with the given URI. */
87    @Override
88    public void updateNotification(Uri newCallUri) {
89        // Lookup the list of new voicemails to include in the notification.
90        // TODO: Move this into a service, to avoid holding the receiver up.
91        final NewCall[] newCalls = mNewCallsQuery.query();
92
93        if (newCalls == null) {
94            // Query failed, just return.
95            return;
96        }
97
98        if (newCalls.length == 0) {
99            // No voicemails to notify about: clear the notification.
100            clearNotification();
101            return;
102        }
103
104        Resources resources = mContext.getResources();
105
106        // This represents a list of names to include in the notification.
107        String callers = null;
108
109        // Maps each number into a name: if a number is in the map, it has already left a more
110        // recent voicemail.
111        final Map<String, String> names = Maps.newHashMap();
112
113        // Determine the call corresponding to the new voicemail we have to notify about.
114        NewCall callToNotify = null;
115
116        // Iterate over the new voicemails to determine all the information above.
117        for (NewCall newCall : newCalls) {
118            // Check if we already know the name associated with this number.
119            String name = names.get(newCall.number);
120            if (name == null) {
121                // Look it up in the database.
122                name = mNameLookupQuery.query(newCall.number);
123                // If we cannot lookup the contact, use the number instead.
124                if (name == null) {
125                    name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString();
126                    if (TextUtils.isEmpty(name)) {
127                        name = newCall.number;
128                    }
129                }
130                names.put(newCall.number, name);
131                // This is a new caller. Add it to the back of the list of callers.
132                if (TextUtils.isEmpty(callers)) {
133                    callers = name;
134                } else {
135                    callers = resources.getString(
136                            R.string.notification_voicemail_callers_list, callers, name);
137                }
138            }
139            // Check if this is the new call we need to notify about.
140            if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) {
141                callToNotify = newCall;
142            }
143        }
144
145        if (newCallUri != null && callToNotify == null) {
146            Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
147        }
148
149        // Determine the title of the notification and the icon for it.
150        final String title = resources.getQuantityString(
151                R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
152        // TODO: Use the photo of contact if all calls are from the same person.
153        final int icon = android.R.drawable.stat_notify_voicemail;
154
155        Notification.Builder notificationBuilder = new Notification.Builder(mContext)
156                .setSmallIcon(icon)
157                .setContentTitle(title)
158                .setContentText(callers)
159                .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
160                .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
161                .setAutoCancel(true);
162
163        // Determine the intent to fire when the notification is clicked on.
164        final Intent contentIntent;
165        if (newCalls.length == 1) {
166            // Open the voicemail directly.
167            contentIntent = new Intent(mContext, CallDetailActivity.class);
168            contentIntent.setData(newCalls[0].callsUri);
169            contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
170                    newCalls[0].voicemailUri);
171            Intent playIntent = new Intent(mContext, CallDetailActivity.class);
172            playIntent.setData(newCalls[0].callsUri);
173            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
174                    newCalls[0].voicemailUri);
175            playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true);
176            playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true);
177            notificationBuilder.addAction(R.drawable.ic_play_holo_dark,
178                    resources.getString(R.string.notification_action_voicemail_play),
179                    PendingIntent.getActivity(mContext, 0, playIntent, 0));
180        } else {
181            // Open the call log.
182            contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
183        }
184        notificationBuilder.setContentIntent(
185                PendingIntent.getActivity(mContext, 0, contentIntent, 0));
186
187        // The text to show in the ticker, describing the new event.
188        if (callToNotify != null) {
189            notificationBuilder.setTicker(resources.getString(
190                    R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
191        }
192
193        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
194    }
195
196    /** Creates a pending intent that marks all new voicemails as old. */
197    private PendingIntent createMarkNewVoicemailsAsOldIntent() {
198        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
199        intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
200        return PendingIntent.getService(mContext, 0, intent, 0);
201    }
202
203    @Override
204    public void clearNotification() {
205        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
206    }
207
208    /** Information about a new voicemail. */
209    private static final class NewCall {
210        public final Uri callsUri;
211        public final Uri voicemailUri;
212        public final String number;
213
214        public NewCall(Uri callsUri, Uri voicemailUri, String number) {
215            this.callsUri = callsUri;
216            this.voicemailUri = voicemailUri;
217            this.number = number;
218        }
219    }
220
221    /** Allows determining the new calls for which a notification should be generated. */
222    public interface NewCallsQuery {
223        /**
224         * Returns the new calls for which a notification should be generated.
225         */
226        public NewCall[] query();
227    }
228
229    /** Create a new instance of {@link NewCallsQuery}. */
230    public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) {
231        return new DefaultNewCallsQuery(contentResolver);
232    }
233
234    /**
235     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
236     * notify about in the call log.
237     */
238    private static final class DefaultNewCallsQuery implements NewCallsQuery {
239        private static final String[] PROJECTION = {
240            Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI
241        };
242        private static final int ID_COLUMN_INDEX = 0;
243        private static final int NUMBER_COLUMN_INDEX = 1;
244        private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
245
246        private final ContentResolver mContentResolver;
247
248        private DefaultNewCallsQuery(ContentResolver contentResolver) {
249            mContentResolver = contentResolver;
250        }
251
252        @Override
253        public NewCall[] query() {
254            final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
255            final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
256            Cursor cursor = null;
257            try {
258                cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
259                        selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
260                if (cursor == null) {
261                    return null;
262                }
263                NewCall[] newCalls = new NewCall[cursor.getCount()];
264                while (cursor.moveToNext()) {
265                    newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
266                }
267                return newCalls;
268            } finally {
269                MoreCloseables.closeQuietly(cursor);
270            }
271        }
272
273        /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
274        private NewCall createNewCallsFromCursor(Cursor cursor) {
275            String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
276            Uri callsUri = ContentUris.withAppendedId(
277                    Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
278            Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
279            return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX));
280        }
281    }
282
283    /** Allows determining the name associated with a given phone number. */
284    public interface NameLookupQuery {
285        /**
286         * Returns the name associated with the given number in the contacts database, or null if
287         * the number does not correspond to any of the contacts.
288         * <p>
289         * If there are multiple contacts with the same phone number, it will return the name of one
290         * of the matching contacts.
291         */
292        public String query(String number);
293    }
294
295    /** Create a new instance of {@link NameLookupQuery}. */
296    public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) {
297        return new DefaultNameLookupQuery(contentResolver);
298    }
299
300    /**
301     * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
302     * contacts database.
303     */
304    private static final class DefaultNameLookupQuery implements NameLookupQuery {
305        private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
306        private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
307
308        private final ContentResolver mContentResolver;
309
310        private DefaultNameLookupQuery(ContentResolver contentResolver) {
311            mContentResolver = contentResolver;
312        }
313
314        @Override
315        public String query(String number) {
316            Cursor cursor = null;
317            try {
318                cursor = mContentResolver.query(
319                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
320                        PROJECTION, null, null, null);
321                if (cursor == null || !cursor.moveToFirst()) return null;
322                return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
323            } finally {
324                if (cursor != null) {
325                    cursor.close();
326                }
327            }
328        }
329    }
330
331    /**
332     * Create a new PhoneNumberHelper.
333     * <p>
334     * This will cause some Disk I/O, at least the first time it is created, so it should not be
335     * called from the main thread.
336     */
337    public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
338        return new PhoneNumberHelper(context.getResources());
339    }
340}
341