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