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.annotation.TargetApi;
     20 import android.app.Notification;
     21 import android.app.Notification.Builder;
     22 import android.app.PendingIntent;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.database.Cursor;
     26 import android.database.sqlite.SQLiteException;
     27 import android.graphics.drawable.Icon;
     28 import android.os.Build.VERSION_CODES;
     29 import android.provider.CallLog;
     30 import android.provider.CallLog.Calls;
     31 import android.support.annotation.NonNull;
     32 import android.support.annotation.Nullable;
     33 import android.support.v4.os.BuildCompat;
     34 import android.telecom.DisconnectCause;
     35 import android.telephony.PhoneNumberUtils;
     36 import android.text.TextUtils;
     37 import com.android.dialer.blocking.FilteredNumberCompat;
     38 import com.android.dialer.blocking.FilteredNumbersUtil;
     39 import com.android.dialer.common.Assert;
     40 import com.android.dialer.common.LogUtil;
     41 import com.android.dialer.common.concurrent.DialerExecutor.Worker;
     42 import com.android.dialer.common.concurrent.DialerExecutorFactory;
     43 import com.android.dialer.location.GeoUtil;
     44 import com.android.dialer.logging.ContactLookupResult;
     45 import com.android.dialer.logging.DialerImpression;
     46 import com.android.dialer.logging.Logger;
     47 import com.android.dialer.notification.DialerNotificationManager;
     48 import com.android.dialer.notification.NotificationChannelId;
     49 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     50 import com.android.dialer.spam.SpamComponent;
     51 import com.android.dialer.telecom.TelecomUtil;
     52 import com.android.dialer.util.PermissionsUtil;
     53 import com.android.incallui.call.CallList;
     54 import com.android.incallui.call.DialerCall;
     55 import com.android.incallui.call.DialerCall.CallHistoryStatus;
     56 import java.util.Random;
     57 
     58 /**
     59  * Creates notifications after a call ends if the call matched the criteria (incoming, accepted,
     60  * etc).
     61  */
     62 public class SpamCallListListener implements CallList.Listener {
     63   /** Common ID for all spam notifications. */
     64   static final int NOTIFICATION_ID = 1;
     65   /** Prefix used to generate a unique tag for each spam notification. */
     66   static final String NOTIFICATION_TAG_PREFIX = "SpamCall_";
     67   /**
     68    * Key used to associate all spam notifications into a single group. Note, unlike other group
     69    * notifications in Dialer, spam notifications don't have a top level group summary notification.
     70    * The group is still useful for things like rate limiting on a per group basis.
     71    */
     72   private static final String GROUP_KEY = "SpamCallGroup";
     73 
     74   private final Context context;
     75   private final Random random;
     76   private final DialerExecutorFactory dialerExecutorFactory;
     77 
     78   public SpamCallListListener(Context context, @NonNull DialerExecutorFactory factory) {
     79     this(context, new Random(), factory);
     80   }
     81 
     82   public SpamCallListListener(
     83       Context context, Random rand, @NonNull DialerExecutorFactory factory) {
     84     this.context = context;
     85     this.random = rand;
     86     this.dialerExecutorFactory = Assert.isNotNull(factory);
     87   }
     88 
     89   /** Checks if the number is in the call history. */
     90   @TargetApi(VERSION_CODES.M)
     91   private static final class NumberInCallHistoryWorker implements Worker<Void, Integer> {
     92 
     93     private final Context appContext;
     94     private final String number;
     95     private final String countryIso;
     96 
     97     public NumberInCallHistoryWorker(
     98         @NonNull Context appContext, String number, String countryIso) {
     99       this.appContext = Assert.isNotNull(appContext);
    100       this.number = number;
    101       this.countryIso = countryIso;
    102     }
    103 
    104     @Override
    105     @NonNull
    106     @CallHistoryStatus
    107     public Integer doInBackground(@Nullable Void input) throws Throwable {
    108       String numberToQuery = number;
    109       String fieldToQuery = Calls.NUMBER;
    110       String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    111 
    112       // If we can normalize the number successfully, look in "normalized_number"
    113       // field instead. Otherwise, look for number in "number" field.
    114       if (!TextUtils.isEmpty(normalizedNumber)) {
    115         numberToQuery = normalizedNumber;
    116         fieldToQuery = Calls.CACHED_NORMALIZED_NUMBER;
    117       }
    118 
    119       try (Cursor cursor =
    120           appContext
    121               .getContentResolver()
    122               .query(
    123                   TelecomUtil.getCallLogUri(appContext),
    124                   new String[] {CallLog.Calls._ID},
    125                   fieldToQuery + " = ?",
    126                   new String[] {numberToQuery},
    127                   null)) {
    128         return cursor != null && cursor.getCount() > 0
    129             ? DialerCall.CALL_HISTORY_STATUS_PRESENT
    130             : DialerCall.CALL_HISTORY_STATUS_NOT_PRESENT;
    131       } catch (SQLiteException e) {
    132         LogUtil.e("NumberInCallHistoryWorker.doInBackground", "query call log error", e);
    133         return DialerCall.CALL_HISTORY_STATUS_UNKNOWN;
    134       }
    135     }
    136   }
    137 
    138   @Override
    139   public void onIncomingCall(final DialerCall call) {
    140     String number = call.getNumber();
    141     if (TextUtils.isEmpty(number)) {
    142       return;
    143     }
    144 
    145     if (!PermissionsUtil.hasCallLogReadPermissions(context)) {
    146       LogUtil.i(
    147           "SpamCallListListener.onIncomingCall",
    148           "call log permission missing, not checking if number is in call history");
    149       return;
    150     }
    151 
    152     NumberInCallHistoryWorker historyTask =
    153         new NumberInCallHistoryWorker(context, number, call.getCountryIso());
    154     dialerExecutorFactory
    155         .createNonUiTaskBuilder(historyTask)
    156         .onSuccess((result) -> call.setCallHistoryStatus(result))
    157         .build()
    158         .executeParallel(null);
    159   }
    160 
    161   @Override
    162   public void onUpgradeToVideo(DialerCall call) {}
    163 
    164   @Override
    165   public void onSessionModificationStateChange(DialerCall call) {}
    166 
    167   @Override
    168   public void onCallListChange(CallList callList) {}
    169 
    170   @Override
    171   public void onWiFiToLteHandover(DialerCall call) {}
    172 
    173   @Override
    174   public void onHandoverToWifiFailed(DialerCall call) {}
    175 
    176   @Override
    177   public void onInternationalCallOnWifi(@NonNull DialerCall call) {}
    178 
    179   @Override
    180   public void onDisconnect(DialerCall call) {
    181     if (!shouldShowAfterCallNotification(call)) {
    182       return;
    183     }
    184     String e164Number =
    185         PhoneNumberUtils.formatNumberToE164(
    186             call.getNumber(), GeoUtil.getCurrentCountryIso(context));
    187     if (!FilteredNumbersUtil.canBlockNumber(context, e164Number, call.getNumber())
    188         || !FilteredNumberCompat.canAttemptBlockOperations(context)) {
    189       return;
    190     }
    191     if (e164Number == null) {
    192       return;
    193     }
    194     showNotification(call);
    195   }
    196 
    197   /** Posts the intent for displaying the after call spam notification to the user. */
    198   private void showNotification(DialerCall call) {
    199     if (call.isSpam()) {
    200       maybeShowSpamCallNotification(call);
    201     } else {
    202       maybeShowNonSpamCallNotification(call);
    203     }
    204   }
    205 
    206   /** Determines if the after call notification should be shown for the specified call. */
    207   private boolean shouldShowAfterCallNotification(DialerCall call) {
    208     if (!SpamComponent.get(context).spam().isSpamNotificationEnabled()) {
    209       return false;
    210     }
    211 
    212     String number = call.getNumber();
    213     if (TextUtils.isEmpty(number)) {
    214       return false;
    215     }
    216 
    217     DialerCall.LogState logState = call.getLogState();
    218     if (!logState.isIncoming) {
    219       return false;
    220     }
    221 
    222     if (logState.duration <= 0) {
    223       return false;
    224     }
    225 
    226     if (logState.contactLookupResult != ContactLookupResult.Type.NOT_FOUND
    227         && logState.contactLookupResult != ContactLookupResult.Type.UNKNOWN_LOOKUP_RESULT_TYPE) {
    228       return false;
    229     }
    230 
    231     int callHistoryStatus = call.getCallHistoryStatus();
    232     if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_PRESENT) {
    233       return false;
    234     } else if (callHistoryStatus == DialerCall.CALL_HISTORY_STATUS_UNKNOWN) {
    235       LogUtil.i("SpamCallListListener.shouldShowAfterCallNotification", "history status unknown");
    236       return false;
    237     }
    238 
    239     // Check if call disconnected because of either user hanging up
    240     int disconnectCause = call.getDisconnectCause().getCode();
    241     if (disconnectCause != DisconnectCause.LOCAL && disconnectCause != DisconnectCause.REMOTE) {
    242       return false;
    243     }
    244 
    245     LogUtil.i("SpamCallListListener.shouldShowAfterCallNotification", "returning true");
    246     return true;
    247   }
    248 
    249   /**
    250    * Creates a notification builder with properties common among the two after call notifications.
    251    */
    252   private Notification.Builder createAfterCallNotificationBuilder(DialerCall call) {
    253     Notification.Builder builder =
    254         new Builder(context)
    255             .setContentIntent(
    256                 createActivityPendingIntent(call, SpamNotificationActivity.ACTION_SHOW_DIALOG))
    257             .setCategory(Notification.CATEGORY_STATUS)
    258             .setPriority(Notification.PRIORITY_DEFAULT)
    259             .setColor(context.getColor(R.color.dialer_theme_color))
    260             .setSmallIcon(R.drawable.quantum_ic_call_end_vd_theme_24)
    261             .setGroup(GROUP_KEY);
    262     if (BuildCompat.isAtLeastO()) {
    263       builder.setChannelId(NotificationChannelId.DEFAULT);
    264     }
    265     return builder;
    266   }
    267 
    268   private CharSequence getDisplayNumber(DialerCall call) {
    269     String formattedNumber =
    270         PhoneNumberHelper.formatNumber(
    271             context, call.getNumber(), GeoUtil.getCurrentCountryIso(context));
    272     return PhoneNumberUtils.createTtsSpannable(formattedNumber);
    273   }
    274 
    275   /** Display a notification with two actions: "add contact" and "report spam". */
    276   private void showNonSpamCallNotification(DialerCall call) {
    277     Notification.Builder notificationBuilder =
    278         createAfterCallNotificationBuilder(call)
    279             .setContentText(
    280                 context.getString(R.string.spam_notification_non_spam_call_collapsed_text))
    281             .setStyle(
    282                 new Notification.BigTextStyle()
    283                     .bigText(
    284                         context.getString(R.string.spam_notification_non_spam_call_expanded_text)))
    285             // Add contact
    286             .addAction(
    287                 new Notification.Action.Builder(
    288                         R.drawable.quantum_ic_person_add_vd_theme_24,
    289                         context.getString(R.string.spam_notification_add_contact_action_text),
    290                         createActivityPendingIntent(
    291                             call, SpamNotificationActivity.ACTION_ADD_TO_CONTACTS))
    292                     .build())
    293             // Block/report spam
    294             .addAction(
    295                 new Notification.Action.Builder(
    296                         R.drawable.quantum_ic_block_vd_theme_24,
    297                         context.getString(R.string.spam_notification_report_spam_action_text),
    298                         createBlockReportSpamPendingIntent(call))
    299                     .build())
    300             .setContentTitle(
    301                 context.getString(R.string.non_spam_notification_title, getDisplayNumber(call)));
    302     DialerNotificationManager.notify(
    303         context, getNotificationTagForCall(call), NOTIFICATION_ID, notificationBuilder.build());
    304   }
    305 
    306   private boolean shouldThrottleSpamNotification() {
    307     int randomNumber = random.nextInt(100);
    308     int thresholdForShowing = SpamComponent.get(context).spam().percentOfSpamNotificationsToShow();
    309     if (thresholdForShowing == 0) {
    310       LogUtil.d(
    311           "SpamCallListListener.shouldThrottleSpamNotification",
    312           "not showing - percentOfSpamNotificationsToShow is 0");
    313       return true;
    314     } else if (randomNumber < thresholdForShowing) {
    315       LogUtil.d(
    316           "SpamCallListListener.shouldThrottleSpamNotification",
    317           "showing " + randomNumber + " < " + thresholdForShowing);
    318       return false;
    319     } else {
    320       LogUtil.d(
    321           "SpamCallListListener.shouldThrottleSpamNotification",
    322           "not showing %d >= %d",
    323           randomNumber,
    324           thresholdForShowing);
    325       return true;
    326     }
    327   }
    328 
    329   private boolean shouldThrottleNonSpamNotification() {
    330     int randomNumber = random.nextInt(100);
    331     int thresholdForShowing =
    332         SpamComponent.get(context).spam().percentOfNonSpamNotificationsToShow();
    333     if (thresholdForShowing == 0) {
    334       LogUtil.d(
    335           "SpamCallListListener.shouldThrottleNonSpamNotification",
    336           "not showing non spam notification: percentOfNonSpamNotificationsToShow is 0");
    337       return true;
    338     } else if (randomNumber < thresholdForShowing) {
    339       LogUtil.d(
    340           "SpamCallListListener.shouldThrottleNonSpamNotification",
    341           "showing non spam notification: %d < %d",
    342           randomNumber,
    343           thresholdForShowing);
    344       return false;
    345     } else {
    346       LogUtil.d(
    347           "SpamCallListListener.shouldThrottleNonSpamNotification",
    348           "not showing non spam notification: %d >= %d",
    349           randomNumber,
    350           thresholdForShowing);
    351       return true;
    352     }
    353   }
    354 
    355   private void maybeShowSpamCallNotification(DialerCall call) {
    356     if (shouldThrottleSpamNotification()) {
    357       Logger.get(context)
    358           .logCallImpression(
    359               DialerImpression.Type.SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
    360               call.getUniqueCallId(),
    361               call.getTimeAddedMs());
    362     } else {
    363       Logger.get(context)
    364           .logCallImpression(
    365               DialerImpression.Type.SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
    366               call.getUniqueCallId(),
    367               call.getTimeAddedMs());
    368       showSpamCallNotification(call);
    369     }
    370   }
    371 
    372   private void maybeShowNonSpamCallNotification(DialerCall call) {
    373     if (shouldThrottleNonSpamNotification()) {
    374       Logger.get(context)
    375           .logCallImpression(
    376               DialerImpression.Type.NON_SPAM_NOTIFICATION_NOT_SHOWN_AFTER_THROTTLE,
    377               call.getUniqueCallId(),
    378               call.getTimeAddedMs());
    379     } else {
    380       Logger.get(context)
    381           .logCallImpression(
    382               DialerImpression.Type.NON_SPAM_NOTIFICATION_SHOWN_AFTER_THROTTLE,
    383               call.getUniqueCallId(),
    384               call.getTimeAddedMs());
    385       showNonSpamCallNotification(call);
    386     }
    387   }
    388 
    389   /** Display a notification with the action "not spam". */
    390   private void showSpamCallNotification(DialerCall call) {
    391     Notification.Builder notificationBuilder =
    392         createAfterCallNotificationBuilder(call)
    393             .setLargeIcon(Icon.createWithResource(context, R.drawable.spam_notification_icon))
    394             .setContentText(context.getString(R.string.spam_notification_spam_call_collapsed_text))
    395             // Not spam
    396             .addAction(
    397                 new Notification.Action.Builder(
    398                         R.drawable.quantum_ic_close_vd_theme_24,
    399                         context.getString(R.string.spam_notification_was_not_spam_action_text),
    400                         createNotSpamPendingIntent(call))
    401                     .build())
    402             // Block/report spam
    403             .addAction(
    404                 new Notification.Action.Builder(
    405                         R.drawable.quantum_ic_block_vd_theme_24,
    406                         context.getString(R.string.spam_notification_block_spam_action_text),
    407                         createBlockReportSpamPendingIntent(call))
    408                     .build())
    409             .setContentTitle(
    410                 context.getString(R.string.spam_notification_title, getDisplayNumber(call)));
    411     DialerNotificationManager.notify(
    412         context, getNotificationTagForCall(call), NOTIFICATION_ID, notificationBuilder.build());
    413   }
    414 
    415   /**
    416    * Creates a pending intent for block/report spam action. If enabled, this intent is forwarded to
    417    * the {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
    418    */
    419   private PendingIntent createBlockReportSpamPendingIntent(DialerCall call) {
    420     String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_SPAM;
    421     return SpamComponent.get(context).spam().isDialogEnabledForSpamNotification()
    422         ? createActivityPendingIntent(call, action)
    423         : createServicePendingIntent(call, action);
    424   }
    425 
    426   /**
    427    * Creates a pending intent for not spam action. If enabled, this intent is forwarded to the
    428    * {@link SpamNotificationActivity}, otherwise to the {@link SpamNotificationService}.
    429    */
    430   private PendingIntent createNotSpamPendingIntent(DialerCall call) {
    431     String action = SpamNotificationActivity.ACTION_MARK_NUMBER_AS_NOT_SPAM;
    432     return SpamComponent.get(context).spam().isDialogEnabledForSpamNotification()
    433         ? createActivityPendingIntent(call, action)
    434         : createServicePendingIntent(call, action);
    435   }
    436 
    437   /** Creates a pending intent for {@link SpamNotificationService}. */
    438   private PendingIntent createServicePendingIntent(DialerCall call, String action) {
    439     Intent intent =
    440         SpamNotificationService.createServiceIntent(
    441             context, call, action, getNotificationTagForCall(call), NOTIFICATION_ID);
    442     return PendingIntent.getService(
    443         context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
    444   }
    445 
    446   /** Creates a pending intent for {@link SpamNotificationActivity}. */
    447   private PendingIntent createActivityPendingIntent(DialerCall call, String action) {
    448     Intent intent =
    449         SpamNotificationActivity.createActivityIntent(
    450             context, call, action, getNotificationTagForCall(call), NOTIFICATION_ID);
    451     return PendingIntent.getActivity(
    452         context, (int) System.currentTimeMillis(), intent, PendingIntent.FLAG_ONE_SHOT);
    453   }
    454 
    455   static String getNotificationTagForCall(@NonNull DialerCall call) {
    456     return NOTIFICATION_TAG_PREFIX + call.getUniqueCallId();
    457   }
    458 }
    459