• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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