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}. */ 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 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 */ 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. */ 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 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 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 */ 246 public NewCall[] query(); 247 } 248 249 /** Create a new instance of {@link NewCallsQuery}. */ 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 280 private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) { 281 mContext = context; 282 mContentResolver = contentResolver; 283 } 284 285 @Override 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. */ 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 */ 339 public String query(String number); 340 } 341 342 /** Create a new instance of {@link NameLookupQuery}. */ 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 359 private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) { 360 mContext = context; 361 mContentResolver = contentResolver; 362 } 363 364 @Override 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