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.text.TextUtils; 37 import android.util.Log; 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}. */ 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, 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 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. */ 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 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 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 */ 226 public NewCall[] query(); 227 } 228 229 /** Create a new instance of {@link NewCallsQuery}. */ 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 248 private DefaultNewCallsQuery(ContentResolver contentResolver) { 249 mContentResolver = contentResolver; 250 } 251 252 @Override 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. */ 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 */ 292 public String query(String number); 293 } 294 295 /** Create a new instance of {@link NameLookupQuery}. */ 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 310 private DefaultNameLookupQuery(ContentResolver contentResolver) { 311 mContentResolver = contentResolver; 312 } 313 314 @Override 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 */ 337 public static PhoneNumberHelper createPhoneNumberHelper(Context context) { 338 return new PhoneNumberHelper(context.getResources()); 339 } 340 } 341