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