Home | History | Annotate | Download | only in compat
      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