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