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