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.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