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