• 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.NotificationManager;
21 import android.app.PendingIntent;
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.UserManagerCompat;
34 import android.support.v4.util.Pair;
35 import android.text.BidiFormatter;
36 import android.text.TextDirectionHeuristics;
37 import android.text.TextUtils;
38 import com.android.contacts.common.ContactsUtils;
39 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
40 import com.android.dialer.app.DialtactsActivity;
41 import com.android.dialer.app.R;
42 import com.android.dialer.app.calllog.CallLogNotificationsQueryHelper.NewCall;
43 import com.android.dialer.app.contactinfo.ContactPhotoLoader;
44 import com.android.dialer.app.list.DialtactsPagerAdapter;
45 import com.android.dialer.callintent.CallInitiationType;
46 import com.android.dialer.callintent.CallIntentBuilder;
47 import com.android.dialer.common.LogUtil;
48 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
49 import com.android.dialer.notification.NotificationChannelManager;
50 import com.android.dialer.notification.NotificationChannelManager.Channel;
51 import com.android.dialer.phonenumbercache.ContactInfo;
52 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
53 import com.android.dialer.util.DialerUtils;
54 import com.android.dialer.util.IntentUtil;
55 import java.util.HashSet;
56 import java.util.List;
57 import java.util.Set;
58 
59 /** Creates a notification for calls that the user missed (neither answered nor rejected). */
60 public class MissedCallNotifier implements Worker<Pair<Integer, String>, Void> {
61 
62   /** The tag used to identify notifications from this class. */
63   static final String NOTIFICATION_TAG = "MissedCallNotifier";
64   /** The identifier of the notification of new missed calls. */
65   private static final int NOTIFICATION_ID = R.id.notification_missed_call;
66 
67   private final Context context;
68   private final CallLogNotificationsQueryHelper callLogNotificationsQueryHelper;
69 
70   @VisibleForTesting
MissedCallNotifier( Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper)71   MissedCallNotifier(
72       Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
73     this.context = context;
74     this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
75   }
76 
getIstance(Context context)77   static MissedCallNotifier getIstance(Context context) {
78     return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context));
79   }
80 
81   @Nullable
82   @Override
doInBackground(@ullable Pair<Integer, String> input)83   public Void doInBackground(@Nullable Pair<Integer, String> input) throws Throwable {
84     updateMissedCallNotification(input.first, input.second);
85     return null;
86   }
87 
88   /**
89    * Update missed call notifications from the call log. Accepts default information in case call
90    * log cannot be accessed.
91    *
92    * @param count the number of missed calls to display if call log cannot be accessed. May be
93    *     {@link CallLogNotificationsService#UNKNOWN_MISSED_CALL_COUNT} if unknown.
94    * @param number the phone number of the most recent call to display if the call log cannot be
95    *     accessed. May be null if unknown.
96    */
97   @VisibleForTesting
98   @WorkerThread
updateMissedCallNotification(int count, @Nullable String number)99   void updateMissedCallNotification(int count, @Nullable String number) {
100     final int titleResId;
101     CharSequence expandedText; // The text in the notification's line 1 and 2.
102 
103     List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
104 
105     if ((newCalls != null && newCalls.isEmpty()) || count == 0) {
106       // No calls to notify about: clear the notification.
107       CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, null);
108       return;
109     }
110 
111     if (newCalls != null) {
112       if (count != CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT
113           && count != newCalls.size()) {
114         LogUtil.w(
115             "MissedCallNotifier.updateMissedCallNotification",
116             "Call count does not match call log count."
117                 + " count: "
118                 + count
119                 + " newCalls.size(): "
120                 + newCalls.size());
121       }
122       count = newCalls.size();
123     }
124 
125     if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
126       // If the intent did not contain a count, and we are unable to get a count from the
127       // call log, then no notification can be shown.
128       return;
129     }
130 
131     Notification.Builder groupSummary = createNotificationBuilder();
132     boolean useCallList = newCalls != null;
133 
134     if (count == 1) {
135       NewCall call =
136           useCallList
137               ? newCalls.get(0)
138               : new NewCall(
139                   null,
140                   null,
141                   number,
142                   Calls.PRESENTATION_ALLOWED,
143                   null,
144                   null,
145                   null,
146                   null,
147                   System.currentTimeMillis());
148 
149       //TODO: look up caller ID that is not in contacts.
150       ContactInfo contactInfo =
151           callLogNotificationsQueryHelper.getContactInfo(
152               call.number, call.numberPresentation, call.countryIso);
153       titleResId =
154           contactInfo.userType == ContactsUtils.USER_TYPE_WORK
155               ? R.string.notification_missedWorkCallTitle
156               : R.string.notification_missedCallTitle;
157 
158       if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
159           || TextUtils.equals(contactInfo.name, contactInfo.number)) {
160         expandedText =
161             PhoneNumberUtilsCompat.createTtsSpannable(
162                 BidiFormatter.getInstance()
163                     .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
164       } else {
165         expandedText = contactInfo.name;
166       }
167 
168       ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
169       Bitmap photoIcon = loader.loadPhotoIcon();
170       if (photoIcon != null) {
171         groupSummary.setLargeIcon(photoIcon);
172       }
173     } else {
174       titleResId = R.string.notification_missedCallsTitle;
175       expandedText = context.getString(R.string.notification_missedCallsMsg, count);
176     }
177 
178     // Create a public viewable version of the notification, suitable for display when sensitive
179     // notification content is hidden.
180     Notification.Builder publicSummaryBuilder = createNotificationBuilder();
181     publicSummaryBuilder
182         .setContentTitle(context.getText(titleResId))
183         .setContentIntent(createCallLogPendingIntent())
184         .setDeleteIntent(createClearMissedCallsPendingIntent(null));
185 
186     // Create the notification summary suitable for display when sensitive information is showing.
187     groupSummary
188         .setContentTitle(context.getText(titleResId))
189         .setContentText(expandedText)
190         .setContentIntent(createCallLogPendingIntent())
191         .setDeleteIntent(createClearMissedCallsPendingIntent(null))
192         .setGroupSummary(useCallList)
193         .setOnlyAlertOnce(useCallList)
194         .setPublicVersion(publicSummaryBuilder.build());
195 
196     NotificationChannelManager.applyChannel(groupSummary, context, Channel.MISSED_CALL, null);
197 
198     Notification notification = groupSummary.build();
199     configureLedOnNotification(notification);
200 
201     LogUtil.i("MissedCallNotifier.updateMissedCallNotification", "adding missed call notification");
202     getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
203 
204     if (useCallList) {
205       // Do not repost active notifications to prevent erasing post call notes.
206       NotificationManager manager = getNotificationMgr();
207       Set<String> activeTags = new HashSet<>();
208       for (StatusBarNotification activeNotification : manager.getActiveNotifications()) {
209         activeTags.add(activeNotification.getTag());
210       }
211 
212       for (NewCall call : newCalls) {
213         String callTag = call.callsUri.toString();
214         if (!activeTags.contains(callTag)) {
215           manager.notify(callTag, NOTIFICATION_ID, getNotificationForCall(call, null));
216         }
217       }
218     }
219   }
220 
insertPostCallNotification(@onNull String number, @NonNull String note)221   public void insertPostCallNotification(@NonNull String number, @NonNull String note) {
222     List<NewCall> newCalls = callLogNotificationsQueryHelper.getNewMissedCalls();
223     if (newCalls != null && !newCalls.isEmpty()) {
224       for (NewCall call : newCalls) {
225         if (call.number.equals(number.replace("tel:", ""))) {
226           // Update the first notification that matches our post call note sender.
227           getNotificationMgr()
228               .notify(
229                   call.callsUri.toString(), NOTIFICATION_ID, getNotificationForCall(call, note));
230           break;
231         }
232       }
233     }
234   }
235 
getNotificationForCall( @onNull NewCall call, @Nullable String postCallMessage)236   private Notification getNotificationForCall(
237       @NonNull NewCall call, @Nullable String postCallMessage) {
238     ContactInfo contactInfo =
239         callLogNotificationsQueryHelper.getContactInfo(
240             call.number, call.numberPresentation, call.countryIso);
241 
242     // Create a public viewable version of the notification, suitable for display when sensitive
243     // notification content is hidden.
244     int titleResId =
245         contactInfo.userType == ContactsUtils.USER_TYPE_WORK
246             ? R.string.notification_missedWorkCallTitle
247             : R.string.notification_missedCallTitle;
248     Notification.Builder publicBuilder =
249         createNotificationBuilder(call).setContentTitle(context.getText(titleResId));
250 
251     Notification.Builder builder = createNotificationBuilder(call);
252     CharSequence expandedText;
253     if (TextUtils.equals(contactInfo.name, contactInfo.formattedNumber)
254         || TextUtils.equals(contactInfo.name, contactInfo.number)) {
255       expandedText =
256           PhoneNumberUtilsCompat.createTtsSpannable(
257               BidiFormatter.getInstance()
258                   .unicodeWrap(contactInfo.name, TextDirectionHeuristics.LTR));
259     } else {
260       expandedText = contactInfo.name;
261     }
262 
263     if (postCallMessage != null) {
264       expandedText =
265           context.getString(R.string.post_call_notification_message, expandedText, postCallMessage);
266     }
267 
268     ContactPhotoLoader loader = new ContactPhotoLoader(context, contactInfo);
269     Bitmap photoIcon = loader.loadPhotoIcon();
270     if (photoIcon != null) {
271       builder.setLargeIcon(photoIcon);
272     }
273     // Create the notification suitable for display when sensitive information is showing.
274     builder
275         .setContentTitle(context.getText(titleResId))
276         .setContentText(expandedText)
277         // Include a public version of the notification to be shown when the missed call
278         // notification is shown on the user's lock screen and they have chosen to hide
279         // sensitive notification information.
280         .setPublicVersion(publicBuilder.build());
281 
282     // Add additional actions when the user isn't locked
283     if (UserManagerCompat.isUserUnlocked(context)) {
284       if (!TextUtils.isEmpty(call.number)
285           && !TextUtils.equals(call.number, context.getString(R.string.handle_restricted))) {
286         builder.addAction(
287             new Notification.Action.Builder(
288                     Icon.createWithResource(context, R.drawable.ic_phone_24dp),
289                     context.getString(R.string.notification_missedCall_call_back),
290                     createCallBackPendingIntent(call.number, call.callsUri))
291                 .build());
292 
293         if (!PhoneNumberHelper.isUriNumber(call.number)) {
294           builder.addAction(
295               new Notification.Action.Builder(
296                       Icon.createWithResource(context, R.drawable.quantum_ic_message_white_24),
297                       context.getString(R.string.notification_missedCall_message),
298                       createSendSmsFromNotificationPendingIntent(call.number, call.callsUri))
299                   .build());
300         }
301       }
302     }
303 
304     Notification notification = builder.build();
305     configureLedOnNotification(notification);
306     return notification;
307   }
308 
createNotificationBuilder()309   private Notification.Builder createNotificationBuilder() {
310     return new Notification.Builder(context)
311         .setGroup(NOTIFICATION_TAG)
312         .setSmallIcon(android.R.drawable.stat_notify_missed_call)
313         .setColor(context.getResources().getColor(R.color.dialer_theme_color, null))
314         .setAutoCancel(true)
315         .setOnlyAlertOnce(true)
316         .setShowWhen(true)
317         .setDefaults(Notification.DEFAULT_VIBRATE);
318   }
319 
createNotificationBuilder(@onNull NewCall call)320   private Notification.Builder createNotificationBuilder(@NonNull NewCall call) {
321     Builder builder =
322         createNotificationBuilder()
323             .setWhen(call.dateMs)
324             .setDeleteIntent(createClearMissedCallsPendingIntent(call.callsUri))
325             .setContentIntent(createCallLogPendingIntent(call.callsUri));
326 
327     NotificationChannelManager.applyChannel(builder, context, Channel.MISSED_CALL, null);
328     return builder;
329   }
330 
331   /** Trigger an intent to make a call from a missed call number. */
332   @WorkerThread
callBackFromMissedCall(String number, Uri callUri)333   public void callBackFromMissedCall(String number, Uri callUri) {
334     closeSystemDialogs(context);
335     CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
336     DialerUtils.startActivityWithErrorToast(
337         context,
338         new CallIntentBuilder(number, CallInitiationType.Type.MISSED_CALL_NOTIFICATION)
339             .build()
340             .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
341   }
342 
343   /** Trigger an intent to send an sms from a missed call number. */
sendSmsFromMissedCall(String number, Uri callUri)344   public void sendSmsFromMissedCall(String number, Uri callUri) {
345     closeSystemDialogs(context);
346     CallLogNotificationsQueryHelper.removeMissedCallNotifications(context, callUri);
347     DialerUtils.startActivityWithErrorToast(
348         context, IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
349   }
350 
351   /**
352    * Creates a new pending intent that sends the user to the call log.
353    *
354    * @return The pending intent.
355    */
createCallLogPendingIntent()356   private PendingIntent createCallLogPendingIntent() {
357     return createCallLogPendingIntent(null);
358   }
359 
360   /**
361    * Creates a new pending intent that sends the user to the call log.
362    *
363    * @return The pending intent.
364    * @param callUri Uri of the call to jump to. May be null
365    */
createCallLogPendingIntent(@ullable Uri callUri)366   private PendingIntent createCallLogPendingIntent(@Nullable Uri callUri) {
367     Intent contentIntent =
368         DialtactsActivity.getShowTabIntent(context, DialtactsPagerAdapter.TAB_INDEX_HISTORY);
369     // TODO (b/35486204): scroll to call
370     contentIntent.setData(callUri);
371     return PendingIntent.getActivity(context, 0, contentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
372   }
373 
374   /** Creates a pending intent that marks all new missed calls as old. */
createClearMissedCallsPendingIntent(@ullable Uri callUri)375   private PendingIntent createClearMissedCallsPendingIntent(@Nullable Uri callUri) {
376     Intent intent = new Intent(context, CallLogNotificationsService.class);
377     intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
378     intent.setData(callUri);
379     return PendingIntent.getService(context, 0, intent, 0);
380   }
381 
createCallBackPendingIntent(String number, @NonNull Uri callUri)382   private PendingIntent createCallBackPendingIntent(String number, @NonNull Uri callUri) {
383     Intent intent = new Intent(context, CallLogNotificationsService.class);
384     intent.setAction(CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
385     intent.putExtra(MissedCallNotificationReceiver.EXTRA_NOTIFICATION_PHONE_NUMBER, number);
386     intent.setData(callUri);
387     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
388     // extra.
389     return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
390   }
391 
createSendSmsFromNotificationPendingIntent( String number, @NonNull Uri callUri)392   private PendingIntent createSendSmsFromNotificationPendingIntent(
393       String number, @NonNull Uri callUri) {
394     Intent intent = new Intent(context, CallLogNotificationsActivity.class);
395     intent.setAction(CallLogNotificationsActivity.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
396     intent.putExtra(CallLogNotificationsActivity.EXTRA_MISSED_CALL_NUMBER, number);
397     intent.setData(callUri);
398     // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
399     // extra.
400     return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
401   }
402 
403   /** Configures a notification to emit the blinky notification light. */
configureLedOnNotification(Notification notification)404   private void configureLedOnNotification(Notification notification) {
405     notification.flags |= Notification.FLAG_SHOW_LIGHTS;
406     notification.defaults |= Notification.DEFAULT_LIGHTS;
407   }
408 
409   /** Closes open system dialogs and the notification shade. */
closeSystemDialogs(Context context)410   private void closeSystemDialogs(Context context) {
411     context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
412   }
413 
getNotificationMgr()414   private NotificationManager getNotificationMgr() {
415     return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
416   }
417 }
418