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