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