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