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