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