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