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