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 { onComplete(boolean hasSendToVoicemailContact)88 public void onComplete(boolean hasSendToVoicemailContact); 89 } 90 91 public interface ImportSendToVoicemailContactsListener { onImportComplete()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 */ checkForSendToVoicemailContact( final Context context, final CheckForSendToVoicemailContactListener listener)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 */ importSendToVoicemailContacts( final Context context, final ImportSendToVoicemailContactsListener listener)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 */ shouldBlockVoicemail( Context context, String number, String countryIso, long voicemailDateMs)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 hasRecentEmergencyCall(Context context)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 recordLastEmergencyCallTime(Context context)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 maybeNotifyCallBlockingDisabled(final Context context)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 canBlockNumber(Context context, String number, String countryIso)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 getRecentEmergencyCallThresholdMs(Context context)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