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