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.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.NotificationChannelManager; 39 import com.android.dialer.notification.NotificationChannelManager.Channel; 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 = 49 R.id.notification_call_blocking_disabled_by_emergency_call; 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 DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context) 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 DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context) 215 .edit() 216 .putLong(LAST_EMERGENCY_CALL_MS_PREF_KEY, System.currentTimeMillis()) 217 .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false) 218 .apply(); 219 220 if (UserManagerCompat.isUserUnlocked(context)) { 221 maybeNotifyCallBlockingDisabled(context); 222 } 223 } 224 maybeNotifyCallBlockingDisabled(final Context context)225 public static void maybeNotifyCallBlockingDisabled(final Context context) { 226 // The Dialer is not responsible for this notification after migrating 227 if (FilteredNumberCompat.useNewFiltering(context)) { 228 return; 229 } 230 // Skip if the user has already received a notification for the most recent emergency call. 231 if (DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context) 232 .getBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, false)) { 233 return; 234 } 235 236 // If the user has blocked numbers, notify that call blocking is temporarily disabled. 237 FilteredNumberAsyncQueryHandler queryHandler = new FilteredNumberAsyncQueryHandler(context); 238 queryHandler.hasBlockedNumbers( 239 new OnHasBlockedNumbersListener() { 240 @Override 241 public void onHasBlockedNumbers(boolean hasBlockedNumbers) { 242 if (context == null || !hasBlockedNumbers) { 243 return; 244 } 245 246 NotificationManager notificationManager = 247 (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 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 NotificationChannelManager.applyChannel(builder, context, Channel.DEFAULT, null); 258 builder.setContentIntent( 259 PendingIntent.getActivity( 260 context, 261 0, 262 FilteredNumberCompat.createManageBlockedNumbersIntent(context), 263 PendingIntent.FLAG_UPDATE_CURRENT)); 264 265 notificationManager.notify( 266 CALL_BLOCKING_NOTIFICATION_TAG, 267 CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_NOTIFICATION_ID, 268 builder.build()); 269 270 // Record that the user has been notified for this emergency call. 271 DialerUtils.getDefaultSharedPreferenceForDeviceProtectedStorageContext(context) 272 .edit() 273 .putBoolean(NOTIFIED_CALL_BLOCKING_DISABLED_BY_EMERGENCY_CALL_PREF_KEY, true) 274 .apply(); 275 } 276 }); 277 } 278 279 /** 280 * @param e164Number The e164 formatted version of the number, or {@code null} if such a format 281 * doesn't exist. 282 * @param number The number to attempt blocking. 283 * @return {@code true} if the number can be blocked, {@code false} otherwise. 284 */ canBlockNumber(Context context, String e164Number, String number)285 public static boolean canBlockNumber(Context context, String e164Number, String number) { 286 String blockableNumber = getBlockableNumber(context, e164Number, number); 287 return !TextUtils.isEmpty(blockableNumber) 288 && !PhoneNumberUtils.isEmergencyNumber(blockableNumber); 289 } 290 291 /** 292 * @param e164Number The e164 formatted version of the number, or {@code null} if such a format 293 * doesn't exist.. 294 * @param number The number to attempt blocking. 295 * @return The version of the given number that can be blocked with the current blocking solution. 296 */ 297 @Nullable getBlockableNumber( Context context, @Nullable String e164Number, String number)298 public static String getBlockableNumber( 299 Context context, @Nullable String e164Number, String number) { 300 if (!FilteredNumberCompat.useNewFiltering(context)) { 301 return e164Number; 302 } 303 return TextUtils.isEmpty(e164Number) ? number : e164Number; 304 } 305 getRecentEmergencyCallThresholdMs(Context context)306 private static long getRecentEmergencyCallThresholdMs(Context context) { 307 if (LogUtil.isVerboseEnabled()) { 308 long thresholdMs = 309 Settings.System.getLong( 310 context.getContentResolver(), RECENT_EMERGENCY_CALL_THRESHOLD_SETTINGS_KEY, 0); 311 return thresholdMs > 0 ? thresholdMs : RECENT_EMERGENCY_CALL_THRESHOLD_MS; 312 } else { 313 return RECENT_EMERGENCY_CALL_THRESHOLD_MS; 314 } 315 } 316 317 public interface CheckForSendToVoicemailContactListener { 318 onComplete(boolean hasSendToVoicemailContact)319 void onComplete(boolean hasSendToVoicemailContact); 320 } 321 322 public interface ImportSendToVoicemailContactsListener { 323 onImportComplete()324 void onImportComplete(); 325 } 326 327 private static class ContactsQuery { 328 329 static final String[] PROJECTION = {Contacts._ID}; 330 331 static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; 332 333 static final int ID_COLUMN_INDEX = 0; 334 } 335 336 public static class PhoneQuery { 337 338 public static final String[] PROJECTION = {Contacts._ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER}; 339 340 public static final int ID_COLUMN_INDEX = 0; 341 public static final int NORMALIZED_NUMBER_COLUMN_INDEX = 1; 342 public static final int NUMBER_COLUMN_INDEX = 2; 343 344 public static final String SELECT_SEND_TO_VOICEMAIL_TRUE = Contacts.SEND_TO_VOICEMAIL + "=1"; 345 } 346 } 347