Home | History | Annotate | Download | only in blocking
      1 /*
      2  * Copyright (C) 2015 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 package com.android.dialer.blocking;
     17 
     18 import android.app.Notification;
     19 import android.app.NotificationManager;
     20 import android.app.PendingIntent;
     21 import android.content.ContentValues;
     22 import android.content.Context;
     23 import android.database.Cursor;
     24 import android.os.AsyncTask;
     25 import android.provider.ContactsContract.CommonDataKinds.Phone;
     26 import android.provider.ContactsContract.Contacts;
     27 import android.provider.Settings;
     28 import android.support.annotation.Nullable;
     29 import android.support.annotation.VisibleForTesting;
     30 import android.support.v4.os.UserManagerCompat;
     31 import android.telephony.PhoneNumberUtils;
     32 import android.text.TextUtils;
     33 import android.widget.Toast;
     34 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnHasBlockedNumbersListener;
     35 import com.android.dialer.common.LogUtil;
     36 import com.android.dialer.logging.InteractionEvent;
     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.util.DialerUtils;
     41 import com.android.dialer.util.PermissionsUtil;
     42 import java.util.concurrent.TimeUnit;
     43 
     44 /** Utility to help with tasks related to filtered numbers. */
     45 public class FilteredNumbersUtil {
     46 
     47   public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking";
     48   public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID =
     49       R.id.notification_call_blocking_disabled_by_emergency_call;
     50   // Pref key for storing the time of end of the last emergency call in milliseconds after epoch.\
     51   @VisibleForTesting
     52   public static final String LAST_EMERGENCY_CALL_MS_PREF_KEY = "last_emergency_call_ms";
     53   // Pref key for storing whether a notification has been dispatched to notify the user that call
     54   // blocking has been disabled because of a recent emergency call.
     55   protected static final String NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY =
     56       "notified_call_blocking_disabled_by_emergency_call";
     57   // Disable incoming call blocking if there was a call within the past 2 days.
     58   static final long RECENT_EMERGENCY_CALL_THRESHOLD_MS = TimeUnit.DAYS.toMillis(2);
     59 
     60   /**
     61    * Used for testing to specify the custom threshold value, in milliseconds for whether an
     62    * emergency call is "recent". The default value will be used if this custom threshold is less
     63    * than zero. For example, to set this threshold to 60 seconds:
     64    *
     65    * <p>adb shell settings put system dialer_emergency_call_threshold_ms 60000
     66    */
     67   private static final String RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY =
     68       "dialer_emergency_call_threshold_ms";
     69 
     70   /** Checks if there exists a contact with {@code Contacts.SEND_TO_VOICEMAIL} set to true. */
     71   public static void checkForSendToVoicemailContact(
     72       final Context context, final CheckForSendToVoicemailContactListener listener) {
     73     final AsyncTask task =
     74         new AsyncTask<Object, Void, Boolean>() {
     75           @Override
     76           public Boolean doInBackground(Object... params) {
     77             if (context == null || !PermissionsUtil.hasContactsReadPermissions(context)) {
     78               return false;
     79             }
     80 
     81             final Cursor cursor =
     82                 context
     83                     .getContentResolver()
     84                     .query(
     85                         Contacts.CONTENT_URI,
     86                         ContactsQuery.PROJECTION,
     87                         ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
     88                         null,
     89                         null);
     90 
     91             boolean hasSendToVoicemailContacts = false;
     92             if (cursor != null) {
     93               try {
     94                 hasSendToVoicemailContacts = cursor.getCount() > 0;
     95               } finally {
     96                 cursor.close();
     97               }
     98             }
     99 
    100             return hasSendToVoicemailContacts;
    101           }
    102 
    103           @Override
    104           public void onPostExecute(Boolean hasSendToVoicemailContact) {
    105             if (listener != null) {
    106               listener.onComplete(hasSendToVoicemailContact);
    107             }
    108           }
    109         };
    110     task.execute();
    111   }
    112 
    113   /**
    114    * Blocks all the phone numbers of any contacts marked as SEND_TO_VOICEMAIL, then clears the
    115    * SEND_TO_VOICEMAIL flag on those contacts.
    116    */
    117   public static void importSendToVoicemailContacts(
    118       final Context context, final ImportSendToVoicemailContactsListener listener) {
    119     Logger.get(context).logInteraction(InteractionEvent.Type.IMPORT_SEND_TO_VOICEMAIL);
    120     final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler =
    121         new FilteredNumberAsyncQueryHandler(context);
    122 
    123     final AsyncTask<Object, Void, Boolean> task =
    124         new AsyncTask<Object, Void, Boolean>() {
    125           @Override
    126           public Boolean doInBackground(Object... params) {
    127             if (context == null) {
    128               return false;
    129             }
    130 
    131             // Get the phone number of contacts marked as SEND_TO_VOICEMAIL.
    132             final Cursor phoneCursor =
    133                 context
    134                     .getContentResolver()
    135                     .query(
    136                         Phone.CONTENT_URI,
    137                         PhoneQuery.PROJECTION,
    138                         PhoneQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
    139                         null,
    140                         null);
    141 
    142             if (phoneCursor == null) {
    143               return false;
    144             }
    145 
    146             try {
    147               while (phoneCursor.moveToNext()) {
    148                 final String normalizedNumber =
    149                     phoneCursor.getString(PhoneQuery.NORMALIZED_NUMBER_COLUMN_INDEX);
    150                 final String number = phoneCursor.getString(PhoneQuery.NUMBER_COLUMN_INDEX);
    151                 if (normalizedNumber != null) {
    152                   // Block the phone number of the contact.
    153                   mFilteredNumberAsyncQueryHandler.blockNumber(
    154                       null, normalizedNumber, number, null);
    155                 }
    156               }
    157             } finally {
    158               phoneCursor.close();
    159             }
    160 
    161             // Clear SEND_TO_VOICEMAIL on all contacts. The setting has been imported to Dialer.
    162             ContentValues newValues = new ContentValues();
    163             newValues.put(Contacts.SEND_TO_VOICEMAIL, 0);
    164             context
    165                 .getContentResolver()
    166                 .update(
    167                     Contacts.CONTENT_URI,
    168                     newValues,
    169                     ContactsQuery.SELECT_SEND_TO_VOICEMAIL_TRUE,
    170                     null);
    171 
    172             return true;
    173           }
    174 
    175           @Override
    176           public void onPostExecute(Boolean success) {
    177             if (success) {
    178               if (listener != null) {
    179                 listener.onImportComplete();
    180               }
    181             } else if (context != null) {
    182               String toastStr = context.getString(R.string.send_to_voicemail_import_failed);
    183               Toast.makeText(context, toastStr, Toast.LENGTH_SHORT).show();
    184             }
    185           }
    186         };
    187     task.execute();
    188   }
    189 
    190   public static long getLastEmergencyCallTimeMillis(Context context) {
    191     return DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
    192         .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0);
    193   }
    194 
    195   public static boolean hasRecentEmergencyCall(Context context) {
    196     if (context == null) {
    197       return false;
    198     }
    199 
    200     Long lastEmergencyCallTime = getLastEmergencyCallTimeMillis(context);
    201     if (lastEmergencyCallTime == 0) {
    202       return false;
    203     }
    204 
    205     return (System.currentTimeMillis() - lastEmergencyCallTime)
    206         < getRecentEmergencyCallThresholdMs(context);
    207   }
    208 
    209   public static void recordLastEmergencyCallTime(Context context) {
    210     if (context == null) {
    211       return;
    212     }
    213 
    214     DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
    215         .edit()
    216         .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis())
    217         .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)
    218         .apply();
    219 
    220     if (UserManagerCompat.isUserUnlocked(context)) {
    221       maybeNotifyCallBlockingDisabled(context);
    222     }
    223   }
    224 
    225   public static void maybeNotifyCallBlockingDisabled(final Context context) {
    226     // The Dialer is not responsible for this notification after migrating
    227     if (FilteredNumberCompat.useNewFiltering(context)) {
    228       return;
    229     }
    230     // Skip if the user has already received a notification for the most recent emergency call.
    231     if (DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
    232         .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) {
    233       return;
    234     }
    235 
    236     // If the user has blocked numbers, notify that call blocking is temporarily disabled.
    237     FilteredNumberAsyncQueryHandler queryHandler = new FilteredNumberAsyncQueryHandler(context);
    238     queryHandler.hasBlockedNumbers(
    239         new OnHasBlockedNumbersListener() {
    240           @Override
    241           public void onHasBlockedNumbers(boolean hasBlockedNumbers) {
    242             if (context == null || !hasBlockedNumbers) {
    243               return;
    244             }
    245 
    246             NotificationManager notificationManager =
    247                 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    248             Notification.Builder builder =
    249                 new Notification.Builder(context)
    250                     .setSmallIcon(R.drawable.quantum_ic_block_white_24)
    251                     .setContentTitle(
    252                         context.getString(R.string.call_blocking_disabled_notification_title))
    253                     .setContentText(
    254                         context.getString(R.string.call_blocking_disabled_notification_text))
    255                     .setAutoCancel(true);
    256 
    257             NotificationChannelManager.applyChannel(builder, context, Channel.DEFAULT, null);
    258             builder.setContentIntent(
    259                 PendingIntent.getActivity(
    260                     context,
    261                     0,
    262                     FilteredNumberCompat.createManageBlockedNumbersIntent(context),
    263                     PendingIntent.FLAG_UPDATE_CURRENT));
    264 
    265             notificationManager.notify(
    266                 CALL_BLOCKING_NOTIFICATION_TAG,
    267                 CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID,
    268                 builder.build());
    269 
    270             // Record that the user has been notified for this emergency call.
    271             DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context)
    272                 .edit()
    273                 .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true)
    274                 .apply();
    275           }
    276         });
    277   }
    278 
    279   /**
    280    * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
    281    *     doesn't exist.
    282    * @param number The number to attempt blocking.
    283    * @return {@code true} if the number can be blocked, {@code false} otherwise.
    284    */
    285   public static boolean canBlockNumber(Context context, String e164Number, String number) {
    286     String blockableNumber = getBlockableNumber(context, e164Number, number);
    287     return !TextUtils.isEmpty(blockableNumber)
    288         && !PhoneNumberUtils.isEmergencyNumber(blockableNumber);
    289   }
    290 
    291   /**
    292    * @param e164Number The e164 formatted version of the number, or {@code null} if such a format
    293    *     doesn't exist..
    294    * @param number The number to attempt blocking.
    295    * @return The version of the given number that can be blocked with the current blocking solution.
    296    */
    297   @Nullable
    298   public static String getBlockableNumber(
    299       Context context, @Nullable String e164Number, String number) {
    300     if (!FilteredNumberCompat.useNewFiltering(context)) {
    301       return e164Number;
    302     }
    303     return TextUtils.isEmpty(e164Number) ? number : e164Number;
    304   }
    305 
    306   private static long getRecentEmergencyCallThresholdMs(Context context) {
    307     if (LogUtil.isVerboseEnabled()) {
    308       long thresholdMs =
    309           Settings.System.getLong(
    310               context.getContentResolver(), RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0);
    311       return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS;
    312     } else {
    313       return RECENT_EMERGENCY_CALL_THRESHOLD_MS;
    314     }
    315   }
    316 
    317   public interface CheckForSendToVoicemailContactListener {
    318 
    319     void onComplete(boolean hasSendToVoicemailContact);
    320   }
    321 
    322   public interface ImportSendToVoicemailContactsListener {
    323 
    324     void onImportComplete();
    325   }
    326 
    327   private static class ContactsQuery {
    328 
    329     static final String[] PROJECTION = {Contacts._ID};
    330 
    331     static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
    332 
    333     static final int ID_COLUMN_INDEX = 0;
    334   }
    335 
    336   public static class PhoneQuery {
    337 
    338     public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER};
    339 
    340     public static final int ID_COLUMN_INDEX = 0;
    341     public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1;
    342     public static final int NUMBER_COLUMN_INDEX = 2;
    343 
    344     public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1";
    345   }
    346 }
    347