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