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