• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 package com.android.dialer.app.calllog;
17 
18 import android.app.Notification;
19 import android.app.Notification.Builder;
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.graphics.drawable.Icon;
26 import android.net.Uri;
27 import android.provider.CallLog.Calls;
28 import android.service.notification.StatusBarNotification;
29 import android.support.annotation.NonNull;
30 import android.support.annotation.Nullable;
31 import android.support.annotation.VisibleForTesting;
32 import android.support.annotation.WorkerThread;
33 import android.support.v4.os.BuildCompat;
34 import android.support.v4.os.UserManagerCompat;
35 import android.support.v4.util.Pair;
36 import android.telecom.PhoneAccount;
37 import android.telecom.PhoneAccountHandle;
38 import android.telecom.TelecomManager;
39 import android.telephony.PhoneNumberUtils;
40 import android.text.BidiFormatter;
41 import android.text.TextDirectionHeuristics;
42 import android.text.TextUtils;
43 import android.util.ArraySet;
44 import com.android.contacts.common.ContactsUtils;
45 import com.android.dialer.app.DialtactsActivity;
46 import com.android.dialer.app.MainComponent;
47 import com.android.dialer.app.R;
48 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
49 import com.android.dialer.app.contactinfo.ContactPhotoLoader;
50 import com.android.dialer.app.list.DialtactsPagerAdapter;
51 import com.android.dialer.callintent.CallInitiationType;
52 import com.android.dialer.callintent.CallIntentBuilder;
53 import com.android.dialer.common.Assert;
54 import com.android.dialer.common.LogUtil;
55 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
56 import com.android.dialer.compat.android.provider.VoicemailCompat;
57 import com.android.dialer.duo.DuoConstants;
58 import com.android.dialer.enrichedcall.FuzzyPhoneNumberMatcher;
59 import com.android.dialer.notification.DialerNotificationManager;
60 import com.android.dialer.notification.NotificationChannelId;
61 import com.android.dialer.notification.missedcalls.MissedCallConstants;
62 import com.android.dialer.notification.missedcalls.MissedCallNotificationCanceller;
63 import com.android.dialer.notification.missedcalls.MissedCallNotificationTags;
64 import com.android.dialer.phonenumbercache.ContactInfo;
65 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
66 import com.android.dialer.precall.PreCall;
67 import com.android.dialer.util.DialerUtils;
68 import com.android.dialer.util.IntentUtil;
69 import java.util.Iterator;
70 import java.util.List;
71 import java.util.Set;
72 
73 /** Creates a notification for calls that the user missed (neither answered nor rejected). */
74 public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
75 
76   private final Context context;
77   private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;
78 
79   @VisibleForTesting
MissedCallNotifier( Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper)80   MissedCallNotifier(
81       Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
82     this.context = context;
83     this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
84   }
85 
getInstance(Context context)86   public static MissedCallNotifier getInstance(Context context) {
87     return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context));
88   }
89 
90   @Nullable
91   @Override
doInBackground(@ullable Pair<Integer, String> input)92   public Void doInBackground(@Nullable Pair<Integer, String> input) throws Throwable {
93     updateMissedCallNotification(input.first, input.second);
94     return null;
95   }
96 
97   /**
98    * Update missed call notifications from the call log. Accepts default information in case call
99    * log cannot be accessed.
100    *
101    * @param count the number of missed calls to display if call log cannot be accessed. May be
102    *     {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown.
103    * @param number the phone number of the most recent call to display if the call log cannot be
104    *     accessed. May be null if unknown.
105    */
106   @VisibleForTesting
107   @WorkerThread
updateMissedCallNotification(int count, @Nullable String number)108   void updateMissedCallNotification(int count, @Nullable String number) {
109     final int titleResId;
110     CharSequence expandedText; // The text in the notification's line 1 and 2.
111 
112     List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
113 
114     removeSelfManagedCalls(newCalls);
115 
116     if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
117       // No calls to notify about: clear the notification.
118       CallLogNotificationsQueryHelper.markAllMissedCallsInCallLogAsRead(context);
119       MissedCallNotificationCanceller.cancelAll(context);
120       return;
121     }
122 
123     if (newCalls != null) {
124       if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT
125           && count != newCalls.size()) {
126         LogUtil.w(
127             "MissedCallNotifier.updateMissedCallNotification",
128             "Call count does not match call log count."
129                 + " count: "
130                 + count
131                 + " newCalls.size(): "
132                 + newCalls.size());
133       }
134       count = newCalls.size();
135     }
136 
137     if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
138       // If the intent did not contain a count, and we are unable to get a count from the
139       // call log, then no notification can be shown.
140       return;
141     }
142 
143     Notification.Builder groupSummary = createNotificationBuilder();
144     boolean useCallList = newCalls != null;
145 
146     if (count == 1) {
147       NewCall call =
148           useCallList
149               ? newCalls.get(0)
150               : new NewCall(
151                   null,
152                   null,
153                   number,
154                   Calls.PRESENTATION_ALLOWED,
155                   null,
156                   null,
157                   null,
158                   null,
159                   System.currentTimeMillis(),
160                   VoicemailCompat.TRANSCRIPTION_NOT_STARTED);
161 
162       // TODO: look up caller ID that is not in contacts.
163       ContactInfo contactInfo =
164           callLogNotificationsQueryHelper.getContactInfo(
165               call.number, call.numberPresentation, call.countryIso);
166       titleResId =
167           contactInfo.userType == ContactsUtils.USER_TYPE_WORK
168               ? R.string.notification_missedWorkCallTitle
169               : R.string.notification_missedCallTitle;
170 
171       if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
172           || TextUtils.equals(contactInfo.name, contactInfo.number)) {
173         expandedText =
174             PhoneNumberUtils.createTtsSpannable(
175                 BidiFormatter.getInstance()
176                     .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
177       } else {
178         expandedText = contactInfo.name;
179       }
180 
181       ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
182       Bitmap photoIcon = loader.loadPhotoIcon();
183       if (photoIcon != null) {
184         groupSummary.setLargeIcon(photoIcon);
185       }
186     } else {
187       titleResId = R.string.notification_missedCallsTitle;
188       expandedText = context.getString(R.string.notification_missedCallsMsg, count);
189     }
190 
191     // Create a public viewable version of the notification, suitable for display when sensitive
192     // notification content is hidden.
193     Notification.Builder publicSummaryBuilder = createNotificationBuilder();
194     publicSummaryBuilder
195         .setContentTitle(context.getText(titleResId))
196         .setContentIntent(createCallLogPendingIntent())
197         .setDeleteIntent(
198             CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context));
199 
200     // Create the notification summary suitable for display when sensitive information is showing.
201     groupSummary
202         .setContentTitle(context.getText(titleResId))
203         .setContentText(expandedText)
204         .setContentIntent(createCallLogPendingIntent())
205         .setDeleteIntent(
206             CallLogNotificationsService.createCancelAllMissedCallsPendingIntent(context))
207         .setGroupSummary(useCallList)
208         .setOnlyAlertOnce(useCallList)
209         .setPublicVersion(publicSummaryBuilder.build());
210     if (BuildCompat.isAtLeastO()) {
211       groupSummary.setChannelId(NotificationChannelId.MISSED_CALL);
212     }
213 
214     Notification notification = groupSummary.build();
215     configureLedOnNotification(notification);
216 
217     LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
218     DialerNotificationManager.notify(
219         context,
220         MissedCallConstants.GROUP_SUMMARY_NOTIFICATION_TAG,
221         MissedCallConstants.NOTIFICATION_ID,
222         notification);
223 
224     if (useCallList) {
225       // Do not repost active notifications to prevent erasing post call notes.
226       Set<String> activeTags = new ArraySet<>();
227       for (StatusBarNotification activeNotification :
228           DialerNotificationManager.getActiveNotifications(context)) {
229         activeTags.add(activeNotification.getTag());
230       }
231 
232       for (NewCall call : newCalls) {
233         String callTag = getNotificationTagForCall(call);
234         if (!activeTags.contains(callTag)) {
235           DialerNotificationManager.notify(
236               context,
237               callTag,
238               MissedCallConstants.NOTIFICATION_ID,
239               getNotificationForCall(call, null));
240         }
241       }
242     }
243   }
244 
245   /**
246    * Remove self-managed calls from {@code newCalls}. If a {@link PhoneAccount} declared it is
247    * {@link PhoneAccount#CAPABILITY_SELF_MANAGED}, it should handle the in call UI and notifications
248    * itself, but might still write to call log with {@link
249    * PhoneAccount#EXTRA_LOG_SELF_MANAGED_CALLS}.
250    */
removeSelfManagedCalls(@ullable List<NewCall> newCalls)251   private void removeSelfManagedCalls(@Nullable List<NewCall> newCalls) {
252     if (newCalls == null) {
253       return;
254     }
255 
256     TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
257     Iterator<NewCall> iterator = newCalls.iterator();
258     while (iterator.hasNext()) {
259       NewCall call = iterator.next();
260       if (call.accountComponentName == null || call.accountId == null) {
261         continue;
262       }
263       ComponentName componentName = ComponentName.unflattenFromString(call.accountComponentName);
264       if (componentName == null) {
265         continue;
266       }
267       PhoneAccountHandle phoneAccountHandle = new PhoneAccountHandle(componentName, call.accountId);
268       PhoneAccount phoneAccount = telecomManager.getPhoneAccount(phoneAccountHandle);
269       if (phoneAccount == null) {
270         continue;
271       }
272       if (DuoConstants.PHONE_ACCOUNT_HANDLE.equals(phoneAccountHandle)) {
273         iterator.remove();
274         continue;
275       }
276       if (phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)) {
277         LogUtil.i(
278             "MissedCallNotifier.removeSelfManagedCalls",
279             "ignoring self-managed call " + call.callsUri);
280         iterator.remove();
281       }
282     }
283   }
284 
getNotificationTagForCall(@onNull NewCall call)285   private static String getNotificationTagForCall(@NonNull NewCall call) {
286     return MissedCallNotificationTags.getNotificationTagForCallUri(call.callsUri);
287   }
288 
289   @WorkerThread
insertPostCallNotification(@onNull String number, @NonNull String note)290   public void insertPostCallNotification(@NonNull String number, @NonNull String note) {
291     Assert.isWorkerThread();
292     LogUtil.enterBlock("MissedCallNotifier.insertPostCallNotification");
293     List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
294     if (newCalls != null && !newCalls.isEmpty()) {
295       for (NewCall call : newCalls) {
296         if (FuzzyPhoneNumberMatcher.matches(call.number, number.replace("tel:", ""))) {
297           LogUtil.i("MissedCallNotifier.insertPostCallNotification", "Notification updated");
298           // Update the first notification that matches our post call note sender.
299           DialerNotificationManager.notify(
300               context,
301               getNotificationTagForCall(call),
302               MissedCallConstants.NOTIFICATION_ID,
303               getNotificationForCall(call, note));
304           return;
305         }
306       }
307     }
308     LogUtil.i("MissedCallNotifier.insertPostCallNotification", "notification not found");
309   }
310 
getNotificationForCall( @onNull NewCall call, @Nullable String postCallMessage)311   private Notification getNotificationForCall(
312       @NonNull NewCall call, @Nullable String postCallMessage) {
313     ContactInfo contactInfo =
314         callLogNotificationsQueryHelper.getContactInfo(
315             call.number, call.numberPresentation, call.countryIso);
316 
317     // Create a public viewable version of the notification, suitable for display when sensitive
318     // notification content is hidden.
319     int titleResId =
320         contactInfo.userType == ContactsUtils.USER_TYPE_WORK
321             ? R.string.notification_missedWorkCallTitle
322             : R.string.notification_missedCallTitle;
323     Notification.Builder publicBuilder =
324         createNotificationBuilder(call).setContentTitle(context.getText(titleResId));
325 
326     Notification.Builder builder = createNotificationBuilder(call);
327     CharSequence expandedText;
328     if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
329         || TextUtils.equals(contactInfo.name, contactInfo.number)) {
330       expandedText =
331           PhoneNumberUtils.createTtsSpannable(
332               BidiFormatter.getInstance()
333                   .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
334     } else {
335       expandedText = contactInfo.name;
336     }
337 
338     if (postCallMessage != null) {
339       expandedText =
340           context.getString(R.string.post_call_notification_message, expandedText, postCallMessage);
341     }
342 
343     ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
344     Bitmap photoIcon = loader.loadPhotoIcon();
345     if (photoIcon != null) {
346       builder.setLargeIcon(photoIcon);
347     }
348     // Create the notification suitable for display when sensitive information is showing.
349     builder
350         .setContentTitle(context.getText(titleResId))
351         .setContentText(expandedText)
352         // Include a public version of the notification to be shown when the missed call
353         // notification is shown on the user's lock screen and they have chosen to hide
354         // sensitive notification information.
355         .setPublicVersion(publicBuilder.build());
356 
357     // Add additional actions when the user isn't locked
358     if (UserManagerCompat.isUserUnlocked(context)) {
359       if (!TextUtils.isEmpty(call.number)
360           && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) {
361         builder.addAction(
362             new Notification.Action.Builder(
363                     Icon.createWithResource(context, R.drawable.ic_phone_24dp),
364                     context.getString(R.string.notification_missedCall_call_back),
365                     createCallBackPendingIntent(call.number, call.callsUri))
366                 .build());
367 
368         if (!PhoneNumberHelper.isUriNumber(call.number)) {
369           builder.addAction(
370               new Notification.Action.Builder(
371                       Icon.createWithResource(context, R.drawable.quantum_ic_message_white_24),
372                       context.getString(R.string.notification_missedCall_message),
373                       createSendSmsFromNotificationPendingIntent(call.number, call.callsUri))
374                   .build());
375         }
376       }
377     }
378 
379     Notification notification = builder.build();
380     configureLedOnNotification(notification);
381     return notification;
382   }
383 
createNotificationBuilder()384   private Notification.Builder createNotificationBuilder() {
385     return new Notification.Builder(context)
386         .setGroup(MissedCallConstants.GROUP_KEY)
387         .setSmallIcon(android.R.drawable.stat_notify_missed_call)
388         .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
389         .setAutoCancel(true)
390         .setOnlyAlertOnce(true)
391         .setShowWhen(true)
392         .setDefaults(Notification.DEFAULT_VIBRATE);
393   }
394 
createNotificationBuilder(@onNull NewCall call)395   private Notification.Builder createNotificationBuilder(@NonNull NewCall call) {
396     Builder builder =
397         createNotificationBuilder()
398             .setWhen(call.dateMs)
399             .setDeleteIntent(
400                 CallLogNotificationsService.createCancelSingleMissedCallPendingIntent(
401                     context, call.callsUri))
402             .setContentIntent(createCallLogPendingIntent(call.callsUri));
403     if (BuildCompat.isAtLeastO()) {
404       builder.setChannelId(NotificationChannelId.MISSED_CALL);
405     }
406 
407     return builder;
408   }
409 
410   /** Trigger an intent to make a call from a missed call number. */
411   @WorkerThread
callBackFromMissedCall(String number, Uri callUri)412   public void callBackFromMissedCall(String number, Uri callUri) {
413     closeSystemDialogs(context);
414     CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
415     MissedCallNotificationCanceller.cancelSingle(context, callUri);
416     DialerUtils.startActivityWithErrorToast(
417         context,
418         PreCall.getIntent(
419                 context,
420                 new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION))
421             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
422   }
423 
424   /** Trigger an intent to send an sms from a missed call number. */
sendSmsFromMissedCall(String number, Uri callUri)425   public void sendSmsFromMissedCall(String number, Uri callUri) {
426     closeSystemDialogs(context);
427     CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead(context, callUri);
428     MissedCallNotificationCanceller.cancelSingle(context, callUri);
429     DialerUtils.startActivityWithErrorToast(
430         context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
431   }
432 
433   /**
434    * Creates a new pending intent that sends the user to the call log.
435    *
436    * @return The pending intent.
437    */
createCallLogPendingIntent()438   private PendingIntent createCallLogPendingIntent() {
439     return createCallLogPendingIntent(null);
440   }
441 
442   /**
443    * Creates a new pending intent that sends the user to the call log.
444    *
445    * @return The pending intent.
446    * @param callUri Uri of the call to jump to. May be null
447    */
createCallLogPendingIntent(@ullable Uri callUri)448   private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
449     Intent contentIntent;
450     if (MainComponent.isNuiComponentEnabled(context)) {
451       contentIntent = MainComponent.getShowCallLogIntent(context);
452     } else {
453       contentIntent =
454           DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_HISTORY);
455     }
456     // TODO (a bug): scroll to call
457     contentIntent.setData(callUri);
458     return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
459   }
460 
createCallBackPendingIntent(String number, @NonNull Uri callUri)461   private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) {
462     Intent intent = new Intent(context, CallLogNotificationsService.class);
463     intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
464     intent.putExtra(MissedCallNotificationReceiver.EXTRA_NOTIFICATION_PHONE_NUMBER, number);
465     intent.setData(callUri);
466     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
467     // extra.
468     return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
469   }
470 
createSendSmsFromNotificationPendingIntent( String number, @NonNull Uri callUri)471   private PendingIntent createSendSmsFromNotificationPendingIntent(
472       String number, @NonNull Uri callUri) {
473     Intent intent = new Intent(context, CallLogNotificationsActivity.class);
474     intent.setAction(CallLogNotificationsActivity.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
475     intent.putExtra(CallLogNotificationsActivity.EXTRA_MISSED_CALL_NUMBER, number);
476     intent.setData(callUri);
477     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
478     // extra.
479     return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
480   }
481 
482   /** Configures a notification to emit the blinky notification light. */
configureLedOnNotification(Notification notification)483   private void configureLedOnNotification(Notification notification) {
484     notification.flags |= Notification.FLAG_SHOW_LIGHTS;
485     notification.defaults |= Notification.DEFAULT_LIGHTS;
486   }
487 
488   /** Closes open system dialogs and the notification shade. */
closeSystemDialogs(Context context)489   private void closeSystemDialogs(Context context) {
490     context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
491   }
492 }
493