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. */
getIdColumnName(Context context)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
getTypeColumnName(Context context)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
getSourceColumnName(Context context)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. */
getOriginalNumberColumnName(Context context)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
getCountryIsoColumnName(Context context)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. */
getE164NumberColumnName(Context context)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    */
canUseNewFiltering()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    */
useNewFiltering(Context context)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    */
hasMigratedToNewBlocking(Context context)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    */
setHasMigratedToNewBlocking(Context context, boolean hasMigrated)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    */
getContentUri(Context context, @Nullable Integer id)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 
getBaseUri(Context context)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
filter(@ullable String[] projection)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    */
newBlockNumberContentValues( Context context, String number, @Nullable String e164Number, @Nullable String countryIso)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    */
maybeShowBlockNumberMigrationDialog( Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener)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 
shouldShowMigrationDialog(Context context)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    */
createManageBlockedNumbersIntent(Context context)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    */
canAttemptBlockOperations(Context context)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)
setCanAttemptBlockOperationsForTest(boolean canAttempt)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    */
canCurrentUserOpenBlockSettings(Context context)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)
safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context)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