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.PendingIntent; 20 import android.content.ContentValues; 21 import android.content.Context; 22 import android.database.Cursor; 23 import android.os.AsyncTask; 24 import android.provider.ContactsContract.CommonDataKinds.Phone; 25 import android.provider.ContactsContract.Contacts; 26 import android.provider.Settings; 27 import android.support.annotation.Nullable; 28 import android.support.annotation.VisibleForTesting; 29 import android.support.v4.os.BuildCompat; 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.DialerNotificationManager; 39 import com.android.dialer.notification.NotificationChannelId; 40 import com.android.dialer.storage.StorageComponent; 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 @Deprecated 46 public class FilteredNumbersUtil { 47 48 public static final String CALL_BLOCKING_NOTIFICATION_TAG = "call_blocking"; 49 public static final int CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID = 10; 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. */ checkForSendToVoicemailContact( final Context context, final CheckForSendToVoicemailContactListener listener)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 */ importSendToVoicemailContacts( final Context context, final ImportSendToVoicemailContactsListener listener)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 getLastEmergencyCallTimeMillis(Context context)190 public static long getLastEmergencyCallTimeMillis(Context context) { 191 return StorageComponent.get(context) 192 .unencryptedSharedPrefs() 193 .getLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, 0); 194 } 195 hasRecentEmergencyCall(Context context)196 public static boolean hasRecentEmergencyCall(Context context) { 197 if (context == null) { 198 return false; 199 } 200 201 Long lastEmergencyCallTime = getLastEmergencyCallTimeMillis(context); 202 if (lastEmergencyCallTime == 0) { 203 return false; 204 } 205 206 return (System.currentTimeMillis() - lastEmergencyCallTime) 207 < getRecentEmergencyCallThresholdMs(context); 208 } 209 recordLastEmergencyCallTime(Context context)210 public static void recordLastEmergencyCallTime(Context context) { 211 if (context == null) { 212 return; 213 } 214 215 StorageComponent.get(context) 216 .unencryptedSharedPrefs() 217 .edit() 218 .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis()) 219 .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false) 220 .apply(); 221 222 if (UserManagerCompat.isUserUnlocked(context)) { 223 maybeNotifyCallBlockingDisabled(context); 224 } 225 } 226 maybeNotifyCallBlockingDisabled(final Context context)227 public static void maybeNotifyCallBlockingDisabled(final Context context) { 228 // The Dialer is not responsible for this notification after migrating 229 if (FilteredNumberCompat.useNewFiltering(context)) { 230 return; 231 } 232 // Skip if the user has already received a notification for the most recent emergency call. 233 if (StorageComponent.get(context) 234 .unencryptedSharedPrefs() 235 .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) { 236 return; 237 } 238 239 // If the user has blocked numbers, notify that call blocking is temporarily disabled. 240 FilteredNumberAsyncQueryHandler queryHandler = new FilteredNumberAsyncQueryHandler(context); 241 queryHandler.hasBlockedNumbers( 242 new OnHasBlockedNumbersListener() { 243 @Override 244 public void onHasBlockedNumbers(boolean hasBlockedNumbers) { 245 if (context == null || !hasBlockedNumbers) { 246 return; 247 } 248 249 Notification.Builder builder = 250 new Notification.Builder(context) 251 .setSmallIcon(R.drawable.quantum_ic_block_white_24) 252 .setContentTitle( 253 context.getString(R.string.call_blocking_disabled_notification_title)) 254 .setContentText( 255 context.getString(R.string.call_blocking_disabled_notification_text)) 256 .setAutoCancel(true); 257 258 if (BuildCompat.isAtLeastO()) { 259 builder.setChannelId(NotificationChannelId.DEFAULT); 260 } 261 builder.setContentIntent( 262 PendingIntent.getActivity( 263 context, 264 0, 265 FilteredNumberCompat.createManageBlockedNumbersIntent(context), 266 PendingIntent.FLAG_UPDATE_CURRENT)); 267 268 DialerNotificationManager.notify( 269 context, 270 CALL_BLOCKING_NOTIFICATION_TAG, 271 CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID, 272 builder.build()); 273 274 // Record that the user has been notified for this emergency call. 275 StorageComponent.get(context) 276 .unencryptedSharedPrefs() 277 .edit() 278 .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true) 279 .apply(); 280 } 281 }); 282 } 283 284 /** 285 * @param e164Number The e164 formatted version of the number, or {@code null} if such a format 286 * doesn't exist. 287 * @param number The number to attempt blocking. 288 * @return {@code true} if the number can be blocked, {@code false} otherwise. 289 */ canBlockNumber(Context context, String e164Number, String number)290 public static boolean canBlockNumber(Context context, String e164Number, String number) { 291 String blockableNumber = getBlockableNumber(context, e164Number, number); 292 return !TextUtils.isEmpty(blockableNumber) 293 && !PhoneNumberUtils.isEmergencyNumber(blockableNumber); 294 } 295 296 /** 297 * @param e164Number The e164 formatted version of the number, or {@code null} if such a format 298 * doesn't exist.. 299 * @param number The number to attempt blocking. 300 * @return The version of the given number that can be blocked with the current blocking solution. 301 */ 302 @Nullable getBlockableNumber( Context context, @Nullable String e164Number, String number)303 public static String getBlockableNumber( 304 Context context, @Nullable String e164Number, String number) { 305 if (!FilteredNumberCompat.useNewFiltering(context)) { 306 return e164Number; 307 } 308 return TextUtils.isEmpty(e164Number) ? number : e164Number; 309 } 310 getRecentEmergencyCallThresholdMs(Context context)311 private static long getRecentEmergencyCallThresholdMs(Context context) { 312 if (LogUtil.isVerboseEnabled()) { 313 long thresholdMs = 314 Settings.System.getLong( 315 context.getContentResolver(), RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0); 316 return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS; 317 } else { 318 return RECENT_EMERGENCY_CALL_THRESHOLD_MS; 319 } 320 } 321 322 public interface CheckForSendToVoicemailContactListener { 323 onComplete(boolean hasSendToVoicemailContact)324 void onComplete(boolean hasSendToVoicemailContact); 325 } 326 327 public interface ImportSendToVoicemailContactsListener { 328 onImportComplete()329 void onImportComplete(); 330 } 331 332 private static class ContactsQuery { 333 334 static final String[] PROJECTION = {Contacts._ID}; 335 336 static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; 337 338 static final int ID_COLUMN_INDEX = 0; 339 } 340 341 public static class PhoneQuery { 342 343 public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER}; 344 345 public static final int ID_COLUMN_INDEX = 0; 346 public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1; 347 public static final int NUMBER_COLUMN_INDEX = 2; 348 349 public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; 350 } 351 } 352