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