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.AlertDialog; 20 import android.app.Dialog; 21 import android.app.DialogFragment; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.content.Intent; 25 import android.os.Bundle; 26 import android.provider.CallLog; 27 import android.provider.ContactsContract; 28 import android.support.v4.app.FragmentActivity; 29 import android.telephony.PhoneNumberUtils; 30 import com.android.dialer.blocking.BlockedNumbersMigrator; 31 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 32 import com.android.dialer.blocking.FilteredNumberCompat; 33 import com.android.dialer.blockreportspam.BlockReportSpamDialogs; 34 import com.android.dialer.common.LogUtil; 35 import com.android.dialer.location.GeoUtil; 36 import com.android.dialer.logging.ContactLookupResult; 37 import com.android.dialer.logging.DialerImpression; 38 import com.android.dialer.logging.Logger; 39 import com.android.dialer.logging.ReportingLocation; 40 import com.android.dialer.notification.DialerNotificationManager; 41 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 42 import com.android.dialer.spam.SpamComponent; 43 import com.android.incallui.call.DialerCall; 44 45 /** Creates the after call notification dialogs. */ 46 public class SpamNotificationActivity extends FragmentActivity { 47 48 /** Action to add number to contacts. */ 49 static final String ACTION_ADD_TO_CONTACTS = "com.android.incallui.spam.ACTION_ADD_TO_CONTACTS"; 50 /** Action to show dialog. */ 51 static final String ACTION_SHOW_DIALOG = "com.android.incallui.spam.ACTION_SHOW_DIALOG"; 52 /** Action to mark a number as spam. */ 53 static final String ACTION_MARK_NUMBER_AS_SPAM = 54 "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_SPAM"; 55 /** Action to mark a number as not spam. */ 56 static final String ACTION_MARK_NUMBER_AS_NOT_SPAM = 57 "com.android.incallui.spam.ACTION_MARK_NUMBER_AS_NOT_SPAM"; 58 59 private static final String TAG = "SpamNotifications"; 60 private static final String EXTRA_NOTIFICATION_TAG = "notification_tag"; 61 private static final String EXTRA_NOTIFICATION_ID = "notification_id"; 62 private static final String EXTRA_CALL_INFO = "call_info"; 63 64 private static final String CALL_INFO_KEY_PHONE_NUMBER = "phone_number"; 65 private static final String CALL_INFO_KEY_IS_SPAM = "is_spam"; 66 private static final String CALL_INFO_KEY_CALL_ID = "call_id"; 67 private static final String CALL_INFO_KEY_START_TIME_MILLIS = "call_start_time_millis"; 68 private static final String CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE = "contact_lookup_result_type"; 69 private final DialogInterface.OnDismissListener dismissListener = 70 new DialogInterface.OnDismissListener() { 71 @Override 72 public void onDismiss(DialogInterface dialog) { 73 if (!isFinishing()) { 74 finish(); 75 } 76 } 77 }; 78 private FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler; 79 80 /** 81 * Creates an intent to start this activity. 82 * 83 * @return Intent intent that starts this activity. 84 */ createActivityIntent( Context context, DialerCall call, String action, String notificationTag, int notificationId)85 public static Intent createActivityIntent( 86 Context context, DialerCall call, String action, String notificationTag, int notificationId) { 87 Intent intent = new Intent(context, SpamNotificationActivity.class); 88 intent.setAction(action); 89 // This ensures only one activity of this kind exists at a time. 90 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); 91 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 92 intent.putExtra(EXTRA_NOTIFICATION_TAG, notificationTag); 93 intent.putExtra(EXTRA_NOTIFICATION_ID, notificationId); 94 intent.putExtra(EXTRA_CALL_INFO, newCallInfoBundle(call)); 95 return intent; 96 } 97 98 /** Creates the intent to insert a contact. */ createInsertContactsIntent(String number)99 private static Intent createInsertContactsIntent(String number) { 100 Intent intent = new Intent(ContactsContract.Intents.Insert.ACTION); 101 // This ensures that the edit contact number field gets updated if called more than once. 102 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 103 intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 104 intent.setType(ContactsContract.RawContacts.CONTENT_TYPE); 105 intent.putExtra(ContactsContract.Intents.Insert.PHONE, number); 106 return intent; 107 } 108 109 /** Returns the formatted version of the given number. */ getFormattedNumber(String number, Context context)110 private static String getFormattedNumber(String number, Context context) { 111 String formattedNumber = 112 PhoneNumberHelper.formatNumber(context, number, GeoUtil.getCurrentCountryIso(context)); 113 return PhoneNumberUtils.createTtsSpannable(formattedNumber).toString(); 114 } 115 logCallImpression(DialerImpression.Type impression)116 private void logCallImpression(DialerImpression.Type impression) { 117 logCallImpression(this, getCallInfo(), impression); 118 } 119 logCallImpression( Context context, Bundle bundle, DialerImpression.Type impression)120 private static void logCallImpression( 121 Context context, Bundle bundle, DialerImpression.Type impression) { 122 Logger.get(context) 123 .logCallImpression( 124 impression, 125 bundle.getString(CALL_INFO_KEY_CALL_ID), 126 bundle.getLong(CALL_INFO_KEY_START_TIME_MILLIS, 0)); 127 } 128 newCallInfoBundle(DialerCall call)129 private static Bundle newCallInfoBundle(DialerCall call) { 130 Bundle bundle = new Bundle(); 131 bundle.putString(CALL_INFO_KEY_PHONE_NUMBER, call.getNumber()); 132 bundle.putBoolean(CALL_INFO_KEY_IS_SPAM, call.isSpam()); 133 bundle.putString(CALL_INFO_KEY_CALL_ID, call.getUniqueCallId()); 134 bundle.putLong(CALL_INFO_KEY_START_TIME_MILLIS, call.getTimeAddedMs()); 135 bundle.putInt( 136 CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, call.getLogState().contactLookupResult.getNumber()); 137 return bundle; 138 } 139 140 @Override onCreate(Bundle savedInstanceState)141 protected void onCreate(Bundle savedInstanceState) { 142 LogUtil.i(TAG, "onCreate"); 143 super.onCreate(savedInstanceState); 144 setFinishOnTouchOutside(true); 145 filteredNumberAsyncQueryHandler = new FilteredNumberAsyncQueryHandler(this); 146 cancelNotification(); 147 } 148 149 @Override onResume()150 protected void onResume() { 151 LogUtil.i(TAG, "onResume"); 152 super.onResume(); 153 Intent intent = getIntent(); 154 String number = getCallInfo().getString(CALL_INFO_KEY_PHONE_NUMBER); 155 boolean isSpam = getCallInfo().getBoolean(CALL_INFO_KEY_IS_SPAM); 156 ContactLookupResult.Type contactLookupResultType = 157 ContactLookupResult.Type.forNumber( 158 getCallInfo().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0)); 159 switch (intent.getAction()) { 160 case ACTION_ADD_TO_CONTACTS: 161 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ADD_TO_CONTACTS); 162 startActivity(createInsertContactsIntent(number)); 163 finish(); 164 break; 165 case ACTION_MARK_NUMBER_AS_SPAM: 166 assertDialogsEnabled(); 167 maybeShowBlockReportSpamDialog(number, contactLookupResultType); 168 break; 169 case ACTION_MARK_NUMBER_AS_NOT_SPAM: 170 assertDialogsEnabled(); 171 maybeShowNotSpamDialog(number, contactLookupResultType); 172 break; 173 case ACTION_SHOW_DIALOG: 174 if (isSpam) { 175 showSpamFullDialog(); 176 } else { 177 showNonSpamDialog(); 178 } 179 break; 180 default: // fall out 181 } 182 } 183 184 @Override onPause()185 protected void onPause() { 186 LogUtil.d(TAG, "onPause"); 187 // Finish activity on pause (e.g: orientation change or back button pressed) 188 filteredNumberAsyncQueryHandler = null; 189 if (!isFinishing()) { 190 finish(); 191 } 192 super.onPause(); 193 } 194 195 /** Creates and displays the dialog for whitelisting a number. */ maybeShowNotSpamDialog( final String number, final ContactLookupResult.Type contactLookupResultType)196 private void maybeShowNotSpamDialog( 197 final String number, final ContactLookupResult.Type contactLookupResultType) { 198 if (SpamComponent.get(this).spam().isDialogEnabledForSpamNotification()) { 199 BlockReportSpamDialogs.ReportNotSpamDialogFragment.newInstance( 200 getFormattedNumber(number, this), 201 new BlockReportSpamDialogs.OnConfirmListener() { 202 @Override 203 public void onClick() { 204 reportNotSpamAndFinish(number, contactLookupResultType); 205 } 206 }, 207 dismissListener) 208 .show(getFragmentManager(), BlockReportSpamDialogs.NOT_SPAM_DIALOG_TAG); 209 } else { 210 reportNotSpamAndFinish(number, contactLookupResultType); 211 } 212 } 213 214 /** Creates and displays the dialog for blocking/reporting a number as spam. */ maybeShowBlockReportSpamDialog( final String number, final ContactLookupResult.Type contactLookupResultType)215 private void maybeShowBlockReportSpamDialog( 216 final String number, final ContactLookupResult.Type contactLookupResultType) { 217 if (SpamComponent.get(this).spam().isDialogEnabledForSpamNotification()) { 218 String displayNumber = getFormattedNumber(number, this); 219 maybeShowBlockNumberMigrationDialog( 220 new BlockedNumbersMigrator.Listener() { 221 @Override 222 public void onComplete() { 223 BlockReportSpamDialogs.BlockReportSpamDialogFragment.newInstance( 224 displayNumber, 225 SpamComponent.get(SpamNotificationActivity.this) 226 .spam() 227 .isDialogReportSpamCheckedByDefault(), 228 new BlockReportSpamDialogs.OnSpamDialogClickListener() { 229 @Override 230 public void onClick(boolean isSpamChecked) { 231 blockReportNumberAndFinish( 232 number, isSpamChecked, contactLookupResultType); 233 } 234 }, 235 dismissListener) 236 .show(getFragmentManager(), BlockReportSpamDialogs.BLOCK_REPORT_SPAM_DIALOG_TAG); 237 } 238 }); 239 } else { 240 blockReportNumberAndFinish(number, true, contactLookupResultType); 241 } 242 } 243 244 /** 245 * Displays the dialog for the first time unknown calls with actions "Add contact", "Block/report 246 * spam", and "Dismiss". 247 */ showNonSpamDialog()248 private void showNonSpamDialog() { 249 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_NON_SPAM_DIALOG); 250 FirstTimeNonSpamCallDialogFragment.newInstance(getCallInfo()) 251 .show(getFragmentManager(), FirstTimeNonSpamCallDialogFragment.TAG); 252 } 253 254 /** 255 * Displays the dialog for first time spam calls with actions "Not spam", "Block", and "Dismiss". 256 */ showSpamFullDialog()257 private void showSpamFullDialog() { 258 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_SHOW_SPAM_DIALOG); 259 FirstTimeSpamCallDialogFragment.newInstance(getCallInfo()) 260 .show(getFragmentManager(), FirstTimeSpamCallDialogFragment.TAG); 261 } 262 263 /** Checks if the user has migrated to the new blocking and display a dialog if necessary. */ maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener)264 private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) { 265 if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog( 266 this, getFragmentManager(), listener)) { 267 listener.onComplete(); 268 } 269 } 270 271 /** Block and report the number as spam. */ blockReportNumberAndFinish( String number, boolean reportAsSpam, ContactLookupResult.Type contactLookupResultType)272 private void blockReportNumberAndFinish( 273 String number, boolean reportAsSpam, ContactLookupResult.Type contactLookupResultType) { 274 if (reportAsSpam) { 275 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_MARKED_NUMBER_AS_SPAM); 276 SpamComponent.get(this) 277 .spam() 278 .reportSpamFromAfterCallNotification( 279 number, 280 getCountryIso(), 281 CallLog.Calls.INCOMING_TYPE, 282 ReportingLocation.Type.FEEDBACK_PROMPT, 283 contactLookupResultType); 284 } 285 286 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_BLOCK_NUMBER); 287 filteredNumberAsyncQueryHandler.blockNumber(null, number, getCountryIso()); 288 // TODO: DialerCall finish() after block/reporting async tasks complete (a bug) 289 finish(); 290 } 291 292 /** Report the number as not spam. */ reportNotSpamAndFinish( String number, ContactLookupResult.Type contactLookupResultType)293 private void reportNotSpamAndFinish( 294 String number, ContactLookupResult.Type contactLookupResultType) { 295 logCallImpression(DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_REPORT_NUMBER_AS_NOT_SPAM); 296 SpamComponent.get(this) 297 .spam() 298 .reportNotSpamFromAfterCallNotification( 299 number, 300 getCountryIso(), 301 CallLog.Calls.INCOMING_TYPE, 302 ReportingLocation.Type.FEEDBACK_PROMPT, 303 contactLookupResultType); 304 // TODO: DialerCall finish() after async task completes (a bug) 305 finish(); 306 } 307 308 /** Cancels the notification associated with the number. */ cancelNotification()309 private void cancelNotification() { 310 String notificationTag = getIntent().getStringExtra(EXTRA_NOTIFICATION_TAG); 311 int notificationId = getIntent().getIntExtra(EXTRA_NOTIFICATION_ID, 1); 312 DialerNotificationManager.cancel(this, notificationTag, notificationId); 313 } 314 getCountryIso()315 private String getCountryIso() { 316 return GeoUtil.getCurrentCountryIso(this); 317 } 318 assertDialogsEnabled()319 private void assertDialogsEnabled() { 320 if (!SpamComponent.get(this).spam().isDialogEnabledForSpamNotification()) { 321 throw new IllegalStateException( 322 "Cannot start this activity with given action because dialogs are not enabled."); 323 } 324 } 325 getCallInfo()326 private Bundle getCallInfo() { 327 return getIntent().getBundleExtra(EXTRA_CALL_INFO); 328 } 329 330 /** Dialog that displays "Not spam", "Block/report spam" and "Dismiss". */ 331 public static class FirstTimeSpamCallDialogFragment extends DialogFragment { 332 333 public static final String TAG = "FirstTimeSpamDialog"; 334 335 private boolean dismissed; 336 private Context applicationContext; 337 newInstance(Bundle bundle)338 private static DialogFragment newInstance(Bundle bundle) { 339 FirstTimeSpamCallDialogFragment fragment = new FirstTimeSpamCallDialogFragment(); 340 fragment.setArguments(bundle); 341 return fragment; 342 } 343 344 @Override onPause()345 public void onPause() { 346 dismiss(); 347 super.onPause(); 348 } 349 350 @Override onDismiss(DialogInterface dialog)351 public void onDismiss(DialogInterface dialog) { 352 logCallImpression( 353 applicationContext, 354 getArguments(), 355 DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_SPAM_DIALOG); 356 super.onDismiss(dialog); 357 // If dialog was not dismissed by user pressing one of the buttons, finish activity 358 if (!dismissed && getActivity() != null && !getActivity().isFinishing()) { 359 getActivity().finish(); 360 } 361 } 362 363 @Override onAttach(Context context)364 public void onAttach(Context context) { 365 super.onAttach(context); 366 applicationContext = context.getApplicationContext(); 367 } 368 369 @Override onCreateDialog(Bundle savedInstanceState)370 public Dialog onCreateDialog(Bundle savedInstanceState) { 371 super.onCreateDialog(savedInstanceState); 372 final SpamNotificationActivity spamNotificationActivity = 373 (SpamNotificationActivity) getActivity(); 374 final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER); 375 final ContactLookupResult.Type contactLookupResultType = 376 ContactLookupResult.Type.forNumber( 377 getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0)); 378 379 return new AlertDialog.Builder(getActivity()) 380 .setCancelable(false) 381 .setTitle( 382 getString( 383 R.string.spam_notification_title, getFormattedNumber(number, applicationContext))) 384 .setNeutralButton( 385 getString(R.string.spam_notification_action_dismiss), 386 new DialogInterface.OnClickListener() { 387 @Override 388 public void onClick(DialogInterface dialog, int which) { 389 dismiss(); 390 } 391 }) 392 .setPositiveButton( 393 getString(R.string.spam_notification_block_spam_action_text), 394 new DialogInterface.OnClickListener() { 395 @Override 396 public void onClick(DialogInterface dialog, int which) { 397 dismissed = true; 398 dismiss(); 399 spamNotificationActivity.maybeShowBlockReportSpamDialog( 400 number, contactLookupResultType); 401 } 402 }) 403 .setNegativeButton( 404 getString(R.string.spam_notification_was_not_spam_action_text), 405 new DialogInterface.OnClickListener() { 406 @Override 407 public void onClick(DialogInterface dialog, int which) { 408 dismissed = true; 409 dismiss(); 410 spamNotificationActivity.maybeShowNotSpamDialog(number, contactLookupResultType); 411 } 412 }) 413 .create(); 414 } 415 } 416 417 /** Dialog that displays "Add contact", "Block/report spam" and "Dismiss". */ 418 public static class FirstTimeNonSpamCallDialogFragment extends DialogFragment { 419 420 public static final String TAG = "FirstTimeNonSpamDialog"; 421 422 private boolean dismissed; 423 private Context context; 424 425 private static DialogFragment newInstance(Bundle bundle) { 426 FirstTimeNonSpamCallDialogFragment fragment = new FirstTimeNonSpamCallDialogFragment(); 427 fragment.setArguments(bundle); 428 return fragment; 429 } 430 431 @Override 432 public void onPause() { 433 // Dismiss on pause e.g: orientation change 434 dismiss(); 435 super.onPause(); 436 } 437 438 @Override 439 public void onDismiss(DialogInterface dialog) { 440 super.onDismiss(dialog); 441 logCallImpression( 442 context, 443 getArguments(), 444 DialerImpression.Type.SPAM_AFTER_CALL_NOTIFICATION_ON_DISMISS_NON_SPAM_DIALOG); 445 // If dialog was not dismissed by user pressing one of the buttons, finish activity 446 if (!dismissed && getActivity() != null && !getActivity().isFinishing()) { 447 getActivity().finish(); 448 } 449 } 450 451 @Override 452 public void onAttach(Context context) { 453 super.onAttach(context); 454 this.context = context.getApplicationContext(); 455 } 456 457 @Override 458 public Dialog onCreateDialog(Bundle savedInstanceState) { 459 super.onCreateDialog(savedInstanceState); 460 final SpamNotificationActivity spamNotificationActivity = 461 (SpamNotificationActivity) getActivity(); 462 final String number = getArguments().getString(CALL_INFO_KEY_PHONE_NUMBER); 463 final ContactLookupResult.Type contactLookupResultType = 464 ContactLookupResult.Type.forNumber( 465 getArguments().getInt(CALL_INFO_CONTACT_LOOKUP_RESULT_TYPE, 0)); 466 return new AlertDialog.Builder(getActivity()) 467 .setTitle( 468 getString(R.string.non_spam_notification_title, getFormattedNumber(number, context))) 469 .setCancelable(false) 470 .setMessage(getString(R.string.spam_notification_non_spam_call_expanded_text)) 471 .setNeutralButton( 472 getString(R.string.spam_notification_action_dismiss), 473 new DialogInterface.OnClickListener() { 474 @Override 475 public void onClick(DialogInterface dialog, int which) { 476 dismiss(); 477 } 478 }) 479 .setPositiveButton( 480 getString(R.string.spam_notification_dialog_add_contact_action_text), 481 new DialogInterface.OnClickListener() { 482 @Override 483 public void onClick(DialogInterface dialog, int which) { 484 dismissed = true; 485 dismiss(); 486 startActivity(createInsertContactsIntent(number)); 487 } 488 }) 489 .setNegativeButton( 490 getString(R.string.spam_notification_dialog_block_report_spam_action_text), 491 new DialogInterface.OnClickListener() { 492 @Override 493 public void onClick(DialogInterface dialog, int which) { 494 dismissed = true; 495 dismiss(); 496 spamNotificationActivity.maybeShowBlockReportSpamDialog( 497 number, contactLookupResultType); 498 } 499 }) 500 .create(); 501 } 502 } 503 } 504