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.compat; 18 19 import com.google.common.base.MoreObjects; 20 import com.google.common.base.Preconditions; 21 22 import android.app.FragmentManager; 23 import android.content.ContentResolver; 24 import android.content.ContentUris; 25 import android.content.ContentValues; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.net.Uri; 29 import android.os.UserManager; 30 import android.preference.PreferenceManager; 31 import android.support.annotation.Nullable; 32 import android.telecom.TelecomManager; 33 import android.telephony.PhoneNumberUtils; 34 import android.util.Log; 35 36 import com.android.contacts.common.compat.CompatUtils; 37 import com.android.contacts.common.compat.TelecomManagerUtil; 38 import com.android.contacts.common.testing.NeededForTesting; 39 import com.android.dialer.DialerApplication; 40 import com.android.dialer.database.FilteredNumberAsyncQueryHandler; 41 import com.android.dialer.database.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; 42 import com.android.dialer.database.FilteredNumberContract.FilteredNumber; 43 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; 44 import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources; 45 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; 46 import com.android.dialer.filterednumber.BlockNumberDialogFragment; 47 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback; 48 import com.android.dialer.filterednumber.BlockedNumbersMigrator; 49 import com.android.dialer.filterednumber.BlockedNumbersSettingsActivity; 50 import com.android.dialer.filterednumber.MigrateBlockedNumbersDialogFragment; 51 import com.android.dialerbind.ObjectFactory; 52 53 import java.util.ArrayList; 54 import java.util.List; 55 56 /** 57 * Compatibility class to encapsulate logic to switch between call blocking using 58 * {@link com.android.dialer.database.FilteredNumberContract} and using 59 * {@link android.provider.BlockedNumberContract}. This class should be used rather than explicitly 60 * referencing columns from either contract class in situations where both blocking solutions may be 61 * used. 62 */ 63 public class FilteredNumberCompat { 64 65 private static final String TAG = "FilteredNumberCompat"; 66 67 protected static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking"; 68 69 private static Boolean isEnabledForTest; 70 71 private static Context contextForTest; 72 73 /** 74 * @return The column name for ID in the filtered number database. 75 */ 76 public static String getIdColumnName() { 77 return useNewFiltering() ? BlockedNumbersSdkCompat._ID : FilteredNumberColumns._ID; 78 } 79 80 /** 81 * @return The column name for type in the filtered number database. Will be {@code null} for 82 * the framework blocking implementation. 83 */ 84 @Nullable 85 public static String getTypeColumnName() { 86 return useNewFiltering() ? null : FilteredNumberColumns.TYPE; 87 } 88 89 /** 90 * @return The column name for source in the filtered number database. Will be {@code null} for 91 * the framework blocking implementation 92 */ 93 @Nullable 94 public static String getSourceColumnName() { 95 return useNewFiltering() ? null : FilteredNumberColumns.SOURCE; 96 } 97 98 /** 99 * @return The column name for the original number in the filtered number database. 100 */ 101 public static String getOriginalNumberColumnName() { 102 return useNewFiltering() ? BlockedNumbersSdkCompat.COLUMN_ORIGINAL_NUMBER 103 : FilteredNumberColumns.NUMBER; 104 } 105 106 /** 107 * @return The column name for country iso in the filtered number database. Will be {@code null} 108 * the framework blocking implementation 109 */ 110 @Nullable 111 public static String getCountryIsoColumnName() { 112 return useNewFiltering() ? null : FilteredNumberColumns.COUNTRY_ISO; 113 } 114 115 /** 116 * @return The column name for the e164 formatted number in the filtered number database. 117 */ 118 public static String getE164NumberColumnName() { 119 return useNewFiltering() ? BlockedNumbersSdkCompat.E164_NUMBER 120 : FilteredNumberColumns.NORMALIZED_NUMBER; 121 } 122 123 /** 124 * @return {@code true} if the current SDK version supports using new filtering, {@code false} 125 * otherwise. 126 */ 127 public static boolean canUseNewFiltering() { 128 if (isEnabledForTest != null) { 129 return CompatUtils.isNCompatible() && isEnabledForTest; 130 } 131 return CompatUtils.isNCompatible() && ObjectFactory 132 .isNewBlockingEnabled(DialerApplication.getContext()); 133 } 134 135 /** 136 * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary 137 * migration has been performed, {@code false} otherwise. 138 */ 139 public static boolean useNewFiltering() { 140 return canUseNewFiltering() && hasMigratedToNewBlocking(); 141 } 142 143 /** 144 * @return {@code true} if the user has migrated to use 145 * {@link android.provider.BlockedNumberContract} blocking, {@code false} otherwise. 146 */ 147 public static boolean hasMigratedToNewBlocking() { 148 return PreferenceManager.getDefaultSharedPreferences(DialerApplication.getContext()) 149 .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false); 150 } 151 152 /** 153 * Called to inform this class whether the user has fully migrated to use 154 * {@link android.provider.BlockedNumberContract} blocking or not. 155 * 156 * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise. 157 */ 158 @NeededForTesting 159 public static void setHasMigratedToNewBlocking(boolean hasMigrated) { 160 PreferenceManager.getDefaultSharedPreferences( 161 MoreObjects.firstNonNull(contextForTest, DialerApplication.getContext())).edit() 162 .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated).apply(); 163 } 164 165 @NeededForTesting 166 public static void setIsEnabledForTest(Boolean isEnabled) { 167 isEnabledForTest = isEnabled; 168 } 169 170 @NeededForTesting 171 public static void setContextForTest(Context context) { 172 contextForTest = context; 173 } 174 175 /** 176 * Gets the content {@link Uri} for number filtering. 177 * 178 * @param id The optional id to append with the base content uri. 179 * @return The Uri for number filtering. 180 */ 181 public static Uri getContentUri(@Nullable Integer id) { 182 if (id == null) { 183 return getBaseUri(); 184 } 185 return ContentUris.withAppendedId(getBaseUri(), id); 186 } 187 188 189 private static Uri getBaseUri() { 190 return useNewFiltering() ? BlockedNumbersSdkCompat.CONTENT_URI : FilteredNumber.CONTENT_URI; 191 } 192 193 /** 194 * Removes any null column names from the given projection array. This method is intended to be 195 * used to strip out any column names that aren't available in every version of number blocking. 196 * Example: 197 * {@literal 198 * getContext().getContentResolver().query( 199 * someUri, 200 * // Filtering ensures that no non-existant columns are queried 201 * FilteredNumberCompat.filter(new String[] {FilteredNumberCompat.getIdColumnName(), 202 * FilteredNumberCompat.getTypeColumnName()}, 203 * FilteredNumberCompat.getE164NumberColumnName() + " = ?", 204 * new String[] {e164Number}); 205 * } 206 * 207 * @param projection The projection array. 208 * @return The filtered projection array. 209 */ 210 @Nullable 211 public static String[] filter(@Nullable String[] projection) { 212 if (projection == null) { 213 return null; 214 } 215 List<String> filtered = new ArrayList<>(); 216 for (String column : projection) { 217 if (column != null) { 218 filtered.add(column); 219 } 220 } 221 return filtered.toArray(new String[filtered.size()]); 222 } 223 224 /** 225 * Creates a new {@link ContentValues} suitable for inserting in the filtered number table. 226 * 227 * @param number The unformatted number to insert. 228 * @param e164Number (optional) The number to insert formatted to E164 standard. 229 * @param countryIso (optional) The country iso to use to format the number. 230 * @return The ContentValues to insert. 231 * @throws NullPointerException If number is null. 232 */ 233 public static ContentValues newBlockNumberContentValues(String number, 234 @Nullable String e164Number, @Nullable String countryIso) { 235 ContentValues contentValues = new ContentValues(); 236 contentValues.put(getOriginalNumberColumnName(), Preconditions.checkNotNull(number)); 237 if (!useNewFiltering()) { 238 if (e164Number == null) { 239 e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 240 } 241 contentValues.put(getE164NumberColumnName(), e164Number); 242 contentValues.put(getCountryIsoColumnName(), countryIso); 243 contentValues.put(getTypeColumnName(), FilteredNumberTypes.BLOCKED_NUMBER); 244 contentValues.put(getSourceColumnName(), FilteredNumberSources.USER); 245 } 246 return contentValues; 247 } 248 249 /** 250 * Shows the flow of {@link android.app.DialogFragment}s for blocking or unblocking numbers. 251 * 252 * @param blockId The id into the blocked numbers database. 253 * @param number The number to block or unblock. 254 * @param countryIso The countryIso used to format the given number. 255 * @param displayNumber The form of the number to block, suitable for displaying. 256 * @param parentViewId The id for the containing view of the Dialog. 257 * @param fragmentManager The {@link FragmentManager} used to show fragments. 258 * @param callback (optional) The {@link Callback} to call when the block or unblock operation 259 * is complete. 260 */ 261 public static void showBlockNumberDialogFlow(final ContentResolver contentResolver, 262 final Integer blockId, final String number, final String countryIso, 263 final String displayNumber, final Integer parentViewId, 264 final FragmentManager fragmentManager, @Nullable final Callback callback) { 265 Log.i(TAG, "showBlockNumberDialogFlow - start"); 266 // If the user is blocking a number and isn't using the framework solution when they 267 // should be, show the migration dialog 268 if (shouldShowMigrationDialog(blockId == null)) { 269 Log.i(TAG, "showBlockNumberDialogFlow - showing migration dialog"); 270 MigrateBlockedNumbersDialogFragment 271 .newInstance(new BlockedNumbersMigrator(contentResolver), newMigrationListener( 272 DialerApplication.getContext().getContentResolver(), number, countryIso, 273 displayNumber, parentViewId, fragmentManager, callback)) 274 .show(fragmentManager, "MigrateBlockedNumbers"); 275 return; 276 } 277 Log.i(TAG, "showBlockNumberDialogFlow - showing block number dialog"); 278 BlockNumberDialogFragment 279 .show(blockId, number, countryIso, displayNumber, parentViewId, fragmentManager, 280 callback); 281 } 282 283 private static boolean shouldShowMigrationDialog(boolean isBlocking) { 284 return isBlocking && canUseNewFiltering() && !hasMigratedToNewBlocking(); 285 } 286 287 private static BlockedNumbersMigrator.Listener newMigrationListener( 288 final ContentResolver contentResolver, final String number, final String countryIso, 289 final String displayNumber, final Integer parentViewId, 290 final FragmentManager fragmentManager, @Nullable final Callback callback) { 291 return new BlockedNumbersMigrator.Listener() { 292 @Override 293 public void onComplete() { 294 Log.i(TAG, "showBlockNumberDialogFlow - listener showing block number dialog"); 295 if (!hasMigratedToNewBlocking()) { 296 Log.i(TAG, "showBlockNumberDialogFlow - migration failed"); 297 return; 298 } 299 /* 300 * Edge case to cover here: if the user initiated the migration workflow with a 301 * number that's already blocked in the framework, don't show the block number 302 * dialog. Doing so would allow them to block the same number twice, causing a 303 * crash. 304 */ 305 new FilteredNumberAsyncQueryHandler(contentResolver).isBlockedNumber( 306 new OnCheckBlockedListener() { 307 @Override 308 public void onCheckComplete(Integer id) { 309 if (id != null) { 310 Log.i(TAG, 311 "showBlockNumberDialogFlow - number already blocked"); 312 return; 313 } 314 Log.i(TAG, "showBlockNumberDialogFlow - need to block number"); 315 BlockNumberDialogFragment 316 .show(null, number, countryIso, displayNumber, parentViewId, 317 fragmentManager, callback); 318 } 319 }, number, countryIso); 320 } 321 }; 322 } 323 324 /** 325 * Creates the {@link Intent} which opens the blocked numbers management interface. 326 * 327 * @param context The {@link Context}. 328 * @return The intent. 329 */ 330 public static Intent createManageBlockedNumbersIntent(Context context) { 331 if (canUseNewFiltering() && hasMigratedToNewBlocking()) { 332 return TelecomManagerUtil.createManageBlockedNumbersIntent( 333 (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE)); 334 } 335 return new Intent(context, BlockedNumbersSettingsActivity.class); 336 } 337 338 /** 339 * Method used to determine if block operations are possible. 340 * 341 * @param context The {@link Context}. 342 * @return {@code true} if the app and user can block numbers, {@code false} otherwise. 343 */ 344 public static boolean canAttemptBlockOperations(Context context) { 345 if (!CompatUtils.isNCompatible()) { 346 // Dialer blocking, must be primary user 347 return UserManagerCompat.isSystemUser( 348 (UserManager) context.getSystemService(Context.USER_SERVICE)); 349 } 350 351 // Great Wall blocking, must be primary user and the default or system dialer 352 // TODO(maxwelb): check that we're the default or system Dialer 353 return BlockedNumbersSdkCompat.canCurrentUserBlockNumbers(context); 354 } 355 356 /** 357 * Used to determine if the call blocking settings can be opened. 358 * 359 * @param context The {@link Context}. 360 * @return {@code true} if the current user can open the call blocking settings, {@code false} 361 * otherwise. 362 */ 363 public static boolean canCurrentUserOpenBlockSettings(Context context) { 364 if (!CompatUtils.isNCompatible()) { 365 // Dialer blocking, must be primary user 366 return UserManagerCompat.isSystemUser( 367 (UserManager) context.getSystemService(Context.USER_SERVICE)); 368 } 369 // BlockedNumberContract blocking, verify through Contract API 370 return BlockedNumbersSdkCompat.canCurrentUserBlockNumbers(context); 371 } 372 } 373