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.app.calllog; 18 19 import android.annotation.TargetApi; 20 import android.app.Notification; 21 import android.app.NotificationManager; 22 import android.app.PendingIntent; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.Resources; 26 import android.graphics.Bitmap; 27 import android.net.Uri; 28 import android.os.Build.VERSION; 29 import android.os.Build.VERSION_CODES; 30 import android.os.PersistableBundle; 31 import android.support.annotation.NonNull; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.VisibleForTesting; 34 import android.support.annotation.WorkerThread; 35 import android.support.v4.os.BuildCompat; 36 import android.support.v4.util.Pair; 37 import android.telecom.PhoneAccount; 38 import android.telecom.PhoneAccountHandle; 39 import android.telecom.TelecomManager; 40 import android.telephony.CarrierConfigManager; 41 import android.telephony.PhoneNumberUtils; 42 import android.telephony.TelephonyManager; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 import com.android.contacts.common.compat.TelephonyManagerCompat; 46 import com.android.contacts.common.util.ContactDisplayUtils; 47 import com.android.dialer.app.DialtactsActivity; 48 import com.android.dialer.app.R; 49 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; 50 import com.android.dialer.app.contactinfo.ContactPhotoLoader; 51 import com.android.dialer.app.list.DialtactsPagerAdapter; 52 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 53 import com.android.dialer.blocking.FilteredNumbersUtil; 54 import com.android.dialer.calllogutils.PhoneAccountUtils; 55 import com.android.dialer.common.Assert; 56 import com.android.dialer.common.LogUtil; 57 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 58 import com.android.dialer.common.concurrent.DialerExecutors; 59 import com.android.dialer.logging.DialerImpression; 60 import com.android.dialer.logging.Logger; 61 import com.android.dialer.notification.NotificationChannelManager; 62 import com.android.dialer.notification.NotificationChannelManager.Channel; 63 import com.android.dialer.phonenumbercache.ContactInfo; 64 import com.android.dialer.telecom.TelecomUtil; 65 import java.util.Iterator; 66 import java.util.List; 67 import java.util.Map; 68 69 /** Shows a voicemail notification in the status bar. */ 70 public class DefaultVoicemailNotifier implements Worker<Void, Void> { 71 72 public static final String TAG = "VoicemailNotifier"; 73 74 /** The tag used to identify notifications from this class. */ 75 static final String VISUAL_VOICEMAIL_NOTIFICATION_TAG = "DefaultVoicemailNotifier"; 76 /** The identifier of the notification of new voicemails. */ 77 private static final int VISUAL_VOICEMAIL_NOTIFICATION_ID = R.id.notification_visual_voicemail; 78 79 private static final int LEGACY_VOICEMAIL_NOTIFICATION_ID = R.id.notification_legacy_voicemail; 80 private static final String LEGACY_VOICEMAIL_NOTIFICATION_TAG = "legacy_voicemail"; 81 82 private final Context context; 83 private final CallLogNotificationsQueryHelper queryHelper; 84 private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler; 85 86 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) DefaultVoicemailNotifier( Context context, CallLogNotificationsQueryHelper queryHelper, FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler)87 DefaultVoicemailNotifier( 88 Context context, 89 CallLogNotificationsQueryHelper queryHelper, 90 FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { 91 this.context = context; 92 this.queryHelper = queryHelper; 93 this.filteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; 94 } 95 DefaultVoicemailNotifier(Context context)96 public DefaultVoicemailNotifier(Context context) { 97 this( 98 context, 99 CallLogNotificationsQueryHelper.getInstance(context), 100 new FilteredNumberAsyncQueryHandler(context)); 101 } 102 103 @Nullable 104 @Override doInBackground(@ullable Void input)105 public Void doInBackground(@Nullable Void input) throws Throwable { 106 updateNotification(); 107 return null; 108 } 109 110 /** 111 * Updates the notification and notifies of the call with the given URI. 112 * 113 * <p>Clears the notification if there are no new voicemails, and notifies if the given URI 114 * corresponds to a new voicemail. 115 * 116 * <p>It is not safe to call this method from the main thread. 117 */ 118 @VisibleForTesting 119 @WorkerThread updateNotification()120 void updateNotification() { 121 Assert.isWorkerThread(); 122 // Lookup the list of new voicemails to include in the notification. 123 final List<NewCall> newCalls = queryHelper.getNewVoicemails(); 124 125 if (newCalls == null) { 126 // Query failed, just return. 127 return; 128 } 129 130 Resources resources = context.getResources(); 131 132 // This represents a list of names to include in the notification. 133 String callers = null; 134 135 // Maps each number into a name: if a number is in the map, it has already left a more 136 // recent voicemail. 137 final Map<String, ContactInfo> contactInfos = new ArrayMap<>(); 138 139 // Iterate over the new voicemails to determine all the information above. 140 Iterator<NewCall> itr = newCalls.iterator(); 141 while (itr.hasNext()) { 142 NewCall newCall = itr.next(); 143 144 // Skip notifying for numbers which are blocked. 145 if (!FilteredNumbersUtil.hasRecentEmergencyCall(context) 146 && filteredNumberAsyncQueryHandler.getBlockedIdSynchronous( 147 newCall.number, newCall.countryIso) 148 != null) { 149 itr.remove(); 150 151 if (newCall.voicemailUri != null) { 152 // Delete the voicemail. 153 CallLogAsyncTaskUtil.deleteVoicemailSynchronous(context, newCall.voicemailUri); 154 } 155 continue; 156 } 157 158 // Check if we already know the name associated with this number. 159 ContactInfo contactInfo = contactInfos.get(newCall.number); 160 if (contactInfo == null) { 161 contactInfo = 162 queryHelper.getContactInfo( 163 newCall.number, newCall.numberPresentation, newCall.countryIso); 164 contactInfos.put(newCall.number, contactInfo); 165 // This is a new caller. Add it to the back of the list of callers. 166 if (TextUtils.isEmpty(callers)) { 167 callers = contactInfo.name; 168 } else { 169 callers = 170 resources.getString( 171 R.string.notification_voicemail_callers_list, callers, contactInfo.name); 172 } 173 } 174 } 175 176 if (newCalls.isEmpty()) { 177 // No voicemails to notify about 178 return; 179 } 180 181 Notification.Builder groupSummary = 182 createNotificationBuilder() 183 .setContentTitle( 184 resources.getQuantityString( 185 R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size())) 186 .setContentText(callers) 187 .setDeleteIntent(createMarkNewVoicemailsAsOldIntent(null)) 188 .setGroupSummary(true) 189 .setContentIntent(newVoicemailIntent(null)); 190 191 if (BuildCompat.isAtLeastO()) { 192 groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN); 193 } 194 195 NotificationChannelManager.applyChannel( 196 groupSummary, 197 context, 198 Channel.VOICEMAIL, 199 PhoneAccountHandles.getAccount(context, newCalls.get(0))); 200 201 LogUtil.i(TAG, "Creating visual voicemail notification"); 202 getNotificationManager() 203 .notify( 204 VISUAL_VOICEMAIL_NOTIFICATION_TAG, 205 VISUAL_VOICEMAIL_NOTIFICATION_ID, 206 groupSummary.build()); 207 208 for (NewCall voicemail : newCalls) { 209 getNotificationManager() 210 .notify( 211 voicemail.callsUri.toString(), 212 VISUAL_VOICEMAIL_NOTIFICATION_ID, 213 createNotificationForVoicemail(voicemail, contactInfos)); 214 } 215 } 216 217 /** 218 * Replicates how packages/services/Telephony/NotificationMgr.java handles legacy voicemail 219 * notification. The notification will not be stackable because no information is available for 220 * individual voicemails. 221 */ 222 @TargetApi(VERSION_CODES.O) notifyLegacyVoicemail( @onNull PhoneAccountHandle phoneAccountHandle, int count, String voicemailNumber, PendingIntent callVoicemailIntent, PendingIntent voicemailSettingIntent)223 public void notifyLegacyVoicemail( 224 @NonNull PhoneAccountHandle phoneAccountHandle, 225 int count, 226 String voicemailNumber, 227 PendingIntent callVoicemailIntent, 228 PendingIntent voicemailSettingIntent) { 229 Assert.isNotNull(phoneAccountHandle); 230 Assert.checkArgument(BuildCompat.isAtLeastO()); 231 TelephonyManager telephonyManager = 232 context 233 .getSystemService(TelephonyManager.class) 234 .createForPhoneAccountHandle(phoneAccountHandle); 235 Assert.isNotNull(telephonyManager); 236 LogUtil.i(TAG, "Creating legacy voicemail notification"); 237 238 PersistableBundle carrierConfig = telephonyManager.getCarrierConfig(); 239 240 String notificationTitle = 241 context 242 .getResources() 243 .getQuantityString(R.plurals.notification_voicemail_title, count, count); 244 245 TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 246 PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle); 247 248 String notificationText; 249 PendingIntent pendingIntent; 250 251 if (voicemailSettingIntent != null) { 252 // If the voicemail number if unknown, instead of calling voicemail, take the user 253 // to the voicemail settings. 254 notificationText = context.getString(R.string.notification_voicemail_no_vm_number); 255 pendingIntent = voicemailSettingIntent; 256 } else { 257 if (PhoneAccountUtils.getSubscriptionPhoneAccounts(context).size() > 1) { 258 notificationText = phoneAccount.getShortDescription().toString(); 259 } else { 260 notificationText = 261 String.format( 262 context.getString(R.string.notification_voicemail_text_format), 263 PhoneNumberUtils.formatNumber(voicemailNumber)); 264 } 265 pendingIntent = callVoicemailIntent; 266 } 267 Notification.Builder builder = new Notification.Builder(context); 268 builder 269 .setSmallIcon(android.R.drawable.stat_notify_voicemail) 270 .setColor(context.getColor(R.color.dialer_theme_color)) 271 .setWhen(System.currentTimeMillis()) 272 .setContentTitle(notificationTitle) 273 .setContentText(notificationText) 274 .setContentIntent(pendingIntent) 275 .setSound(telephonyManager.getVoicemailRingtoneUri(phoneAccountHandle)) 276 .setOngoing( 277 carrierConfig.getBoolean( 278 CarrierConfigManager.KEY_VOICEMAIL_NOTIFICATION_PERSISTENT_BOOL)); 279 280 if (telephonyManager.isVoicemailVibrationEnabled(phoneAccountHandle)) { 281 builder.setDefaults(Notification.DEFAULT_VIBRATE); 282 } 283 284 NotificationChannelManager.applyChannel( 285 builder, context, Channel.VOICEMAIL, phoneAccountHandle); 286 Notification notification = builder.build(); 287 getNotificationManager() 288 .notify(LEGACY_VOICEMAIL_NOTIFICATION_TAG, LEGACY_VOICEMAIL_NOTIFICATION_ID, notification); 289 } 290 cancelLegacyNotification()291 public void cancelLegacyNotification() { 292 LogUtil.i(TAG, "Clearing legacy voicemail notification"); 293 getNotificationManager() 294 .cancel(LEGACY_VOICEMAIL_NOTIFICATION_TAG, LEGACY_VOICEMAIL_NOTIFICATION_ID); 295 } 296 297 /** 298 * Determines which ringtone Uri and Notification defaults to use when updating the notification 299 * for the given call. 300 */ getNotificationInfo(@ullable NewCall callToNotify)301 private Pair<Uri, Integer> getNotificationInfo(@Nullable NewCall callToNotify) { 302 LogUtil.v(TAG, "getNotificationInfo"); 303 if (callToNotify == null) { 304 LogUtil.i(TAG, "callToNotify == null"); 305 return new Pair<>(null, 0); 306 } 307 PhoneAccountHandle accountHandle = PhoneAccountHandles.getAccount(context, callToNotify); 308 if (accountHandle == null) { 309 LogUtil.i(TAG, "No default phone account found, using default notification ringtone"); 310 return new Pair<>(null, Notification.DEFAULT_ALL); 311 } 312 return new Pair<>( 313 TelephonyManagerCompat.getVoicemailRingtoneUri(getTelephonyManager(), accountHandle), 314 getNotificationDefaults(accountHandle)); 315 } 316 getNotificationDefaults(PhoneAccountHandle accountHandle)317 private int getNotificationDefaults(PhoneAccountHandle accountHandle) { 318 if (VERSION.SDK_INT >= VERSION_CODES.N) { 319 return TelephonyManagerCompat.isVoicemailVibrationEnabled( 320 getTelephonyManager(), accountHandle) 321 ? Notification.DEFAULT_VIBRATE 322 : 0; 323 } 324 return Notification.DEFAULT_ALL; 325 } 326 327 /** Creates a pending intent that marks all new voicemails as old. */ createMarkNewVoicemailsAsOldIntent(@ullable Uri voicemailUri)328 private PendingIntent createMarkNewVoicemailsAsOldIntent(@Nullable Uri voicemailUri) { 329 Intent intent = new Intent(context, CallLogNotificationsService.class); 330 intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_VOICEMAILS_AS_OLD); 331 intent.setData(voicemailUri); 332 return PendingIntent.getService(context, 0, intent, 0); 333 } 334 getNotificationManager()335 private NotificationManager getNotificationManager() { 336 return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 337 } 338 getTelephonyManager()339 private TelephonyManager getTelephonyManager() { 340 return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 341 } 342 createNotificationForVoicemail( @onNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos)343 private Notification createNotificationForVoicemail( 344 @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos) { 345 Pair<Uri, Integer> notificationInfo = getNotificationInfo(voicemail); 346 ContactInfo contactInfo = contactInfos.get(voicemail.number); 347 348 Notification.Builder notificationBuilder = 349 createNotificationBuilder() 350 .setContentTitle( 351 context 352 .getResources() 353 .getQuantityString(R.plurals.notification_voicemail_title, 1, 1)) 354 .setContentText( 355 ContactDisplayUtils.getTtsSpannedPhoneNumber( 356 context.getResources(), 357 R.string.notification_new_voicemail_ticker, 358 contactInfo.name)) 359 .setWhen(voicemail.dateMs) 360 .setSound(notificationInfo.first) 361 .setDefaults(notificationInfo.second); 362 363 if (voicemail.voicemailUri != null) { 364 notificationBuilder.setDeleteIntent( 365 createMarkNewVoicemailsAsOldIntent(voicemail.voicemailUri)); 366 } 367 368 NotificationChannelManager.applyChannel( 369 notificationBuilder, 370 context, 371 Channel.VOICEMAIL, 372 PhoneAccountHandles.getAccount(context, voicemail)); 373 374 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 375 Bitmap photoIcon = loader.loadPhotoIcon(); 376 if (photoIcon != null) { 377 notificationBuilder.setLargeIcon(photoIcon); 378 } 379 if (!TextUtils.isEmpty(voicemail.transcription)) { 380 Logger.get(context) 381 .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION); 382 notificationBuilder.setStyle( 383 new Notification.BigTextStyle().bigText(voicemail.transcription)); 384 } 385 notificationBuilder.setContentIntent(newVoicemailIntent(voicemail)); 386 Logger.get(context).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED); 387 return notificationBuilder.build(); 388 } 389 createNotificationBuilder()390 private Notification.Builder createNotificationBuilder() { 391 return new Notification.Builder(context) 392 .setSmallIcon(android.R.drawable.stat_notify_voicemail) 393 .setColor(context.getColor(R.color.dialer_theme_color)) 394 .setGroup(VISUAL_VOICEMAIL_NOTIFICATION_TAG) 395 .setOnlyAlertOnce(true) 396 .setAutoCancel(true); 397 } 398 newVoicemailIntent(@ullable NewCall voicemail)399 private PendingIntent newVoicemailIntent(@Nullable NewCall voicemail) { 400 Intent intent = 401 DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); 402 // TODO (b/35486204): scroll to this voicemail 403 if (voicemail != null) { 404 intent.setData(voicemail.voicemailUri); 405 } 406 intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); 407 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 408 } 409 410 /** 411 * Updates the voicemail notifications displayed. 412 * 413 * @param runnable Called when the async update task completes no matter if it succeeds or fails. 414 * May be null. 415 */ updateVoicemailNotifications(Context context, Runnable runnable)416 static void updateVoicemailNotifications(Context context, Runnable runnable) { 417 if (!TelecomUtil.isDefaultDialer(context)) { 418 LogUtil.i( 419 "DefaultVoicemailNotifier.updateVoicemailNotifications", 420 "not default dialer, not scheduling update to voicemail notifications"); 421 return; 422 } 423 424 DialerExecutors.createNonUiTaskBuilder(new DefaultVoicemailNotifier(context)) 425 .onSuccess( 426 output -> { 427 LogUtil.i( 428 "DefaultVoicemailNotifier.updateVoicemailNotifications", 429 "update voicemail notifications successful"); 430 if (runnable != null) { 431 runnable.run(); 432 } 433 }) 434 .onFailure( 435 throwable -> { 436 LogUtil.i( 437 "DefaultVoicemailNotifier.updateVoicemailNotifications", 438 "update voicemail notifications failed"); 439 if (runnable != null) { 440 runnable.run(); 441 } 442 }) 443 .build() 444 .executeParallel(null); 445 } 446 } 447