Home | History | Annotate | Download | only in calllog
      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
     71   MissedCallNotifier(
     72       Context context, CallLogNotificationsQueryHelper callLogNotificationsQueryHelper) {
     73     this.context = context;
     74     this.callLogNotificationsQueryHelper = callLogNotificationsQueryHelper;
     75   }
     76 
     77   static MissedCallNotifier getIstance(Context context) {
     78     return new MissedCallNotifier(context, CallLogNotificationsQueryHelper.getInstance(context));
     79   }
     80 
     81   @Nullable
     82   @Override
     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
     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 
    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 
    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 
    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 
    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
    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. */
    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    */
    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    */
    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. */
    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 
    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 
    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. */
    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. */
    410   private void closeSystemDialogs(Context context) {
    411     context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
    412   }
    413 
    414   private NotificationManager getNotificationMgr() {
    415     return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    416   }
    417 }
    418