summaryrefslogtreecommitdiffstats
path: root/java/com/android/dialer/blocking/FilteredNumberCompat.java
blob: e104c4f94d92fe9bc6e38885409ba31c3b9efd04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.dialer.blocking;

import android.annotation.TargetApi;
import android.app.FragmentManager;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.UserManager;
import android.preference.PreferenceManager;
import android.provider.BlockedNumberContract;
import android.provider.BlockedNumberContract.BlockedNumbers;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import com.android.dialer.common.LogUtil;
import com.android.dialer.database.FilteredNumberContract.FilteredNumber;
import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
import com.android.dialer.database.FilteredNumberContract.FilteredNumberSources;
import com.android.dialer.database.FilteredNumberContract.FilteredNumberTypes;
import com.android.dialer.telecom.TelecomUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * Compatibility class to encapsulate logic to switch between call blocking using {@link
 * com.android.dialer.database.FilteredNumberContract} and using {@link
 * android.provider.BlockedNumberContract}. This class should be used rather than explicitly
 * referencing columns from either contract class in situations where both blocking solutions may be
 * used.
 */
public class FilteredNumberCompat {

  private static Boolean canAttemptBlockOperationsForTest;

  @VisibleForTesting
  public static final String HAS_MIGRATED_TO_NEW_BLOCKING_KEY = "migratedToNewBlocking";

  /** @return The column name for ID in the filtered number database. */
  public static String getIdColumnName(Context context) {
    return useNewFiltering(context) ? BlockedNumbers.COLUMN_ID : FilteredNumberColumns._ID;
  }

  /**
   * @return The column name for type in the filtered number database. Will be {@code null} for the
   *     framework blocking implementation.
   */
  @Nullable
  public static String getTypeColumnName(Context context) {
    return useNewFiltering(context) ? null : FilteredNumberColumns.TYPE;
  }

  /**
   * @return The column name for source in the filtered number database. Will be {@code null} for
   *     the framework blocking implementation
   */
  @Nullable
  public static String getSourceColumnName(Context context) {
    return useNewFiltering(context) ? null : FilteredNumberColumns.SOURCE;
  }

  /** @return The column name for the original number in the filtered number database. */
  public static String getOriginalNumberColumnName(Context context) {
    return useNewFiltering(context)
        ? BlockedNumbers.COLUMN_ORIGINAL_NUMBER
        : FilteredNumberColumns.NUMBER;
  }

  /**
   * @return The column name for country iso in the filtered number database. Will be {@code null}
   *     the framework blocking implementation
   */
  @Nullable
  public static String getCountryIsoColumnName(Context context) {
    return useNewFiltering(context) ? null : FilteredNumberColumns.COUNTRY_ISO;
  }

  /** @return The column name for the e164 formatted number in the filtered number database. */
  public static String getE164NumberColumnName(Context context) {
    return useNewFiltering(context)
        ? BlockedNumbers.COLUMN_E164_NUMBER
        : FilteredNumberColumns.NORMALIZED_NUMBER;
  }

  /**
   * @return {@code true} if the current SDK version supports using new filtering, {@code false}
   *     otherwise.
   */
  public static boolean canUseNewFiltering() {
    return VERSION.SDK_INT >= VERSION_CODES.N;
  }

  /**
   * @return {@code true} if the new filtering should be used, i.e. it's enabled and any necessary
   *     migration has been performed, {@code false} otherwise.
   */
  public static boolean useNewFiltering(Context context) {
    return canUseNewFiltering() && hasMigratedToNewBlocking(context);
  }

  /**
   * @return {@code true} if the user has migrated to use {@link
   *     android.provider.BlockedNumberContract} blocking, {@code false} otherwise.
   */
  public static boolean hasMigratedToNewBlocking(Context context) {
    return PreferenceManager.getDefaultSharedPreferences(context)
        .getBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, false);
  }

  /**
   * Called to inform this class whether the user has fully migrated to use {@link
   * android.provider.BlockedNumberContract} blocking or not.
   *
   * @param hasMigrated {@code true} if the user has migrated, {@code false} otherwise.
   */
  public static void setHasMigratedToNewBlocking(Context context, boolean hasMigrated) {
    PreferenceManager.getDefaultSharedPreferences(context)
        .edit()
        .putBoolean(HAS_MIGRATED_TO_NEW_BLOCKING_KEY, hasMigrated)
        .apply();
  }

  /**
   * Gets the content {@link Uri} for number filtering.
   *
   * @param id The optional id to append with the base content uri.
   * @return The Uri for number filtering.
   */
  public static Uri getContentUri(Context context, @Nullable Integer id) {
    if (id == null) {
      return getBaseUri(context);
    }
    return ContentUris.withAppendedId(getBaseUri(context), id);
  }

  private static Uri getBaseUri(Context context) {
    // Explicit version check to aid static analysis
    return useNewFiltering(context) && VERSION.SDK_INT >= VERSION_CODES.N
        ? BlockedNumbers.CONTENT_URI
        : FilteredNumber.CONTENT_URI;
  }

  /**
   * Removes any null column names from the given projection array. This method is intended to be
   * used to strip out any column names that aren't available in every version of number blocking.
   * Example: {@literal getContext().getContentResolver().query( someUri, // Filtering ensures that
   * no non-existant columns are queried FilteredNumberCompat.filter(new String[]
   * {FilteredNumberCompat.getIdColumnName(), FilteredNumberCompat.getTypeColumnName()},
   * FilteredNumberCompat.getE164NumberColumnName() + " = ?", new String[] {e164Number}); }
   *
   * @param projection The projection array.
   * @return The filtered projection array.
   */
  @Nullable
  public static String[] filter(@Nullable String[] projection) {
    if (projection == null) {
      return null;
    }
    List<String> filtered = new ArrayList<>();
    for (String column : projection) {
      if (column != null) {
        filtered.add(column);
      }
    }
    return filtered.toArray(new String[filtered.size()]);
  }

  /**
   * Creates a new {@link ContentValues} suitable for inserting in the filtered number table.
   *
   * @param number The unformatted number to insert.
   * @param e164Number (optional) The number to insert formatted to E164 standard.
   * @param countryIso (optional) The country iso to use to format the number.
   * @return The ContentValues to insert.
   * @throws NullPointerException If number is null.
   */
  public static ContentValues newBlockNumberContentValues(
      Context context, String number, @Nullable String e164Number, @Nullable String countryIso) {
    ContentValues contentValues = new ContentValues();
    contentValues.put(getOriginalNumberColumnName(context), Objects.requireNonNull(number));
    if (!useNewFiltering(context)) {
      if (e164Number == null) {
        e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
      }
      contentValues.put(getE164NumberColumnName(context), e164Number);
      contentValues.put(getCountryIsoColumnName(context), countryIso);
      contentValues.put(getTypeColumnName(context), FilteredNumberTypes.BLOCKED_NUMBER);
      contentValues.put(getSourceColumnName(context), FilteredNumberSources.USER);
    }
    return contentValues;
  }

  /**
   * Shows block number migration dialog if necessary.
   *
   * @param fragmentManager The {@link FragmentManager} used to show fragments.
   * @param listener The {@link BlockedNumbersMigrator.Listener} to call when migration is complete.
   * @return boolean True if migration dialog is shown.
   */
  public static boolean maybeShowBlockNumberMigrationDialog(
      Context context, FragmentManager fragmentManager, BlockedNumbersMigrator.Listener listener) {
    if (shouldShowMigrationDialog(context)) {
      LogUtil.i(
          "FilteredNumberCompat.maybeShowBlockNumberMigrationDialog",
          "maybeShowBlockNumberMigrationDialog - showing migration dialog");
      MigrateBlockedNumbersDialogFragment.newInstance(new BlockedNumbersMigrator(context), listener)
          .show(fragmentManager, "MigrateBlockedNumbers");
      return true;
    }
    return false;
  }

  private static boolean shouldShowMigrationDialog(Context context) {
    return canUseNewFiltering() && !hasMigratedToNewBlocking(context);
  }

  /**
   * Creates the {@link Intent} which opens the blocked numbers management interface.
   *
   * @param context The {@link Context}.
   * @return The intent.
   */
  public static Intent createManageBlockedNumbersIntent(Context context) {
    // Explicit version check to aid static analysis
    if (canUseNewFiltering()
        && hasMigratedToNewBlocking(context)
        && VERSION.SDK_INT >= VERSION_CODES.N) {
      return context.getSystemService(TelecomManager.class).createManageBlockedNumbersIntent();
    }
    Intent intent = new Intent("com.android.dialer.action.BLOCKED_NUMBERS_SETTINGS");
    intent.setPackage(context.getPackageName());
    return intent;
  }

  /**
   * Method used to determine if block operations are possible.
   *
   * @param context The {@link Context}.
   * @return {@code true} if the app and user can block numbers, {@code false} otherwise.
   */
  public static boolean canAttemptBlockOperations(Context context) {
    if (canAttemptBlockOperationsForTest != null) {
      return canAttemptBlockOperationsForTest;
    }

    if (VERSION.SDK_INT < VERSION_CODES.N) {
      // Dialer blocking, must be primary user
      return context.getSystemService(UserManager.class).isSystemUser();
    }

    // Great Wall blocking, must be primary user and the default or system dialer
    // TODO(maxwelb): check that we're the system Dialer
    return TelecomUtil.isDefaultDialer(context)
        && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
  }

  @VisibleForTesting(otherwise = VisibleForTesting.NONE)
  public static void setCanAttemptBlockOperationsForTest(boolean canAttempt) {
    canAttemptBlockOperationsForTest = canAttempt;
  }

  /**
   * Used to determine if the call blocking settings can be opened.
   *
   * @param context The {@link Context}.
   * @return {@code true} if the current user can open the call blocking settings, {@code false}
   *     otherwise.
   */
  public static boolean canCurrentUserOpenBlockSettings(Context context) {
    if (VERSION.SDK_INT < VERSION_CODES.N) {
      // Dialer blocking, must be primary user
      return context.getSystemService(UserManager.class).isSystemUser();
    }
    // BlockedNumberContract blocking, verify through Contract API
    return TelecomUtil.isDefaultDialer(context)
        && safeBlockedNumbersContractCanCurrentUserBlockNumbers(context);
  }

  /**
   * Calls {@link BlockedNumberContract#canCurrentUserBlockNumbers(Context)} in such a way that it
   * never throws an exception. While on the CryptKeeper screen, the BlockedNumberContract isn't
   * available, using this method ensures that the Dialer doesn't crash when on that screen.
   *
   * @param context The {@link Context}.
   * @return the result of BlockedNumberContract#canCurrentUserBlockNumbers, or {@code false} if an
   *     exception was thrown.
   */
  @TargetApi(VERSION_CODES.N)
  private static boolean safeBlockedNumbersContractCanCurrentUserBlockNumbers(Context context) {
    try {
      return BlockedNumberContract.canCurrentUserBlockNumbers(context);
    } catch (Exception e) {
      LogUtil.e(
          "FilteredNumberCompat.safeBlockedNumbersContractCanCurrentUserBlockNumbers",
          "Exception while querying BlockedNumberContract",
          e);
      return false;
    }
  }
}