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