1 /* 2 * Copyright (C) 2016 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 17 package com.android.dialer.blocking; 18 19 import android.annotation.TargetApi; 20 import android.app.FragmentManager; 21 import android.content.ContentUris; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.net.Uri; 26 import android.os.Build.VERSION; 27 import android.os.Build.VERSION_CODES; 28 import android.os.UserManager; 29 import android.preference.PreferenceManager; 30 import android.provider.BlockedNumberContract; 31 import android.provider.BlockedNumberContract.BlockedNumbers; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.VisibleForTesting; 34 import android.telecom.TelecomManager; 35 import android.telephony.PhoneNumberUtils; 36 import com.android.dialer.common.LogUtil; 37 import com.android.dialer.configprovider.ConfigProviderBindings; 38 import com.android.dialer.database.FilteredNumberContract.FilteredNumber; 39 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; 40 import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources; 41 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; 42 import com.android.dialer.strictmode.StrictModeUtils; 43 import com.android.dialer.telecom.TelecomUtil; 44 import java.util.ArrayList; 45 import java.util.List; 46 import java.util.Objects; 47 48 /** 49 * Compatibility class to encapsulate logic to switch between call blocking using {@link 50 * com.android.dialer.database.FilteredNumberContract} and using {@link 51 * android.provider.BlockedNumberContract}. This class should be used rather than explicitly 52 * referencing columns from either contract class in situations where both blocking solutions may be 53 * used. 54 */ 55 public class FilteredNumberCompat { 56 57 private static Boolean canAttemptBlockOperationsForTest; 58 59 @VisibleForTesting 60 public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking"; 61 62 /** @return The column name for ID in the filtered number database. */ getIdColumnName(Context context)63 public static String getIdColumnName(Context context) { 64 return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID; 65 } 66 67 /** 68 * @return The column name for type in the filtered number database. Will be {@code null} for the 69 * framework blocking implementation. 70 */ 71 @Nullable getTypeColumnName(Context context)72 public static String getTypeColumnName(Context context) { 73 return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE; 74 } 75 76 /** 77 * @return The column name for source in the filtered number database. Will be {@code null} for 78 * the framework blocking implementation 79 */ 80 @Nullable getSourceColumnName(Context context)81 public static String getSourceColumnName(Context context) { 82 return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE; 83 } 84 85 /** @return The column name for the original number in the filtered number database. */ getOriginalNumberColumnName(Context context)86 public static String getOriginalNumberColumnName(Context context) { 87 return useNewFiltering(context) 88 ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER 89 : FilteredNumberColumns.NUMBER; 90 } 91 92 /** 93 * @return The column name for country iso in the filtered number database. Will be {@code null} 94 * the framework blocking implementation 95 */ 96 @Nullable getCountryIsoColumnName(Context context)97 public static String getCountryIsoColumnName(Context context) { 98 return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO; 99 } 100 101 /** @return The column name for the e164 formatted number in the filtered number database. */ getE164NumberColumnName(Context context)102 public static String getE164NumberColumnName(Context context) { 103 return useNewFiltering(context) 104 ? BlockedNumbers.COLUMN_E164_NUMBER 105 : FilteredNumberColumns.NORMALIZED_NUMBER; 106 } 107 108 /** 109 * @return {@code true} if the current SDK version supports using new filtering, {@code false} 110 * otherwise. 111 */ canUseNewFiltering()112 public static boolean canUseNewFiltering() { 113 return VERSION.SDK_INT >= VERSION_CODES.N; 114 } 115 116 /** 117 * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary 118 * migration has been performed, {@code false} otherwise. 119 */ useNewFiltering(Context context)120 public static boolean useNewFiltering(Context context) { 121 return !ConfigProviderBindings.get(context).getBoolean("debug_force_dialer_filtering", false) 122 && canUseNewFiltering() 123 && hasMigratedToNewBlocking(context); 124 } 125 126 /** 127 * @return {@code true} if the user has migrated to use {@link 128 * android.provider.BlockedNumberContract} blocking, {@code false} otherwise. 129 */ hasMigratedToNewBlocking(Context context)130 public static boolean hasMigratedToNewBlocking(Context context) { 131 return StrictModeUtils.bypass( 132 () -> 133 PreferenceManager.getDefaultSharedPreferences(context) 134 .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false)); 135 } 136 137 /** 138 * Called to inform this class whether the user has fully migrated to use {@link 139 * android.provider.BlockedNumberContract} blocking or not. 140 * 141 * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise. 142 */ setHasMigratedToNewBlocking(Context context, boolean hasMigrated)143 public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) { 144 PreferenceManager.getDefaultSharedPreferences(context) 145 .edit() 146 .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated) 147 .apply(); 148 } 149 150 /** 151 * Gets the content {@link Uri} for number filtering. 152 * 153 * @param id The optional id to append with the base content uri. 154 * @return The Uri for number filtering. 155 */ getContentUri(Context context, @Nullable Integer id)156 public static Uri getContentUri(Context context, @Nullable Integer id) { 157 if (id == null) { 158 return getBaseUri(context); 159 } 160 return ContentUris.withAppendedId(getBaseUri(context), id); 161 } 162 getBaseUri(Context context)163 private static Uri getBaseUri(Context context) { 164 // Explicit version check to aid static analysis 165 return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N 166 ? BlockedNumbers.CONTENT_URI 167 : FilteredNumber.CONTENT_URI; 168 } 169 170 /** 171 * Removes any null column names from the given projection array. This method is intended to be 172 * used to strip out any column names that aren't available in every version of number blocking. 173 * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that 174 * no non-existant columns are queried FilteredNumberCompat.filter(new String[] 175 * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()}, 176 * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); } 177 * 178 * @param projection The projection array. 179 * @return The filtered projection array. 180 */ 181 @Nullable filter(@ullable String[] projection)182 public static String[] filter(@Nullable String[] projection) { 183 if (projection == null) { 184 return null; 185 } 186 List<String> filtered = new ArrayList<>(); 187 for (String column : projection) { 188 if (column != null) { 189 filtered.add(column); 190 } 191 } 192 return filtered.toArray(new String[filtered.size()]); 193 } 194 195 /** 196 * Creates a new {@link ContentValues} suitable for inserting in the filtered number table. 197 * 198 * @param number The unformatted number to insert. 199 * @param e164Number (optional) The number to insert formatted to E164 standard. 200 * @param countryIso (optional) The country iso to use to format the number. 201 * @return The ContentValues to insert. 202 * @throws NullPointerException If number is null. 203 */ newBlockNumberContentValues( Context context, String number, @Nullable String e164Number, @Nullable String countryIso)204 public static ContentValues newBlockNumberContentValues( 205 Context context, String number, @Nullable String e164Number, @Nullable String countryIso) { 206 ContentValues contentValues = new ContentValues(); 207 contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number)); 208 if (!useNewFiltering(context)) { 209 if (e164Number == null) { 210 e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 211 } 212 contentValues.put(getE164NumberColumnName(context), e164Number); 213 contentValues.put(getCountryIsoColumnName(context), countryIso); 214 contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER); 215 contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER); 216 } 217 return contentValues; 218 } 219 220 /** 221 * Shows block number migration dialog if necessary. 222 * 223 * @param fragmentManager The {@link FragmentManager} used to show fragments. 224 * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete. 225 * @return boolean True if migration dialog is shown. 226 */ maybeShowBlockNumberMigrationDialog( Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener)227 public static boolean maybeShowBlockNumberMigrationDialog( 228 Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) { 229 if (shouldShowMigrationDialog(context)) { 230 LogUtil.i( 231 "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog", 232 "maybeShowBlockNumberMigrationDialog - showing migration dialog"); 233 MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener) 234 .show(fragmentManager, "MigrateBlockedNumbers"); 235 return true; 236 } 237 return false; 238 } 239 shouldShowMigrationDialog(Context context)240 private static boolean shouldShowMigrationDialog(Context context) { 241 return canUseNewFiltering() && !hasMigratedToNewBlocking(context); 242 } 243 244 /** 245 * Creates the {@link Intent} which opens the blocked numbers management interface. 246 * 247 * @param context The {@link Context}. 248 * @return The intent. 249 */ createManageBlockedNumbersIntent(Context context)250 public static Intent createManageBlockedNumbersIntent(Context context) { 251 // Explicit version check to aid static analysis 252 if (canUseNewFiltering() 253 && hasMigratedToNewBlocking(context) 254 && VERSION.SDK_INT >= VERSION_CODES.N) { 255 return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent(); 256 } 257 Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"); 258 intent.setPackage(context.getPackageName()); 259 return intent; 260 } 261 262 /** 263 * Method used to determine if block operations are possible. 264 * 265 * @param context The {@link Context}. 266 * @return {@code true} if the app and user can block numbers, {@code false} otherwise. 267 */ canAttemptBlockOperations(Context context)268 public static boolean canAttemptBlockOperations(Context context) { 269 if (canAttemptBlockOperationsForTest != null) { 270 return canAttemptBlockOperationsForTest; 271 } 272 273 if (VERSION.SDK_INT < VERSION_CODES.N) { 274 // Dialer blocking, must be primary user 275 return context.getSystemService(UserManager.class).isSystemUser(); 276 } 277 278 // Great Wall blocking, must be primary user and the default or system dialer 279 // TODO(maxwelb): check that we're the system Dialer 280 return TelecomUtil.isDefaultDialer(context) 281 && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); 282 } 283 284 @VisibleForTesting(otherwise = VisibleForTesting.NONE) setCanAttemptBlockOperationsForTest(boolean canAttempt)285 public static void setCanAttemptBlockOperationsForTest(boolean canAttempt) { 286 canAttemptBlockOperationsForTest = canAttempt; 287 } 288 289 /** 290 * Used to determine if the call blocking settings can be opened. 291 * 292 * @param context The {@link Context}. 293 * @return {@code true} if the current user can open the call blocking settings, {@code false} 294 * otherwise. 295 */ canCurrentUserOpenBlockSettings(Context context)296 public static boolean canCurrentUserOpenBlockSettings(Context context) { 297 if (VERSION.SDK_INT < VERSION_CODES.N) { 298 // Dialer blocking, must be primary user 299 return context.getSystemService(UserManager.class).isSystemUser(); 300 } 301 // BlockedNumberContract blocking, verify through Contract API 302 return TelecomUtil.isDefaultDialer(context) 303 && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); 304 } 305 306 /** 307 * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it 308 * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't 309 * available, using this method ensures that the Dialer doesn't crash when on that screen. 310 * 311 * @param context The {@link Context}. 312 * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an 313 * exception was thrown. 314 */ 315 @TargetApi(VERSION_CODES.N) safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context)316 private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) { 317 try { 318 return BlockedNumberContract.canCurrentUserBlockNumbers(context); 319 } catch (Exception e) { 320 LogUtil.e( 321 "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers", 322 "Exception while querying BlockedNumberContract", 323 e); 324 return false; 325 } 326 } 327 } 328