/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.dialer.blocking; import android.annotation.TargetApi; import android.app.FragmentManager; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.UserManager; import android.preference.PreferenceManager; import android.provider.BlockedNumberContract; import android.provider.BlockedNumberContract.BlockedNumbers; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import com.android.dialer.common.LogUtil; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.database.FilteredNumberContract.FilteredNumber; import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources; import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes; import com.android.dialer.strictmode.StrictModeUtils; import com.android.dialer.telecom.TelecomUtil; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Compatibility class to encapsulate logic to switch between call blocking using {@link * com.android.dialer.database.FilteredNumberContract} and using {@link * android.provider.BlockedNumberContract}. This class should be used rather than explicitly * referencing columns from either contract class in situations where both blocking solutions may be * used. */ public class FilteredNumberCompat { private static Boolean canAttemptBlockOperationsForTest; @VisibleForTesting public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking"; /** @return The column name for ID in the filtered number database. */ public static String getIdColumnName(Context context) { return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID; } /** * @return The column name for type in the filtered number database. Will be {@code null} for the * framework blocking implementation. */ @Nullable public static String getTypeColumnName(Context context) { return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE; } /** * @return The column name for source in the filtered number database. Will be {@code null} for * the framework blocking implementation */ @Nullable public static String getSourceColumnName(Context context) { return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE; } /** @return The column name for the original number in the filtered number database. */ public static String getOriginalNumberColumnName(Context context) { return useNewFiltering(context) ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER : FilteredNumberColumns.NUMBER; } /** * @return The column name for country iso in the filtered number database. Will be {@code null} * the framework blocking implementation */ @Nullable public static String getCountryIsoColumnName(Context context) { return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO; } /** @return The column name for the e164 formatted number in the filtered number database. */ public static String getE164NumberColumnName(Context context) { return useNewFiltering(context) ? BlockedNumbers.COLUMN_E164_NUMBER : FilteredNumberColumns.NORMALIZED_NUMBER; } /** * @return {@code true} if the current SDK version supports using new filtering, {@code false} * otherwise. */ public static boolean canUseNewFiltering() { return VERSION.SDK_INT >= VERSION_CODES.N; } /** * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary * migration has been performed, {@code false} otherwise. */ public static boolean useNewFiltering(Context context) { return !ConfigProviderBindings.get(context).getBoolean("debug_force_dialer_filtering", false) && canUseNewFiltering() && hasMigratedToNewBlocking(context); } /** * @return {@code true} if the user has migrated to use {@link * android.provider.BlockedNumberContract} blocking, {@code false} otherwise. */ public static boolean hasMigratedToNewBlocking(Context context) { return StrictModeUtils.bypass( () -> PreferenceManager.getDefaultSharedPreferences(context) .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false)); } /** * Called to inform this class whether the user has fully migrated to use {@link * android.provider.BlockedNumberContract} blocking or not. * * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise. */ public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) { PreferenceManager.getDefaultSharedPreferences(context) .edit() .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated) .apply(); } /** * Gets the content {@link Uri} for number filtering. * * @param id The optional id to append with the base content uri. * @return The Uri for number filtering. */ public static Uri getContentUri(Context context, @Nullable Integer id) { if (id == null) { return getBaseUri(context); } return ContentUris.withAppendedId(getBaseUri(context), id); } private static Uri getBaseUri(Context context) { // Explicit version check to aid static analysis return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N ? BlockedNumbers.CONTENT_URI : FilteredNumber.CONTENT_URI; } /** * Removes any null column names from the given projection array. This method is intended to be * used to strip out any column names that aren't available in every version of number blocking. * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that * no non-existant columns are queried FilteredNumberCompat.filter(new String[] * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()}, * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); } * * @param projection The projection array. * @return The filtered projection array. */ @Nullable public static String[] filter(@Nullable String[] projection) { if (projection == null) { return null; } List filtered = new ArrayList<>(); for (String column : projection) { if (column != null) { filtered.add(column); } } return filtered.toArray(new String[filtered.size()]); } /** * Creates a new {@link ContentValues} suitable for inserting in the filtered number table. * * @param number The unformatted number to insert. * @param e164Number (optional) The number to insert formatted to E164 standard. * @param countryIso (optional) The country iso to use to format the number. * @return The ContentValues to insert. * @throws NullPointerException If number is null. */ public static ContentValues newBlockNumberContentValues( Context context, String number, @Nullable String e164Number, @Nullable String countryIso) { ContentValues contentValues = new ContentValues(); contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number)); if (!useNewFiltering(context)) { if (e164Number == null) { e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); } contentValues.put(getE164NumberColumnName(context), e164Number); contentValues.put(getCountryIsoColumnName(context), countryIso); contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER); contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER); } return contentValues; } /** * Shows block number migration dialog if necessary. * * @param fragmentManager The {@link FragmentManager} used to show fragments. * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete. * @return boolean True if migration dialog is shown. */ public static boolean maybeShowBlockNumberMigrationDialog( Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) { if (shouldShowMigrationDialog(context)) { LogUtil.i( "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog", "maybeShowBlockNumberMigrationDialog - showing migration dialog"); MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener) .show(fragmentManager, "MigrateBlockedNumbers"); return true; } return false; } private static boolean shouldShowMigrationDialog(Context context) { return canUseNewFiltering() && !hasMigratedToNewBlocking(context); } /** * Creates the {@link Intent} which opens the blocked numbers management interface. * * @param context The {@link Context}. * @return The intent. */ public static Intent createManageBlockedNumbersIntent(Context context) { // Explicit version check to aid static analysis if (canUseNewFiltering() && hasMigratedToNewBlocking(context) && VERSION.SDK_INT >= VERSION_CODES.N) { return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent(); } Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS"); intent.setPackage(context.getPackageName()); return intent; } /** * Method used to determine if block operations are possible. * * @param context The {@link Context}. * @return {@code true} if the app and user can block numbers, {@code false} otherwise. */ public static boolean canAttemptBlockOperations(Context context) { if (canAttemptBlockOperationsForTest != null) { return canAttemptBlockOperationsForTest; } if (VERSION.SDK_INT < VERSION_CODES.N) { // Dialer blocking, must be primary user return context.getSystemService(UserManager.class).isSystemUser(); } // Great Wall blocking, must be primary user and the default or system dialer // TODO(maxwelb): check that we're the system Dialer return TelecomUtil.isDefaultDialer(context) && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) public static void setCanAttemptBlockOperationsForTest(boolean canAttempt) { canAttemptBlockOperationsForTest = canAttempt; } /** * Used to determine if the call blocking settings can be opened. * * @param context The {@link Context}. * @return {@code true} if the current user can open the call blocking settings, {@code false} * otherwise. */ public static boolean canCurrentUserOpenBlockSettings(Context context) { if (VERSION.SDK_INT < VERSION_CODES.N) { // Dialer blocking, must be primary user return context.getSystemService(UserManager.class).isSystemUser(); } // BlockedNumberContract blocking, verify through Contract API return TelecomUtil.isDefaultDialer(context) && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context); } /** * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't * available, using this method ensures that the Dialer doesn't crash when on that screen. * * @param context The {@link Context}. * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an * exception was thrown. */ @TargetApi(VERSION_CODES.N) private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) { try { return BlockedNumberContract.canCurrentUserBlockNumbers(context); } catch (Exception e) { LogUtil.e( "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers", "Exception while querying BlockedNumberContract", e); return false; } } }