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.app.FragmentManager;
20 import android.content.ContentUris;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.net.Uri;
25 import android.preference.PreferenceManager;
26 import android.provider.BlockedNumberContract;
27 import android.provider.BlockedNumberContract.BlockedNumbers;
28 import android.support.annotation.Nullable;
29 import android.support.annotation.VisibleForTesting;
30 import android.telecom.TelecomManager;
31 import android.telephony.PhoneNumberUtils;
32 import com.android.dialer.common.LogUtil;
33 import com.android.dialer.configprovider.ConfigProviderComponent;
34 import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
35 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
36 import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
37 import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
38 import com.android.dialer.strictmode.StrictModeUtils;
39 import com.android.dialer.telecom.TelecomUtil;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Objects;
43 
44 /**
45  * Compatibility class to encapsulate logic to switch between call blocking using {@link
46  * com.android.dialer.database.FilteredNumberContract} and using {@link
47  * android.provider.BlockedNumberContract}. This class should be used rather than explicitly
48  * referencing columns from either contract class in situations where both blocking solutions may be
49  * used.
50  */
51 @Deprecated
52 public class FilteredNumberCompat {
53 
54   private static Boolean canAttemptBlockOperationsForTest;
55 
56   @VisibleForTesting
57   public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking";
58 
59   /** @return The column name for ID in the filtered number database. */
getIdColumnName(Context context)60   public static String getIdColumnName(Context context) {
61     return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID;
62   }
63 
64   /**
65    * @return The column name for type in the filtered number database. Will be {@code null} for the
66    *     framework blocking implementation.
67    */
68   @Nullable
getTypeColumnName(Context context)69   public static String getTypeColumnName(Context context) {
70     return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE;
71   }
72 
73   /**
74    * @return The column name for source in the filtered number database. Will be {@code null} for
75    *     the framework blocking implementation
76    */
77   @Nullable
getSourceColumnName(Context context)78   public static String getSourceColumnName(Context context) {
79     return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE;
80   }
81 
82   /** @return The column name for the original number in the filtered number database. */
getOriginalNumberColumnName(Context context)83   public static String getOriginalNumberColumnName(Context context) {
84     return useNewFiltering(context)
85         ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER
86         : FilteredNumberColumns.NUMBER;
87   }
88 
89   /**
90    * @return The column name for country iso in the filtered number database. Will be {@code null}
91    *     the framework blocking implementation
92    */
93   @Nullable
getCountryIsoColumnName(Context context)94   public static String getCountryIsoColumnName(Context context) {
95     return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO;
96   }
97 
98   /** @return The column name for the e164 formatted number in the filtered number database. */
getE164NumberColumnName(Context context)99   public static String getE164NumberColumnName(Context context) {
100     return useNewFiltering(context)
101         ? BlockedNumbers.COLUMN_E164_NUMBER
102         : FilteredNumberColumns.NORMALIZED_NUMBER;
103   }
104 
105   /**
106    * @return {@code true} if the current SDK version supports using new filtering, {@code false}
107    *     otherwise.
108    */
canUseNewFiltering()109   public static boolean canUseNewFiltering() {
110     return true;
111   }
112 
113   /**
114    * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary
115    *     migration has been performed, {@code false} otherwise.
116    */
useNewFiltering(Context context)117   public static boolean useNewFiltering(Context context) {
118     return !ConfigProviderComponent.get(context)
119             .getConfigProvider()
120             .getBoolean("debug_force_dialer_filtering", false)
121         && canUseNewFiltering()
122         && hasMigratedToNewBlocking(context);
123   }
124 
125   /**
126    * @return {@code true} if the user has migrated to use {@link
127    *     android.provider.BlockedNumberContract} blocking, {@code false} otherwise.
128    */
hasMigratedToNewBlocking(Context context)129   public static boolean hasMigratedToNewBlocking(Context context) {
130     return StrictModeUtils.bypass(
131         () ->
132             PreferenceManager.getDefaultSharedPreferences(context)
133                 .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false));
134   }
135 
136   /**
137    * Called to inform this class whether the user has fully migrated to use {@link
138    * android.provider.BlockedNumberContract} blocking or not.
139    *
140    * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise.
141    */
setHasMigratedToNewBlocking(Context context, boolean hasMigrated)142   public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) {
143     PreferenceManager.getDefaultSharedPreferences(context)
144         .edit()
145         .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated)
146         .apply();
147   }
148 
149   /**
150    * Gets the content {@link Uri} for number filtering.
151    *
152    * @param id The optional id to append with the base content uri.
153    * @return The Uri for number filtering.
154    */
getContentUri(Context context, @Nullable Integer id)155   public static Uri getContentUri(Context context, @Nullable Integer id) {
156     if (id == null) {
157       return getBaseUri(context);
158     }
159     return ContentUris.withAppendedId(getBaseUri(context), id);
160   }
161 
getBaseUri(Context context)162   private static Uri getBaseUri(Context context) {
163     // Explicit version check to aid static analysis
164     return useNewFiltering(context) ? BlockedNumbers.CONTENT_URI : FilteredNumber.CONTENT_URI;
165   }
166 
167   /**
168    * Removes any null column names from the given projection array. This method is intended to be
169    * used to strip out any column names that aren't available in every version of number blocking.
170    * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that
171    * no non-existent columns are queried FilteredNumberCompat.filter(new String[]
172    * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()},
173    * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); }
174    *
175    * @param projection The projection array.
176    * @return The filtered projection array.
177    */
178   @Nullable
filter(@ullable String[] projection)179   public static String[] filter(@Nullable String[] projection) {
180     if (projection == null) {
181       return null;
182     }
183     List<String> filtered = new ArrayList<>();
184     for (String column : projection) {
185       if (column != null) {
186         filtered.add(column);
187       }
188     }
189     return filtered.toArray(new String[filtered.size()]);
190   }
191 
192   /**
193    * Creates a new {@link ContentValues} suitable for inserting in the filtered number table.
194    *
195    * @param number The unformatted number to insert.
196    * @param e164Number (optional) The number to insert formatted to E164 standard.
197    * @param countryIso (optional) The country iso to use to format the number.
198    * @return The ContentValues to insert.
199    * @throws NullPointerException If number is null.
200    */
newBlockNumberContentValues( Context context, String number, @Nullable String e164Number, @Nullable String countryIso)201   public static ContentValues newBlockNumberContentValues(
202       Context context, String number, @Nullable String e164Number, @Nullable String countryIso) {
203     ContentValues contentValues = new ContentValues();
204     contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number));
205     if (!useNewFiltering(context)) {
206       if (e164Number == null) {
207         e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
208       }
209       contentValues.put(getE164NumberColumnName(context), e164Number);
210       contentValues.put(getCountryIsoColumnName(context), countryIso);
211       contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER);
212       contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER);
213     }
214     return contentValues;
215   }
216 
217   /**
218    * Shows block number migration dialog if necessary.
219    *
220    * @param fragmentManager The {@link FragmentManager} used to show fragments.
221    * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete.
222    * @return boolean True if migration dialog is shown.
223    */
maybeShowBlockNumberMigrationDialog( Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener)224   public static boolean maybeShowBlockNumberMigrationDialog(
225       Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) {
226     if (shouldShowMigrationDialog(context)) {
227       LogUtil.i(
228           "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog",
229           "maybeShowBlockNumberMigrationDialog - showing migration dialog");
230       MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener)
231           .show(fragmentManager, "MigrateBlockedNumbers");
232       return true;
233     }
234     return false;
235   }
236 
shouldShowMigrationDialog(Context context)237   private static boolean shouldShowMigrationDialog(Context context) {
238     return canUseNewFiltering() && !hasMigratedToNewBlocking(context);
239   }
240 
241   /**
242    * Creates the {@link Intent} which opens the blocked numbers management interface.
243    *
244    * @param context The {@link Context}.
245    * @return The intent.
246    */
createManageBlockedNumbersIntent(Context context)247   public static Intent createManageBlockedNumbersIntent(Context context) {
248     // Explicit version check to aid static analysis
249     if (canUseNewFiltering() && hasMigratedToNewBlocking(context)) {
250       return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent();
251     }
252     Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS");
253     intent.setPackage(context.getPackageName());
254     return intent;
255   }
256 
257   /**
258    * Method used to determine if block operations are possible.
259    *
260    * @param context The {@link Context}.
261    * @return {@code true} if the app and user can block numbers, {@code false} otherwise.
262    */
canAttemptBlockOperations(Context context)263   public static boolean canAttemptBlockOperations(Context context) {
264     if (canAttemptBlockOperationsForTest != null) {
265       return canAttemptBlockOperationsForTest;
266     }
267 
268     // Great Wall blocking, must be primary user and the default or system dialer
269     // TODO(maxwelb): check that we're the system Dialer
270     return TelecomUtil.isDefaultDialer(context)
271         && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
272   }
273 
274   @VisibleForTesting(otherwise = VisibleForTesting.NONE)
setCanAttemptBlockOperationsForTest(boolean canAttempt)275   public static void setCanAttemptBlockOperationsForTest(boolean canAttempt) {
276     canAttemptBlockOperationsForTest = canAttempt;
277   }
278 
279   /**
280    * Used to determine if the call blocking settings can be opened.
281    *
282    * @param context The {@link Context}.
283    * @return {@code true} if the current user can open the call blocking settings, {@code false}
284    *     otherwise.
285    */
canCurrentUserOpenBlockSettings(Context context)286   public static boolean canCurrentUserOpenBlockSettings(Context context) {
287     // BlockedNumberContract blocking, verify through Contract API
288     return TelecomUtil.isDefaultDialer(context)
289         && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
290   }
291 
292   /**
293    * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it
294    * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't
295    * available, using this method ensures that the Dialer doesn't crash when on that screen.
296    *
297    * @param context The {@link Context}.
298    * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an
299    *     exception was thrown.
300    */
safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context)301   private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) {
302     try {
303       return BlockedNumberContract.canCurrentUserBlockNumbers(context);
304     } catch (Exception e) {
305       LogUtil.e(
306           "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers",
307           "Exception while querying BlockedNumberContract",
308           e);
309       return false;
310     }
311   }
312 }
313