• 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 android.app.Notification;
20 import android.app.NotificationManager;
21 import android.app.PendingIntent;
22 import android.content.ContentResolver;
23 import android.content.ContentUris;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.res.Resources;
27 import android.database.Cursor;
28 import android.net.Uri;
29 import android.provider.CallLog.Calls;
30 import android.provider.ContactsContract.PhoneLookup;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import com.android.common.io.MoreCloseables;
35 import com.android.dialer.CallDetailActivity;
36 import com.android.dialer.R;
37 import com.google.common.collect.Maps;
38 
39 import java.util.Map;
40 
41 /**
42  * Implementation of {@link VoicemailNotifier} that shows a notification in the
43  * status bar.
44  */
45 public 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}. */
getInstance(Context context)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 
DefaultVoicemailNotifier(Context context, NotificationManager notificationManager, NewCallsQuery newCallsQuery, NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper)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
updateNotification(Uri newCallUri)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. */
createMarkNewVoicemailsAsOldIntent()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
clearNotification()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 
NewCall(Uri callsUri, Uri voicemailUri, String number)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          */
query()226         public NewCall[] query();
227     }
228 
229     /** Create a new instance of {@link NewCallsQuery}. */
createNewCallsQuery(ContentResolver contentResolver)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 
DefaultNewCallsQuery(ContentResolver contentResolver)248         private DefaultNewCallsQuery(ContentResolver contentResolver) {
249             mContentResolver = contentResolver;
250         }
251 
252         @Override
query()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. */
createNewCallsFromCursor(Cursor 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          */
query(String number)292         public String query(String number);
293     }
294 
295     /** Create a new instance of {@link NameLookupQuery}. */
createNameLookupQuery(ContentResolver contentResolver)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 
DefaultNameLookupQuery(ContentResolver contentResolver)310         private DefaultNameLookupQuery(ContentResolver contentResolver) {
311             mContentResolver = contentResolver;
312         }
313 
314         @Override
query(String number)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      */
createPhoneNumberHelper(Context context)337     public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
338         return new PhoneNumberHelper(context.getResources());
339     }
340 }
341