1 /* 2 * Copyright (C) 2017 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.app.Notification; 20 import android.app.PendingIntent; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.graphics.Bitmap; 25 import android.net.Uri; 26 import android.os.Build.VERSION; 27 import android.os.Build.VERSION_CODES; 28 import android.support.annotation.NonNull; 29 import android.support.annotation.Nullable; 30 import android.support.v4.app.NotificationCompat; 31 import android.telecom.PhoneAccount; 32 import android.telecom.PhoneAccountHandle; 33 import android.telephony.TelephonyManager; 34 import android.text.TextUtils; 35 import com.android.contacts.common.util.ContactDisplayUtils; 36 import com.android.dialer.app.DialtactsActivity; 37 import com.android.dialer.app.MainComponent; 38 import com.android.dialer.app.R; 39 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall; 40 import com.android.dialer.app.contactinfo.ContactPhotoLoader; 41 import com.android.dialer.app.list.DialtactsPagerAdapter; 42 import com.android.dialer.common.LogUtil; 43 import com.android.dialer.compat.android.provider.VoicemailCompat; 44 import com.android.dialer.logging.DialerImpression; 45 import com.android.dialer.logging.Logger; 46 import com.android.dialer.notification.DialerNotificationManager; 47 import com.android.dialer.notification.NotificationChannelManager; 48 import com.android.dialer.notification.NotificationManagerUtils; 49 import com.android.dialer.phonenumbercache.ContactInfo; 50 import com.android.dialer.telecom.TelecomUtil; 51 import java.util.List; 52 import java.util.Map; 53 54 /** Shows a notification in the status bar for visual voicemail. */ 55 final class VisualVoicemailNotifier { 56 /** Prefix used to generate a unique tag for each voicemail notification. */ 57 static final String NOTIFICATION_TAG_PREFIX = "VisualVoicemail_"; 58 /** Common ID for all voicemail notifications. */ 59 static final int NOTIFICATION_ID = 1; 60 /** Tag for the group summary notification. */ 61 static final String GROUP_SUMMARY_NOTIFICATION_TAG = "GroupSummary_VisualVoicemail"; 62 /** 63 * Key used to associate all voicemail notifications and the summary as belonging to a single 64 * group. 65 */ 66 private static final String GROUP_KEY = "VisualVoicemailGroup"; 67 68 /** 69 * @param shouldAlert whether ringtone or vibration should be made when the notification is posted 70 * or updated. Should only be true when there is a real new voicemail. 71 */ showNotifications( @onNull Context context, @NonNull List<NewCall> newCalls, @NonNull Map<String, ContactInfo> contactInfos, @Nullable String callers, boolean shouldAlert)72 public static void showNotifications( 73 @NonNull Context context, 74 @NonNull List<NewCall> newCalls, 75 @NonNull Map<String, ContactInfo> contactInfos, 76 @Nullable String callers, 77 boolean shouldAlert) { 78 LogUtil.enterBlock("VisualVoicemailNotifier.showNotifications"); 79 PendingIntent deleteIntent = 80 CallLogNotificationsService.createMarkAllNewVoicemailsAsOldIntent(context); 81 String contentTitle = 82 context 83 .getResources() 84 .getQuantityString( 85 R.plurals.notification_voicemail_title, newCalls.size(), newCalls.size()); 86 NotificationCompat.Builder groupSummary = 87 createNotificationBuilder(context) 88 .setContentTitle(contentTitle) 89 .setContentText(callers) 90 .setDeleteIntent(deleteIntent) 91 .setGroupSummary(true) 92 .setContentIntent(newVoicemailIntent(context, null)); 93 94 if (VERSION.SDK_INT >= VERSION_CODES.O) { 95 if (shouldAlert) { 96 groupSummary.setOnlyAlertOnce(false); 97 // Group summary will alert when posted/updated 98 groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_ALL); 99 } else { 100 // Only children will alert. but since all children are set to "only alert summary" it is 101 // effectively silenced. 102 groupSummary.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN); 103 } 104 PhoneAccountHandle handle = getAccountForCall(context, newCalls.get(0)); 105 groupSummary.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle)); 106 } 107 108 DialerNotificationManager.notify( 109 context, GROUP_SUMMARY_NOTIFICATION_TAG, NOTIFICATION_ID, groupSummary.build()); 110 111 for (NewCall voicemail : newCalls) { 112 DialerNotificationManager.notify( 113 context, 114 getNotificationTagForVoicemail(voicemail), 115 NOTIFICATION_ID, 116 createNotificationForVoicemail(context, voicemail, contactInfos)); 117 } 118 } 119 cancelAllVoicemailNotifications(@onNull Context context)120 public static void cancelAllVoicemailNotifications(@NonNull Context context) { 121 LogUtil.enterBlock("VisualVoicemailNotifier.cancelAllVoicemailNotifications"); 122 NotificationManagerUtils.cancelAllInGroup(context, GROUP_KEY); 123 } 124 cancelSingleVoicemailNotification( @onNull Context context, @Nullable Uri voicemailUri)125 public static void cancelSingleVoicemailNotification( 126 @NonNull Context context, @Nullable Uri voicemailUri) { 127 LogUtil.enterBlock("VisualVoicemailNotifier.cancelSingleVoicemailNotification"); 128 if (voicemailUri == null) { 129 LogUtil.e("VisualVoicemailNotifier.cancelSingleVoicemailNotification", "uri is null"); 130 return; 131 } 132 // This will also dismiss the group summary if there are no more voicemail notifications. 133 DialerNotificationManager.cancel( 134 context, getNotificationTagForUri(voicemailUri), NOTIFICATION_ID); 135 } 136 getNotificationTagForVoicemail(@onNull NewCall voicemail)137 private static String getNotificationTagForVoicemail(@NonNull NewCall voicemail) { 138 return getNotificationTagForUri(voicemail.voicemailUri); 139 } 140 getNotificationTagForUri(@onNull Uri voicemailUri)141 private static String getNotificationTagForUri(@NonNull Uri voicemailUri) { 142 return NOTIFICATION_TAG_PREFIX + voicemailUri; 143 } 144 createNotificationBuilder(@onNull Context context)145 private static NotificationCompat.Builder createNotificationBuilder(@NonNull Context context) { 146 return new NotificationCompat.Builder(context) 147 .setSmallIcon(android.R.drawable.stat_notify_voicemail) 148 .setColor(context.getColor(R.color.dialer_theme_color)) 149 .setGroup(GROUP_KEY) 150 .setOnlyAlertOnce(true) 151 .setAutoCancel(true); 152 } 153 createNotificationForVoicemail( @onNull Context context, @NonNull NewCall voicemail, @NonNull Map<String, ContactInfo> contactInfos)154 static Notification createNotificationForVoicemail( 155 @NonNull Context context, 156 @NonNull NewCall voicemail, 157 @NonNull Map<String, ContactInfo> contactInfos) { 158 PhoneAccountHandle handle = getAccountForCall(context, voicemail); 159 ContactInfo contactInfo = contactInfos.get(voicemail.number); 160 161 NotificationCompat.Builder builder = 162 createNotificationBuilder(context) 163 .setContentTitle( 164 ContactDisplayUtils.getTtsSpannedPhoneNumber( 165 context.getResources(), 166 R.string.notification_new_voicemail_ticker, 167 contactInfo.name)) 168 .setWhen(voicemail.dateMs) 169 .setSound(getVoicemailRingtoneUri(context, handle)) 170 .setDefaults(getNotificationDefaultFlags(context, handle)); 171 172 if (!TextUtils.isEmpty(voicemail.transcription)) { 173 Logger.get(context) 174 .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION); 175 builder 176 .setContentText(voicemail.transcription) 177 .setStyle(new NotificationCompat.BigTextStyle().bigText(voicemail.transcription)); 178 } else { 179 switch (voicemail.transcriptionState) { 180 case VoicemailCompat.TRANSCRIPTION_IN_PROGRESS: 181 Logger.get(context) 182 .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_IN_PROGRESS); 183 builder.setContentText(context.getString(R.string.voicemail_transcription_in_progress)); 184 break; 185 case VoicemailCompat.TRANSCRIPTION_FAILED_NO_SPEECH_DETECTED: 186 Logger.get(context) 187 .logImpression( 188 DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE); 189 builder.setContentText( 190 context.getString(R.string.voicemail_transcription_failed_no_speech)); 191 break; 192 case VoicemailCompat.TRANSCRIPTION_FAILED_LANGUAGE_NOT_SUPPORTED: 193 Logger.get(context) 194 .logImpression( 195 DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE); 196 builder.setContentText( 197 context.getString(R.string.voicemail_transcription_failed_language_not_supported)); 198 break; 199 case VoicemailCompat.TRANSCRIPTION_FAILED: 200 Logger.get(context) 201 .logImpression( 202 DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_TRANSCRIPTION_FAILURE); 203 builder.setContentText(context.getString(R.string.voicemail_transcription_failed)); 204 break; 205 default: 206 Logger.get(context) 207 .logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED_WITH_NO_TRANSCRIPTION); 208 break; 209 } 210 } 211 212 if (voicemail.voicemailUri != null) { 213 builder.setDeleteIntent( 214 CallLogNotificationsService.createMarkSingleNewVoicemailAsOldIntent( 215 context, voicemail.voicemailUri)); 216 } 217 218 if (VERSION.SDK_INT >= VERSION_CODES.O) { 219 builder.setChannelId(NotificationChannelManager.getVoicemailChannelId(context, handle)); 220 builder.setGroupAlertBehavior(Notification.GROUP_ALERT_SUMMARY); 221 } 222 223 ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo); 224 Bitmap photoIcon = loader.loadPhotoIcon(); 225 if (photoIcon != null) { 226 builder.setLargeIcon(photoIcon); 227 } 228 builder.setContentIntent(newVoicemailIntent(context, voicemail)); 229 Logger.get(context).logImpression(DialerImpression.Type.VVM_NOTIFICATION_CREATED); 230 return builder.build(); 231 } 232 233 @Nullable getVoicemailRingtoneUri( @onNull Context context, @Nullable PhoneAccountHandle handle)234 private static Uri getVoicemailRingtoneUri( 235 @NonNull Context context, @Nullable PhoneAccountHandle handle) { 236 if (VERSION.SDK_INT < VERSION_CODES.N) { 237 return null; 238 } 239 if (handle == null) { 240 LogUtil.i("VisualVoicemailNotifier.getVoicemailRingtoneUri", "null handle, getting fallback"); 241 handle = getFallbackAccount(context); 242 if (handle == null) { 243 LogUtil.i( 244 "VisualVoicemailNotifier.getVoicemailRingtoneUri", 245 "no fallback handle, using null (default) ringtone"); 246 return null; 247 } 248 } 249 return context.getSystemService(TelephonyManager.class).getVoicemailRingtoneUri(handle); 250 } 251 getNotificationDefaultFlags( @onNull Context context, @Nullable PhoneAccountHandle handle)252 private static int getNotificationDefaultFlags( 253 @NonNull Context context, @Nullable PhoneAccountHandle handle) { 254 if (VERSION.SDK_INT < VERSION_CODES.N) { 255 return Notification.DEFAULT_ALL; 256 } 257 if (handle == null) { 258 LogUtil.i( 259 "VisualVoicemailNotifier.getNotificationDefaultFlags", "null handle, getting fallback"); 260 handle = getFallbackAccount(context); 261 if (handle == null) { 262 LogUtil.i( 263 "VisualVoicemailNotifier.getNotificationDefaultFlags", 264 "no fallback handle, using default vibration"); 265 return Notification.DEFAULT_ALL; 266 } 267 } 268 if (context.getSystemService(TelephonyManager.class).isVoicemailVibrationEnabled(handle)) { 269 return Notification.DEFAULT_VIBRATE; 270 } 271 return 0; 272 } 273 newVoicemailIntent( @onNull Context context, @Nullable NewCall voicemail)274 private static PendingIntent newVoicemailIntent( 275 @NonNull Context context, @Nullable NewCall voicemail) { 276 Intent intent; 277 if (MainComponent.isNuiComponentEnabled(context)) { 278 intent = MainComponent.getShowVoicemailIntent(context); 279 } else { 280 intent = 281 DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_VOICEMAIL); 282 } 283 // TODO (a bug): scroll to this voicemail 284 if (voicemail != null) { 285 intent.setData(voicemail.voicemailUri); 286 } 287 intent.putExtra(DialtactsActivity.EXTRA_CLEAR_NEW_VOICEMAILS, true); 288 return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); 289 } 290 291 /** 292 * Gets a phone account for the given call entry. This could be null if SIM associated with the 293 * entry is no longer in the device or for other reasons (for example, modem reboot). 294 */ 295 @Nullable getAccountForCall( @onNull Context context, @Nullable NewCall call)296 public static PhoneAccountHandle getAccountForCall( 297 @NonNull Context context, @Nullable NewCall call) { 298 if (call == null || call.accountComponentName == null || call.accountId == null) { 299 return null; 300 } 301 return new PhoneAccountHandle( 302 ComponentName.unflattenFromString(call.accountComponentName), call.accountId); 303 } 304 305 /** 306 * Gets any available phone account that can be used to get sound settings for voicemail. This is 307 * only called if the phone account for the voicemail entry can't be found. 308 */ 309 @Nullable getFallbackAccount(@onNull Context context)310 public static PhoneAccountHandle getFallbackAccount(@NonNull Context context) { 311 PhoneAccountHandle handle = 312 TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); 313 if (handle == null) { 314 List<PhoneAccountHandle> handles = TelecomUtil.getCallCapablePhoneAccounts(context); 315 if (!handles.isEmpty()) { 316 handle = handles.get(0); 317 } 318 } 319 return handle; 320 } 321 VisualVoicemailNotifier()322 private VisualVoicemailNotifier() {} 323 } 324