blob: 0ee85d8978b7e83d1b0995d559c28371603bcdf4 [file] [log] [blame]
/*
* 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.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.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 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 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<String> 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: check that we're the system Dialer
return TelecomUtil.isDefaultDialer(context)
&& safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
}
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;
}
}
}