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 PhoneNumberDisplayHelper mPhoneNumberHelper; 61 62 /** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */ 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 76 private DefaultVoicemailNotifier(Context context, 77 NotificationManager notificationManager, NewCallsQuery newCallsQuery, 78 NameLookupQuery nameLookupQuery, PhoneNumberDisplayHelper 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 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 name = mPhoneNumberHelper.getDisplayName(newCall.number, 122 newCall.numberPresentation).toString(); 123 // If we cannot lookup the contact, use the number instead. 124 if (TextUtils.isEmpty(name)) { 125 // Look it up in the database. 126 name = mNameLookupQuery.query(newCall.number); 127 if (TextUtils.isEmpty(name)) { 128 name = newCall.number; 129 } 130 } 131 names.put(newCall.number, name); 132 // This is a new caller. Add it to the back of the list of callers. 133 if (TextUtils.isEmpty(callers)) { 134 callers = name; 135 } else { 136 callers = resources.getString( 137 R.string.notification_voicemail_callers_list, callers, name); 138 } 139 } 140 // Check if this is the new call we need to notify about. 141 if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) { 142 callToNotify = newCall; 143 } 144 } 145 146 if (newCallUri != null && callToNotify == null) { 147 Log.e(TAG, "The new call could not be found in the call log: " + newCallUri); 148 } 149 150 // Determine the title of the notification and the icon for it. 151 final String title = resources.getQuantityString( 152 R.plurals.notification_voicemail_title, newCalls.length, newCalls.length); 153 // TODO: Use the photo of contact if all calls are from the same person. 154 final int icon = android.R.drawable.stat_notify_voicemail; 155 156 Notification.Builder notificationBuilder = new Notification.Builder(mContext) 157 .setSmallIcon(icon) 158 .setContentTitle(title) 159 .setContentText(callers) 160 .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0) 161 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent()) 162 .setAutoCancel(true); 163 164 // Determine the intent to fire when the notification is clicked on. 165 final Intent contentIntent; 166 if (newCalls.length == 1) { 167 // Open the voicemail directly. 168 contentIntent = new Intent(mContext, CallDetailActivity.class); 169 contentIntent.setData(newCalls[0].callsUri); 170 contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, 171 newCalls[0].voicemailUri); 172 Intent playIntent = new Intent(mContext, CallDetailActivity.class); 173 playIntent.setData(newCalls[0].callsUri); 174 playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, 175 newCalls[0].voicemailUri); 176 playIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, true); 177 playIntent.putExtra(CallDetailActivity.EXTRA_FROM_NOTIFICATION, true); 178 notificationBuilder.addAction(R.drawable.ic_play_holo_dark, 179 resources.getString(R.string.notification_action_voicemail_play), 180 PendingIntent.getActivity(mContext, 0, playIntent, 0)); 181 } else { 182 // Open the call log. 183 contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI); 184 } 185 notificationBuilder.setContentIntent( 186 PendingIntent.getActivity(mContext, 0, contentIntent, 0)); 187 188 // The text to show in the ticker, describing the new event. 189 if (callToNotify != null) { 190 notificationBuilder.setTicker(resources.getString( 191 R.string.notification_new_voicemail_ticker, names.get(callToNotify.number))); 192 } 193 194 mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build()); 195 } 196 197 /** Creates a pending intent that marks all new voicemails as old. */ 198 private PendingIntent createMarkNewVoicemailsAsOldIntent() { 199 Intent intent = new Intent(mContext, CallLogNotificationsService.class); 200 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); 201 return PendingIntent.getService(mContext, 0, intent, 0); 202 } 203 204 @Override 205 public void clearNotification() { 206 mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID); 207 } 208 209 /** Information about a new voicemail. */ 210 private static final class NewCall { 211 public final Uri callsUri; 212 public final Uri voicemailUri; 213 public final String number; 214 public final int numberPresentation; 215 216 public NewCall(Uri callsUri, Uri voicemailUri, String number, 217 int numberPresentation) { 218 this.callsUri = callsUri; 219 this.voicemailUri = voicemailUri; 220 this.number = number; 221 this.numberPresentation = numberPresentation; 222 } 223 } 224 225 /** Allows determining the new calls for which a notification should be generated. */ 226 public interface NewCallsQuery { 227 /** 228 * Returns the new calls for which a notification should be generated. 229 */ 230 public NewCall[] query(); 231 } 232 233 /** Create a new instance of {@link NewCallsQuery}. */ 234 public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) { 235 return new DefaultNewCallsQuery(contentResolver); 236 } 237 238 /** 239 * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to 240 * notify about in the call log. 241 */ 242 private static final class DefaultNewCallsQuery implements NewCallsQuery { 243 private static final String[] PROJECTION = { 244 Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI, Calls.NUMBER_PRESENTATION 245 }; 246 private static final int ID_COLUMN_INDEX = 0; 247 private static final int NUMBER_COLUMN_INDEX = 1; 248 private static final int VOICEMAIL_URI_COLUMN_INDEX = 2; 249 private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3; 250 251 private final ContentResolver mContentResolver; 252 253 private DefaultNewCallsQuery(ContentResolver contentResolver) { 254 mContentResolver = contentResolver; 255 } 256 257 @Override 258 public NewCall[] query() { 259 final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE); 260 final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) }; 261 Cursor cursor = null; 262 try { 263 cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION, 264 selection, selectionArgs, Calls.DEFAULT_SORT_ORDER); 265 if (cursor == null) { 266 return null; 267 } 268 NewCall[] newCalls = new NewCall[cursor.getCount()]; 269 while (cursor.moveToNext()) { 270 newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor); 271 } 272 return newCalls; 273 } finally { 274 MoreCloseables.closeQuietly(cursor); 275 } 276 } 277 278 /** Returns an instance of {@link NewCall} created by using the values of the cursor. */ 279 private NewCall createNewCallsFromCursor(Cursor cursor) { 280 String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX); 281 Uri callsUri = ContentUris.withAppendedId( 282 Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX)); 283 Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString); 284 return new NewCall(callsUri, voicemailUri, cursor.getString(NUMBER_COLUMN_INDEX), 285 cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX)); 286 } 287 } 288 289 /** Allows determining the name associated with a given phone number. */ 290 public interface NameLookupQuery { 291 /** 292 * Returns the name associated with the given number in the contacts database, or null if 293 * the number does not correspond to any of the contacts. 294 * <p> 295 * If there are multiple contacts with the same phone number, it will return the name of one 296 * of the matching contacts. 297 */ 298 public String query(String number); 299 } 300 301 /** Create a new instance of {@link NameLookupQuery}. */ 302 public static NameLookupQuery createNameLookupQuery(ContentResolver contentResolver) { 303 return new DefaultNameLookupQuery(contentResolver); 304 } 305 306 /** 307 * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the 308 * contacts database. 309 */ 310 private static final class DefaultNameLookupQuery implements NameLookupQuery { 311 private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME }; 312 private static final int DISPLAY_NAME_COLUMN_INDEX = 0; 313 314 private final ContentResolver mContentResolver; 315 316 private DefaultNameLookupQuery(ContentResolver contentResolver) { 317 mContentResolver = contentResolver; 318 } 319 320 @Override 321 public String query(String number) { 322 Cursor cursor = null; 323 try { 324 cursor = mContentResolver.query( 325 Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)), 326 PROJECTION, null, null, null); 327 if (cursor == null || !cursor.moveToFirst()) return null; 328 return cursor.getString(DISPLAY_NAME_COLUMN_INDEX); 329 } finally { 330 if (cursor != null) { 331 cursor.close(); 332 } 333 } 334 } 335 } 336 337 /** 338 * Create a new PhoneNumberHelper. 339 * <p> 340 * This will cause some Disk I/O, at least the first time it is created, so it should not be 341 * called from the main thread. 342 */ 343 public static PhoneNumberDisplayHelper createPhoneNumberHelper(Context context) { 344 return new PhoneNumberDisplayHelper(context.getResources()); 345 } 346 } 347