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 */ getIdColumnName()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 getTypeColumnName()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 getSourceColumnName()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 */ getOriginalNumberColumnName()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 getCountryIsoColumnName()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 */ getE164NumberColumnName()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 */ canUseNewFiltering()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 */ useNewFiltering()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 */ hasMigratedToNewBlocking()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 setHasMigratedToNewBlocking(boolean hasMigrated)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 setIsEnabledForTest(Boolean isEnabled)166 public static void setIsEnabledForTest(Boolean isEnabled) { 167 isEnabledForTest = isEnabled; 168 } 169 170 @NeededForTesting setContextForTest(Context context)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 */ getContentUri(@ullable Integer id)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 getBaseUri()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 filter(@ullable String[] projection)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 */ newBlockNumberContentValues(String number, @Nullable String e164Number, @Nullable String countryIso)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 */ showBlockNumberDialogFlow(final ContentResolver contentResolver, final Integer blockId, final String number, final String countryIso, final String displayNumber, final Integer parentViewId, final FragmentManager fragmentManager, @Nullable final Callback callback)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 shouldShowMigrationDialog(boolean isBlocking)283 private static boolean shouldShowMigrationDialog(boolean isBlocking) { 284 return isBlocking && canUseNewFiltering() && !hasMigratedToNewBlocking(); 285 } 286 newMigrationListener( final ContentResolver contentResolver, final String number, final String countryIso, final String displayNumber, final Integer parentViewId, final FragmentManager fragmentManager, @Nullable final Callback callback)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