• 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.dialer.calllog;
18 
19 import static android.Manifest.permission.READ_CALL_LOG;
20 import static android.Manifest.permission.READ_CONTACTS;
21 
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.app.PendingIntent;
25 import android.content.ContentResolver;
26 import android.content.ContentUris;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.res.Resources;
30 import android.database.Cursor;
31 import android.net.Uri;
32 import android.provider.CallLog.Calls;
33 import android.provider.ContactsContract.PhoneLookup;
34 import android.text.TextUtils;
35 import android.util.Log;
36 
37 import com.android.common.io.MoreCloseables;
38 import com.android.contacts.common.util.PermissionsUtil;
39 import com.android.dialer.DialtactsActivity;
40 import com.android.dialer.R;
41 import com.android.dialer.calllog.PhoneAccountUtils;
42 import com.android.dialer.list.ListsFragment;
43 import com.google.common.collect.Maps;
44 
45 import java.util.Map;
46 
47 /**
48  * VoicemailNotifier that shows a notification in the status bar.
49  */
50 public class DefaultVoicemailNotifier {
51     public static final String TAG = "DefaultVoicemailNotifier";
52 
53     /** The tag used to identify notifications from this class. */
54     private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
55     /** The identifier of the notification of new voicemails. */
56     private static final int NOTIFICATION_ID = 1;
57 
58     /** The singleton instance of {@link DefaultVoicemailNotifier}. */
59     private static DefaultVoicemailNotifier sInstance;
60 
61     private final Context mContext;
62     private final NotificationManager mNotificationManager;
63     private final NewCallsQuery mNewCallsQuery;
64     private final NameLookupQuery mNameLookupQuery;
65 
66     /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
getInstance(Context context)67     public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
68         if (sInstance == null) {
69             NotificationManager notificationManager =
70                     (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
71             ContentResolver contentResolver = context.getContentResolver();
72             sInstance = new DefaultVoicemailNotifier(context, notificationManager,
73                     createNewCallsQuery(context, contentResolver),
74                     createNameLookupQuery(context, contentResolver));
75         }
76         return sInstance;
77     }
78 
DefaultVoicemailNotifier(Context context, NotificationManager notificationManager, NewCallsQuery newCallsQuery, NameLookupQuery nameLookupQuery)79     private DefaultVoicemailNotifier(Context context,
80             NotificationManager notificationManager, NewCallsQuery newCallsQuery,
81             NameLookupQuery nameLookupQuery) {
82         mContext = context;
83         mNotificationManager = notificationManager;
84         mNewCallsQuery = newCallsQuery;
85         mNameLookupQuery = nameLookupQuery;
86     }
87 
88     /**
89      * Updates the notification and notifies of the call with the given URI.
90      *
91      * Clears the notification if there are no new voicemails, and notifies if the given URI
92      * corresponds to a new voicemail.
93      *
94      * It is not safe to call this method from the main thread.
95      */
updateNotification(Uri newCallUri)96     public void updateNotification(Uri newCallUri) {
97         // Lookup the list of new voicemails to include in the notification.
98         // TODO: Move this into a service, to avoid holding the receiver up.
99         final NewCall[] newCalls = mNewCallsQuery.query();
100 
101         if (newCalls == null) {
102             // Query failed, just return.
103             return;
104         }
105 
106         if (newCalls.length == 0) {
107             // No voicemails to notify about: clear the notification.
108             clearNotification();
109             return;
110         }
111 
112         Resources resources = mContext.getResources();
113 
114         // This represents a list of names to include in the notification.
115         String callers = null;
116 
117         // Maps each number into a name: if a number is in the map, it has already left a more
118         // recent voicemail.
119         final Map<String, String> names = Maps.newHashMap();
120 
121         // Determine the call corresponding to the new voicemail we have to notify about.
122         NewCall callToNotify = null;
123 
124         // Iterate over the new voicemails to determine all the information above.
125         for (NewCall newCall : newCalls) {
126             // Check if we already know the name associated with this number.
127             String name = names.get(newCall.number);
128             if (name == null) {
129                 name = PhoneNumberDisplayUtil.getDisplayName(
130                         mContext,
131                         newCall.number,
132                         newCall.numberPresentation,
133                         /* isVoicemail */ false).toString();
134                 // If we cannot lookup the contact, use the number instead.
135                 if (TextUtils.isEmpty(name)) {
136                     // Look it up in the database.
137                     name = mNameLookupQuery.query(newCall.number);
138                     if (TextUtils.isEmpty(name)) {
139                         name = newCall.number;
140                     }
141                 }
142                 names.put(newCall.number, name);
143                 // This is a new caller. Add it to the back of the list of callers.
144                 if (TextUtils.isEmpty(callers)) {
145                     callers = name;
146                 } else {
147                     callers = resources.getString(
148                             R.string.notification_voicemail_callers_list, callers, name);
149                 }
150             }
151             // Check if this is the new call we need to notify about.
152             if (newCallUri != null && newCall.voicemailUri != null &&
153                     ContentUris.parseId(newCallUri) == ContentUris.parseId(newCall.voicemailUri)) {
154                 callToNotify = newCall;
155             }
156         }
157 
158         // If there is only one voicemail, set its transcription as the "long text".
159         String transcription = null;
160         if (newCalls.length == 1) {
161             transcription = newCalls[0].transcription;
162         }
163 
164         if (newCallUri != null && callToNotify == null) {
165             Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
166         }
167 
168         // Determine the title of the notification and the icon for it.
169         final String title = resources.getQuantityString(
170                 R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
171         // TODO: Use the photo of contact if all calls are from the same person.
172         final int icon = android.R.drawable.stat_notify_voicemail;
173 
174         Notification.Builder notificationBuilder = new Notification.Builder(mContext)
175                 .setSmallIcon(icon)
176                 .setContentTitle(title)
177                 .setContentText(callers)
178                 .setStyle(new Notification.BigTextStyle().bigText(transcription))
179                 .setColor(resources.getColor(R.color.dialer_theme_color))
180                 .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
181                 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
182                 .setAutoCancel(true);
183 
184         // Determine the intent to fire when the notification is clicked on.
185         final Intent contentIntent;
186         // Open the call log.
187         // TODO: Send to recents tab in Dialer instead.
188         contentIntent = new Intent(mContext, DialtactsActivity.class);
189         contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_VOICEMAIL);
190         notificationBuilder.setContentIntent(PendingIntent.getActivity(
191                 mContext, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT));
192 
193         // The text to show in the ticker, describing the new event.
194         if (callToNotify != null) {
195             notificationBuilder.setTicker(resources.getString(
196                     R.string.notification_new_voicemail_ticker, names.get(callToNotify.number)));
197         }
198 
199         mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
200     }
201 
202     /** Creates a pending intent that marks all new voicemails as old. */
createMarkNewVoicemailsAsOldIntent()203     private PendingIntent createMarkNewVoicemailsAsOldIntent() {
204         Intent intent = new Intent(mContext, CallLogNotificationsService.class);
205         intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD);
206         return PendingIntent.getService(mContext, 0, intent, 0);
207     }
208 
clearNotification()209     public void clearNotification() {
210         mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
211     }
212 
213     /** Information about a new voicemail. */
214     private static final class NewCall {
215         public final Uri callsUri;
216         public final Uri voicemailUri;
217         public final String number;
218         public final int numberPresentation;
219         public final String accountComponentName;
220         public final String accountId;
221         public final String transcription;
222 
NewCall( Uri callsUri, Uri voicemailUri, String number, int numberPresentation, String accountComponentName, String accountId, String transcription)223         public NewCall(
224                 Uri callsUri,
225                 Uri voicemailUri,
226                 String number,
227                 int numberPresentation,
228                 String accountComponentName,
229                 String accountId,
230                 String transcription) {
231             this.callsUri = callsUri;
232             this.voicemailUri = voicemailUri;
233             this.number = number;
234             this.numberPresentation = numberPresentation;
235             this.accountComponentName = accountComponentName;
236             this.accountId = accountId;
237             this.transcription = transcription;
238         }
239     }
240 
241     /** Allows determining the new calls for which a notification should be generated. */
242     public interface NewCallsQuery {
243         /**
244          * Returns the new calls for which a notification should be generated.
245          */
query()246         public NewCall[] query();
247     }
248 
249     /** Create a new instance of {@link NewCallsQuery}. */
createNewCallsQuery(Context context, ContentResolver contentResolver)250     public static NewCallsQuery createNewCallsQuery(Context context,
251             ContentResolver contentResolver) {
252         return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
253     }
254 
255     /**
256      * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
257      * notify about in the call log.
258      */
259     private static final class DefaultNewCallsQuery implements NewCallsQuery {
260         private static final String[] PROJECTION = {
261             Calls._ID,
262             Calls.NUMBER,
263             Calls.VOICEMAIL_URI,
264             Calls.NUMBER_PRESENTATION,
265             Calls.PHONE_ACCOUNT_COMPONENT_NAME,
266             Calls.PHONE_ACCOUNT_ID,
267             Calls.TRANSCRIPTION
268         };
269         private static final int ID_COLUMN_INDEX = 0;
270         private static final int NUMBER_COLUMN_INDEX = 1;
271         private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
272         private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
273         private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
274         private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
275         private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
276 
277         private final ContentResolver mContentResolver;
278         private final Context mContext;
279 
DefaultNewCallsQuery(Context context, ContentResolver contentResolver)280         private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
281             mContext = context;
282             mContentResolver = contentResolver;
283         }
284 
285         @Override
query()286         public NewCall[] query() {
287             if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
288                 Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
289                 return null;
290             }
291             final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
292             final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
293             Cursor cursor = null;
294             try {
295                 cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
296                         selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
297                 if (cursor == null) {
298                     return null;
299                 }
300                 NewCall[] newCalls = new NewCall[cursor.getCount()];
301                 while (cursor.moveToNext()) {
302                     newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
303                 }
304                 return newCalls;
305             } catch (RuntimeException e) {
306                 Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
307                 return null;
308             } finally {
309                 MoreCloseables.closeQuietly(cursor);
310             }
311         }
312 
313         /** Returns an instance of {@link NewCall} created by using the values of the cursor. */
createNewCallsFromCursor(Cursor cursor)314         private NewCall createNewCallsFromCursor(Cursor cursor) {
315             String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
316             Uri callsUri = ContentUris.withAppendedId(
317                     Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
318             Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
319             return new NewCall(
320                     callsUri,
321                     voicemailUri,
322                     cursor.getString(NUMBER_COLUMN_INDEX),
323                     cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
324                     cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
325                     cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
326                     cursor.getString(TRANSCRIPTION_COLUMN_INDEX));
327         }
328     }
329 
330     /** Allows determining the name associated with a given phone number. */
331     public interface NameLookupQuery {
332         /**
333          * Returns the name associated with the given number in the contacts database, or null if
334          * the number does not correspond to any of the contacts.
335          * <p>
336          * If there are multiple contacts with the same phone number, it will return the name of one
337          * of the matching contacts.
338          */
query(String number)339         public String query(String number);
340     }
341 
342     /** Create a new instance of {@link NameLookupQuery}. */
createNameLookupQuery(Context context, ContentResolver contentResolver)343     public static NameLookupQuery createNameLookupQuery(Context context,
344             ContentResolver contentResolver) {
345         return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
346     }
347 
348     /**
349      * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
350      * contacts database.
351      */
352     private static final class DefaultNameLookupQuery implements NameLookupQuery {
353         private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
354         private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
355 
356         private final ContentResolver mContentResolver;
357         private final Context mContext;
358 
DefaultNameLookupQuery(Context context, ContentResolver contentResolver)359         private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
360             mContext = context;
361             mContentResolver = contentResolver;
362         }
363 
364         @Override
query(String number)365         public String query(String number) {
366             if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
367                 Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
368                 return null;
369             }
370             Cursor cursor = null;
371             try {
372                 cursor = mContentResolver.query(
373                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
374                         PROJECTION, null, null, null);
375                 if (cursor == null || !cursor.moveToFirst()) return null;
376                 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
377             } catch (RuntimeException e) {
378                 Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
379                 return null;
380             } finally {
381                 if (cursor != null) {
382                     cursor.close();
383                 }
384             }
385         }
386     }
387 }
388