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
17package com.android.dialer.blocking;
18
19import android.annotation.TargetApi;
20import android.app.FragmentManager;
21import android.content.ContentUris;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.Intent;
25import android.net.Uri;
26import android.os.Build.VERSION;
27import android.os.Build.VERSION_CODES;
28import android.os.UserManager;
29import android.preference.PreferenceManager;
30import android.provider.BlockedNumberContract;
31import android.provider.BlockedNumberContract.BlockedNumbers;
32import android.support.annotation.Nullable;
33import android.support.annotation.VisibleForTesting;
34import android.telecom.TelecomManager;
35import android.telephony.PhoneNumberUtils;
36import com.android.dialer.common.LogUtil;
37import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
38import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
39import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
40import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
41import com.android.dialer.telecom.TelecomUtil;
42import java.util.ArrayList;
43import java.util.List;
44import 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 */
53public 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