Home | History | Annotate | Download | only in blocking
      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. */
     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
     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
     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. */
     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
     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. */
    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    */
    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    */
    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    */
    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    */
    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    */
    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 
    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
    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    */
    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    */
    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 
    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    */
    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    */
    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)
    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    */
    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)
    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