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    */
createActivityIntent( Context context, DialerCall call, String action, int notificationId)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. */
createInsertContactsIntent(String number)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. */
getFormattedNumber(String number)108   private static String getFormattedNumber(String number) {
109     return PhoneNumberUtilsCompat.createTtsSpannable(number).toString();
110   }
111 
logCallImpression( Context context, Bundle bundle, DialerImpression.Type impression)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 
newCallInfoBundle(DialerCall call)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
onCreate(Bundle savedInstanceState)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
onResume()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
onPause()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. */
maybeShowNotSpamDialog( final String number, final ContactLookupResult.Type contactLookupResultType)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. */
maybeShowBlockReportSpamDialog( final String number, final ContactLookupResult.Type contactLookupResultType)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    */
showNonSpamDialog()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    */
showSpamFullDialog()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. */
maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener)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. */
blockReportNumberAndFinish( String number, boolean reportAsSpam, ContactLookupResult.Type contactLookupResultType)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. */
reportNotSpamAndFinish( String number, ContactLookupResult.Type contactLookupResultType)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. */
cancelNotification()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 
getCountryIso()303   private String getCountryIso() {
304     return GeoUtil.getCurrentCountryIso(this);
305   }
306 
assertDialogsEnabled()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 
getCallInfo()314   private Bundle getCallInfo() {
315     return getIntent().getBundleExtra(EXTRA_CALL_INFO);
316   }
317 
logCallImpression(DialerImpression.Type impression)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 
newInstance(Bundle bundle)330     private static DialogFragment newInstance(Bundle bundle) {
331       FirstTimeSpamCallDialogFragment fragment = new FirstTimeSpamCallDialogFragment();
332       fragment.setArguments(bundle);
333       return fragment;
334     }
335 
336     @Override
onPause()337     public void onPause() {
338       dismiss();
339       super.onPause();
340     }
341 
342     @Override
onDismiss(DialogInterface dialog)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
onAttach(Context context)356     public void onAttach(Context context) {
357       super.onAttach(context);
358       applicationContext = context.getApplicationContext();
359     }
360 
361     @Override
onCreateDialog(Bundle savedInstanceState)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