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