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