Home | History | Annotate | Download | only in spam
      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 
     17 package com.android.incallui.spam;
     18 
     19 import android.app.Notification;
     20 import android.app.Notification.Builder;
     21 import android.app.NotificationManager;
     22 import android.app.PendingIntent;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.graphics.drawable.Icon;
     26 import android.support.annotation.NonNull;
     27 import android.telecom.DisconnectCause;
     28 import android.telephony.PhoneNumberUtils;
     29 import android.text.TextUtils;
     30 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
     31 import com.android.dialer.blocking.FilteredNumberCompat;
     32 import com.android.dialer.blocking.FilteredNumbersUtil;
     33 import com.android.dialer.common.LogUtil;
     34 import com.android.dialer.location.GeoUtil;
     35 import com.android.dialer.logging.ContactLookupResult;
     36 import com.android.dialer.logging.DialerImpression;
     37 import com.android.dialer.logging.Logger;
     38 import com.android.dialer.notification.NotificationChannelManager;
     39 import com.android.dialer.notification.NotificationChannelManager.Channel;
     40 import com.android.dialer.spam.Spam;
     41 import com.android.incallui.R;
     42 import com.android.incallui.call.CallList;
     43 import com.android.incallui.call.DialerCall;
     44 import com.android.incallui.call.DialerCall.CallHistoryStatus;
     45 import java.util.Random;
     46 
     47 /**
     48  * Creates notifications after a call ends if the call matched the criteria (incoming, accepted,
     49  * etc).
     50  */
     51 public class SpamCallListListener implements CallList.Listener {
     52 
     53   static final int NOTIFICATION_ID = R.id.notification_spam_call;
     54   private static final String TAG = "SpamCallListListener";
     55   private final Context context;
     56   private final Random random;
     57 
     58   public SpamCallListListener(Context context) {
     59     this.context = context;
     60     this.random = new Random();
     61   }
     62 
     63   public SpamCallListListener(Context context, Random rand) {
     64     this.context = context;
     65     this.random = rand;
     66   }
     67 
     68   private static String pii(String pii) {
     69     return com.android.incallui.Log.pii(pii);
     70   }
     71 
     72   @Override
     73   public void onIncomingCall(final DialerCall call) {
     74     String number = call.getNumber();
     75     if (TextUtils.isEmpty(number)) {
     76       return;
     77     }
     78     NumberInCallHistoryTask.Listener listener =
     79         new NumberInCallHistoryTask.Listener() {
     80           @Override
     81           public void onComplete(@CallHistoryStatus int callHistoryStatus) {
     82             call.setCallHistoryStatus(callHistoryStatus);
     83           }
     84         };
     85     new NumberInCallHistoryTask(context, listener, number, GeoUtil.getCurrentCountryIso(context))
     86         .submitTask();
     87   }
     88 
     89   @Override
     90   public void onUpgradeToVideo(DialerCall call) {}
     91 
     92   @Override
     93   public void onSessionModificationStateChange(DialerCall call) {}
     94 
     95   @Override
     96   public void onCallListChange(CallList callList) {}
     97 
     98   @Override
     99   public void onWiFiToLteHandover(DialerCall call) {}
    100 
    101   @Override
    102   public void onHandoverToWifiFailed(DialerCall call) {}
    103 
    104   @Override
    105   public void onInternationalCallOnWifi(@NonNull DialerCall call) {}
    106 
    107   @Override
    108   public void onDisconnect(DialerCall call) {
    109     if (!shouldShowAfterCallNotification(call)) {
    110       return;
    111     }
    112     String e164Number =
    113         PhoneNumberUtils.formatNumberToE164(
    114             call.getNumber(), GeoUtil.getCurrentCountryIso(context));
    115     if (!FilteredNumbersUtil.canBlockNumber(context, e164Number, call.getNumber())
    116         || !FilteredNumberCompat.canAttemptBlockOperations(context)) {
    117       return;
    118     }
    119     if (e164Number == null) {
    120       return;
    121     }
    122     showNotification(call);
    123   }
    124 
    125   /** Posts the intent for displaying the after call spam notification to the user. */
    126   private void showNotification(DialerCall call) {
    127     if (call.isSpam()) {
    128       maybeShowSpamCallNotification(call);
    129     } else {
    130       LogUtil.d(TAG, "Showing not spam notification for number=" + pii(call.getNumber()));
    131       maybeShowNonSpamCallNotification(call);
    132     }
    133   }
    134 
    135   /** Determines if the after call notification should be shown for the specified call. */
    136   private boolean shouldShowAfterCallNotification(DialerCall call) {
    137     if (!Spam.get(context).isSpamNotificationEnabled()) {
    138       return false;
    139     }
    140 
    141     String number = call.getNumber();
    142     if (TextUtils.isEmpty(number)) {
    143       return false;
    144     }
    145 
    146     DialerCall.LogState logState = call.getLogState();
    147     if (!logState.isIncoming) {
    148       return false;
    149     }
    150 
    151     if (logState.duration <= 0) {
    152       return false;
    153     }
    154 
    155     if (logState.contactLookupResult != ContactLookupResult.Type.NOT_FOUND
    156         && logState.contactLookupResult != ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE) {
    157       return false;
    158     }
    159 
    160     int callHistoryStatus = call.getCallHistoryStatus();
    161     if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_PRESENT) {
    162       return false;
    163     } else if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_UNKNOWN) {
    164       LogUtil.i(TAG, "DialerCall history status is unknown, returning false");
    165       return false;
    166     }
    167 
    168     // Check if call disconnected because of either user hanging up
    169     int disconnectCause = call.getDisconnectCause().getCode();
    170     if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) {
    171       return false;
    172     }
    173 
    174     LogUtil.i(TAG, "shouldShowAfterCallNotification, returning true");
    175     return true;
    176   }
    177 
    178   /**
    179    * Creates a notification builder with properties common among the two after call notifications.
    180    */
    181   private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
    182     Builder builder =
    183         new Builder(context)
    184             .setContentIntent(
    185                 createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
    186             .setCategory(Notification.CATEGORY_STATUS)
    187             .setPriority(Notification.PRIORITY_DEFAULT)
    188             .setColor(context.getColor(R.color.dialer_theme_color))
    189             .setSmallIcon(R.drawable.ic_call_end_white_24dp);
    190     NotificationChannelManager.applyChannel(builder, context, Channel.DEFAULT, null);
    191     return builder;
    192   }
    193 
    194   private CharSequence getDisplayNumber(DialerCall call) {
    195     String formattedNumber =
    196         PhoneNumberUtils.formatNumber(call.getNumber(), GeoUtil.getCurrentCountryIso(context));
    197     return PhoneNumberUtilsCompat.createTtsSpannable(formattedNumber);
    198   }
    199 
    200   /** Display a notification with two actions: "add contact" and "report spam". */
    201   private void showNonSpamCallNotification(DialerCall call) {
    202     Notification.Builder notificationBuilder =
    203         createAfterCallNotificationBuilder(call)
    204             .setLargeIcon(Icon.createWithResource(context, R.drawable.unknown_notification_icon))
    205             .setContentText(
    206                 context.getString(R.string.spam_notification_non_spam_call_collapsed_text))
    207             .setStyle(
    208                 new Notification.BigTextStyle()
    209                     .bigText(
    210                         context.getString(R.string.spam_notification_non_spam_call_expanded_text)))
    211             // Add contact
    212             .addAction(
    213                 new Notification.Action.Builder(
    214                         R.drawable.ic_person_add_grey600_24dp,
    215                         context.getString(R.string.spam_notification_add_contact_action_text),
    216                         createActivityPendingIntent(
    217                             call, SpamNotificationActivity.ACTION_ADD_TO_CONTACTS))
    218                     .build())
    219             // Block/report spam
    220             .addAction(
    221                 new Notification.Action.Builder(
    222                         R.drawable.ic_block_grey600_24dp,
    223                         context.getString(R.string.spam_notification_report_spam_action_text),
    224                         createBlockReportSpamPendingIntent(call))
    225                     .build())
    226             .setContentTitle(
    227                 context.getString(R.string.non_spam_notification_title, getDisplayNumber(call)));
    228     ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
    229         .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
    230   }
    231 
    232   private boolean shouldThrottleSpamNotification() {
    233     int randomNumber = random.nextInt(100);
    234     int thresholdForShowing = Spam.get(context).percentOfSpamNotificationsToShow();
    235     if (thresholdForShowing == 0) {
    236       LogUtil.d(
    237           TAG,
    238           "shouldThrottleSpamNotification, not showing - percentOfSpamNotificationsToShow is 0");
    239       return true;
    240     } else if (randomNumber < thresholdForShowing) {
    241       LogUtil.d(
    242           TAG,
    243           "shouldThrottleSpamNotification, showing " + randomNumber + " < " + thresholdForShowing);
    244       return false;
    245     } else {
    246       LogUtil.d(
    247           TAG,
    248           "shouldThrottleSpamNotification, not showing "
    249               + randomNumber
    250               + " >= "
    251               + thresholdForShowing);
    252       return true;
    253     }
    254   }
    255 
    256   private boolean shouldThrottleNonSpamNotification() {
    257     int randomNumber = random.nextInt(100);
    258     int thresholdForShowing = Spam.get(context).percentOfNonSpamNotificationsToShow();
    259     if (thresholdForShowing == 0) {
    260       LogUtil.d(TAG, "Not showing non spam notification: percentOfNonSpamNotificationsToShow is 0");
    261       return true;
    262     } else if (randomNumber < thresholdForShowing) {
    263       LogUtil.d(
    264           TAG, "Showing non spam notification: " + randomNumber + " < " + thresholdForShowing);
    265       return false;
    266     } else {
    267       LogUtil.d(
    268           TAG, "Not showing non spam notification:" + randomNumber + " >= " + thresholdForShowing);
    269       return true;
    270     }
    271   }
    272 
    273   private void maybeShowSpamCallNotification(DialerCall call) {
    274     if (shouldThrottleSpamNotification()) {
    275       Logger.get(context)
    276           .logCallImpression(
    277               DialerImpression.Type.SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
    278               call.getUniqueCallId(),
    279               call.getTimeAddedMs());
    280     } else {
    281       Logger.get(context)
    282           .logCallImpression(
    283               DialerImpression.Type.SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
    284               call.getUniqueCallId(),
    285               call.getTimeAddedMs());
    286       showSpamCallNotification(call);
    287     }
    288   }
    289 
    290   private void maybeShowNonSpamCallNotification(DialerCall call) {
    291     if (shouldThrottleNonSpamNotification()) {
    292       Logger.get(context)
    293           .logCallImpression(
    294               DialerImpression.Type.NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
    295               call.getUniqueCallId(),
    296               call.getTimeAddedMs());
    297     } else {
    298       Logger.get(context)
    299           .logCallImpression(
    300               DialerImpression.Type.NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
    301               call.getUniqueCallId(),
    302               call.getTimeAddedMs());
    303       showNonSpamCallNotification(call);
    304     }
    305   }
    306 
    307   /** Display a notification with the action "not spam". */
    308   private void showSpamCallNotification(DialerCall call) {
    309     Notification.Builder notificationBuilder =
    310         createAfterCallNotificationBuilder(call)
    311             .setLargeIcon(Icon.createWithResource(context, R.drawable.spam_notification_icon))
    312             .setContentText(context.getString(R.string.spam_notification_spam_call_collapsed_text))
    313             .setStyle(
    314                 new Notification.BigTextStyle()
    315                     .bigText(context.getString(R.string.spam_notification_spam_call_expanded_text)))
    316             // Not spam
    317             .addAction(
    318                 new Notification.Action.Builder(
    319                         R.drawable.ic_close_grey600_24dp,
    320                         context.getString(R.string.spam_notification_not_spam_action_text),
    321                         createNotSpamPendingIntent(call))
    322                     .build())
    323             // Block/report spam
    324             .addAction(
    325                 new Notification.Action.Builder(
    326                         R.drawable.ic_block_grey600_24dp,
    327                         context.getString(R.string.spam_notification_block_spam_action_text),
    328                         createBlockReportSpamPendingIntent(call))
    329                     .build())
    330             .setContentTitle(
    331                 context.getString(R.string.spam_notification_title, getDisplayNumber(call)));
    332     ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE))
    333         .notify(call.getNumber(), NOTIFICATION_ID, notificationBuilder.build());
    334   }
    335 
    336   /**
    337    * Creates a pending intent for block/report spam action. If enabled, this intent is forwarded to
    338    * the {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
    339    */
    340   private PendingIntent createBlockReportSpamPendingIntent(DialerCall call) {
    341     String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM;
    342     return Spam.get(context).isDialogEnabledForSpamNotification()
    343         ? createActivityPendingIntent(call, action)
    344         : createServicePendingIntent(call, action);
    345   }
    346 
    347   /**
    348    * Creates a pending intent for not spam action. If enabled, this intent is forwarded to the
    349    * {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
    350    */
    351   private PendingIntent createNotSpamPendingIntent(DialerCall call) {
    352     String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM;
    353     return Spam.get(context).isDialogEnabledForSpamNotification()
    354         ? createActivityPendingIntent(call, action)
    355         : createServicePendingIntent(call, action);
    356   }
    357 
    358   /** Creates a pending intent for {@link SpamNotificationService}. */
    359   private PendingIntent createServicePendingIntent(DialerCall call, String action) {
    360     Intent intent =
    361         SpamNotificationService.createServiceIntent(context, call, action, NOTIFICATION_ID);
    362     return PendingIntent.getService(
    363         context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
    364   }
    365 
    366   /** Creates a pending intent for {@link SpamNotificationActivity}. */
    367   private PendingIntent createActivityPendingIntent(DialerCall call, String action) {
    368     Intent intent =
    369         SpamNotificationActivity.createActivityIntent(context, call, action, NOTIFICATION_ID);
    370     return PendingIntent.getActivity(
    371         context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
    372   }
    373 }
    374