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