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