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