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 */
16package com.android.providers.blockednumber;
17
18import android.Manifest;
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.app.AppOpsManager;
22import android.app.backup.BackupManager;
23import android.content.ContentProvider;
24import android.content.ContentUris;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.Intent;
28import android.content.SharedPreferences;
29import android.content.UriMatcher;
30import android.content.pm.PackageManager;
31import android.database.Cursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.database.sqlite.SQLiteQueryBuilder;
34import android.net.Uri;
35import android.os.Binder;
36import android.os.Bundle;
37import android.os.CancellationSignal;
38import android.os.Process;
39import android.os.UserManager;
40import android.provider.BlockedNumberContract;
41import android.provider.BlockedNumberContract.SystemContract;
42import android.telecom.TelecomManager;
43import android.telephony.CarrierConfigManager;
44import android.telephony.PhoneNumberUtils;
45import android.telephony.TelephonyManager;
46import android.text.TextUtils;
47import android.util.Log;
48
49import com.android.common.content.ProjectionMap;
50import com.android.internal.annotations.VisibleForTesting;
51import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables;
52
53import java.util.Arrays;
54
55/**
56 * Blocked phone number provider.
57 *
58 * <p>Note the provider allows emergency numbers.  The caller (telecom) should never call it with
59 * emergency numbers.
60 */
61public class BlockedNumberProvider extends ContentProvider {
62    static final String TAG = "BlockedNumbers";
63
64    private static final boolean DEBUG = false; // DO NOT SUBMIT WITH TRUE.
65
66    private static final int BLOCKED_LIST = 1000;
67    private static final int BLOCKED_ID = 1001;
68
69    private static final UriMatcher sUriMatcher;
70
71    private static final String PREF_FILE = "block_number_provider_prefs";
72    private static final String BLOCK_SUPPRESSION_EXPIRY_TIME_PREF =
73            "block_suppression_expiry_time_pref";
74    private static final int MAX_BLOCKING_DISABLED_DURATION_SECONDS = 7 * 24 * 3600; // 1 week
75    // Normally, we allow calls from self, *except* in unit tests, where we clear this flag
76    // to emulate calls from other apps.
77    @VisibleForTesting
78    static boolean ALLOW_SELF_CALL = true;
79
80    static {
81        sUriMatcher = new UriMatcher(0);
82        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST);
83        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID);
84    }
85
86    private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder()
87            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID)
88            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER)
89            .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)
90            .build();
91
92    private static final String ID_SELECTION =
93            BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?";
94
95    private static final String ORIGINAL_NUMBER_SELECTION =
96            BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?";
97
98    private static final String E164_NUMBER_SELECTION =
99            BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?";
100
101    @VisibleForTesting
102    protected BlockedNumberDatabaseHelper mDbHelper;
103    @VisibleForTesting
104    protected BackupManager mBackupManager;
105
106    @Override
107    public boolean onCreate() {
108        mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext());
109        mBackupManager = new BackupManager(getContext());
110        return true;
111    }
112
113    @Override
114    public String getType(@NonNull Uri uri) {
115        final int match = sUriMatcher.match(uri);
116        switch (match) {
117            case BLOCKED_LIST:
118                return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE;
119            case BLOCKED_ID:
120                return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE;
121            default:
122                throw new IllegalArgumentException("Unsupported URI: " + uri);
123        }
124    }
125
126    @Override
127    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
128        enforceWritePermissionAndPrimaryUser();
129
130        final int match = sUriMatcher.match(uri);
131        switch (match) {
132            case BLOCKED_LIST:
133                Uri blockedUri = insertBlockedNumber(values);
134                getContext().getContentResolver().notifyChange(blockedUri, null);
135                mBackupManager.dataChanged();
136                return blockedUri;
137            default:
138                throw new IllegalArgumentException("Unsupported URI: " + uri);
139        }
140    }
141
142    /**
143     * Implements the "blocked/" insert.
144     */
145    private Uri insertBlockedNumber(ContentValues cv) {
146        throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID);
147
148        final String phoneNumber = cv.getAsString(
149                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
150
151        if (TextUtils.isEmpty(phoneNumber)) {
152            throw new IllegalArgumentException("Missing a required column " +
153                    BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
154        }
155
156        // Fill in with autogenerated columns.
157        final String e164Number = Utils.getE164Number(getContext(), phoneNumber,
158                cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER));
159        cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number);
160
161        if (DEBUG) {
162            Log.d(TAG, String.format("inserted blocked number: %s", cv));
163        }
164
165        // Then insert.
166        final long id = mDbHelper.getWritableDatabase().insertWithOnConflict(
167                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv,
168                SQLiteDatabase.CONFLICT_REPLACE);
169
170        return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id);
171    }
172
173    private static void throwIfSpecified(ContentValues cv, String column) {
174        if (cv.containsKey(column)) {
175            throw new IllegalArgumentException("Column " + column + " must not be specified");
176        }
177    }
178
179    @Override
180    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
181            @Nullable String[] selectionArgs) {
182        enforceWritePermissionAndPrimaryUser();
183
184        throw new UnsupportedOperationException(
185                "Update is not supported.  Use delete + insert instead");
186    }
187
188    @Override
189    public int delete(@NonNull Uri uri, @Nullable String selection,
190            @Nullable String[] selectionArgs) {
191        enforceWritePermissionAndPrimaryUser();
192
193        final int match = sUriMatcher.match(uri);
194        int numRows;
195        switch (match) {
196            case BLOCKED_LIST:
197                numRows = deleteBlockedNumber(selection, selectionArgs);
198                break;
199            case BLOCKED_ID:
200                numRows = deleteBlockedNumberWithId(ContentUris.parseId(uri), selection);
201                break;
202            default:
203                throw new IllegalArgumentException("Unsupported URI: " + uri);
204        }
205        getContext().getContentResolver().notifyChange(uri, null);
206        mBackupManager.dataChanged();
207        return numRows;
208    }
209
210    /**
211     * Implements the "blocked/#" delete.
212     */
213    private int deleteBlockedNumberWithId(long id, String selection) {
214        throwForNonEmptySelection(selection);
215
216        return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)});
217    }
218
219    /**
220     * Implements the "blocked/" delete.
221     */
222    private int deleteBlockedNumber(String selection, String[] selectionArgs) {
223        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
224
225        // When selection is specified, compile it within (...) to detect SQL injection.
226        if (!TextUtils.isEmpty(selection)) {
227            db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " +
228                    Utils.wrapSelectionWithParens(selection),
229                    /* cancellationSignal =*/ null);
230        }
231
232        return db.delete(
233                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS,
234                selection, selectionArgs);
235    }
236
237    @Override
238    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
239            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
240        enforceReadPermissionAndPrimaryUser();
241
242        return query(uri, projection, selection, selectionArgs, sortOrder, null);
243    }
244
245    @Override
246    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
247            @Nullable String[] selectionArgs, @Nullable String sortOrder,
248            @Nullable CancellationSignal cancellationSignal) {
249        enforceReadPermissionAndPrimaryUser();
250
251        final int match = sUriMatcher.match(uri);
252        Cursor cursor;
253        switch (match) {
254            case BLOCKED_LIST:
255                cursor = queryBlockedList(projection, selection, selectionArgs, sortOrder,
256                        cancellationSignal);
257                break;
258            case BLOCKED_ID:
259                cursor = queryBlockedListWithId(ContentUris.parseId(uri), projection, selection,
260                        cancellationSignal);
261                break;
262            default:
263                throw new IllegalArgumentException("Unsupported URI: " + uri);
264        }
265        // Tell the cursor what uri to watch, so it knows when its source data changes
266        cursor.setNotificationUri(getContext().getContentResolver(), uri);
267        return cursor;
268    }
269
270    /**
271     * Implements the "blocked/#" query.
272     */
273    private Cursor queryBlockedListWithId(long id, String[] projection, String selection,
274            CancellationSignal cancellationSignal) {
275        throwForNonEmptySelection(selection);
276
277        return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)},
278                null, cancellationSignal);
279    }
280
281    /**
282     * Implements the "blocked/" query.
283     */
284    private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs,
285            String sortOrder, CancellationSignal cancellationSignal) {
286        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
287        qb.setStrict(true);
288        qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS);
289        qb.setProjectionMap(sBlockedNumberColumns);
290
291        return qb.query(mDbHelper.getReadableDatabase(), projection, selection, selectionArgs,
292                /* groupBy =*/ null, /* having =*/null, sortOrder,
293                /* limit =*/ null, cancellationSignal);
294    }
295
296    private void throwForNonEmptySelection(String selection) {
297        if (!TextUtils.isEmpty(selection)) {
298            throw new IllegalArgumentException(
299                    "When ID is specified in URI, selection must be null");
300        }
301    }
302
303    @Override
304    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
305        final Bundle res = new Bundle();
306        switch (method) {
307            case BlockedNumberContract.METHOD_IS_BLOCKED:
308                enforceReadPermissionAndPrimaryUser();
309
310                res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg));
311                break;
312            case BlockedNumberContract.METHOD_CAN_CURRENT_USER_BLOCK_NUMBERS:
313                // No permission checks: any app should be able to access this API.
314                res.putBoolean(
315                        BlockedNumberContract.RES_CAN_BLOCK_NUMBERS, canCurrentUserBlockUsers());
316                break;
317            case BlockedNumberContract.METHOD_UNBLOCK:
318                enforceWritePermissionAndPrimaryUser();
319
320                res.putInt(BlockedNumberContract.RES_NUM_ROWS_DELETED, unblock(arg));
321                break;
322            case SystemContract.METHOD_NOTIFY_EMERGENCY_CONTACT:
323                enforceSystemWritePermissionAndPrimaryUser();
324
325                notifyEmergencyContact();
326                break;
327            case SystemContract.METHOD_END_BLOCK_SUPPRESSION:
328                enforceSystemWritePermissionAndPrimaryUser();
329
330                endBlockSuppression();
331                break;
332            case SystemContract.METHOD_GET_BLOCK_SUPPRESSION_STATUS:
333                enforceSystemReadPermissionAndPrimaryUser();
334
335                SystemContract.BlockSuppressionStatus status = getBlockSuppressionStatus();
336                res.putBoolean(SystemContract.RES_IS_BLOCKING_SUPPRESSED, status.isSuppressed);
337                res.putLong(SystemContract.RES_BLOCKING_SUPPRESSED_UNTIL_TIMESTAMP,
338                        status.untilTimestampMillis);
339                break;
340            case SystemContract.METHOD_SHOULD_SYSTEM_BLOCK_NUMBER:
341                enforceSystemReadPermissionAndPrimaryUser();
342                res.putBoolean(
343                        BlockedNumberContract.RES_NUMBER_IS_BLOCKED, shouldSystemBlockNumber(arg));
344                break;
345            default:
346                enforceReadPermissionAndPrimaryUser();
347
348                throw new IllegalArgumentException("Unsupported method " + method);
349        }
350        return res;
351    }
352
353    private int unblock(String phoneNumber) {
354        if (TextUtils.isEmpty(phoneNumber)) {
355            return 0;
356        }
357
358        StringBuilder selectionBuilder = new StringBuilder(ORIGINAL_NUMBER_SELECTION);
359        String[] selectionArgs = new String[]{phoneNumber};
360        final String e164Number = Utils.getE164Number(getContext(), phoneNumber, null);
361        if (!TextUtils.isEmpty(e164Number)) {
362            selectionBuilder.append(" or " + E164_NUMBER_SELECTION);
363            selectionArgs = new String[]{phoneNumber, e164Number};
364        }
365        String selection = selectionBuilder.toString();
366        if (DEBUG) {
367            Log.d(TAG, String.format("Unblocking numbers using selection: %s, args: %s",
368                    selection, Arrays.toString(selectionArgs)));
369        }
370        return deleteBlockedNumber(selection, selectionArgs);
371    }
372
373    private boolean isEmergencyNumber(String phoneNumber) {
374        if (TextUtils.isEmpty(phoneNumber)) {
375            return false;
376        }
377
378        final String e164Number = Utils.getE164Number(getContext(), phoneNumber, null);
379        return PhoneNumberUtils.isEmergencyNumber(phoneNumber)
380                || PhoneNumberUtils.isEmergencyNumber(e164Number);
381    }
382
383    private boolean isBlocked(String phoneNumber) {
384        if (TextUtils.isEmpty(phoneNumber)) {
385            return false;
386        }
387
388        final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty.
389
390        if (DEBUG) {
391            Log.d(TAG, String.format("isBlocked: in=%s, e164=%s", phoneNumber, inE164));
392        }
393
394        final Cursor c = mDbHelper.getReadableDatabase().rawQuery(
395                "SELECT " +
396                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," +
397                BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER +
398                " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS +
399                " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" +
400                " OR (?2 != '' AND " +
401                        BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)",
402                new String[] {phoneNumber, inE164}
403                );
404        try {
405            while (c.moveToNext()) {
406                if (DEBUG) {
407                    final String original = c.getString(0);
408                    final String e164 = c.getString(1);
409
410                    Log.d(TAG, String.format("match found: original=%s, e164=%s", original, e164));
411                }
412                return true;
413            }
414        } finally {
415            c.close();
416        }
417        // No match found.
418        return false;
419    }
420
421    private boolean canCurrentUserBlockUsers() {
422        UserManager userManager = getContext().getSystemService(UserManager.class);
423        return userManager.isPrimaryUser();
424    }
425
426    private void notifyEmergencyContact() {
427        writeBlockSuppressionExpiryTimePref(System.currentTimeMillis() +
428                getBlockSuppressSecondsFromCarrierConfig() * 1000);
429        notifyBlockSuppressionStateChange();
430    }
431
432    private void endBlockSuppression() {
433        // Nothing to do if blocks are not being suppressed.
434        if (getBlockSuppressionStatus().isSuppressed) {
435            writeBlockSuppressionExpiryTimePref(0);
436            notifyBlockSuppressionStateChange();
437        }
438    }
439
440    private SystemContract.BlockSuppressionStatus getBlockSuppressionStatus() {
441        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
442        long blockSuppressionExpiryTimeMillis = pref.getLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, 0);
443        return new SystemContract.BlockSuppressionStatus(System.currentTimeMillis() <
444                blockSuppressionExpiryTimeMillis, blockSuppressionExpiryTimeMillis);
445    }
446
447    private boolean shouldSystemBlockNumber(String phoneNumber) {
448        if (getBlockSuppressionStatus().isSuppressed) {
449            return false;
450        }
451        if (isEmergencyNumber(phoneNumber)) {
452            return false;
453        }
454        return isBlocked(phoneNumber);
455    }
456
457    private void writeBlockSuppressionExpiryTimePref(long expiryTimeMillis) {
458        SharedPreferences pref = getContext().getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE);
459        SharedPreferences.Editor editor = pref.edit();
460        editor.putLong(BLOCK_SUPPRESSION_EXPIRY_TIME_PREF, expiryTimeMillis);
461        editor.apply();
462    }
463
464    private long getBlockSuppressSecondsFromCarrierConfig() {
465        CarrierConfigManager carrierConfigManager =
466                getContext().getSystemService(CarrierConfigManager.class);
467        int carrierConfigValue = carrierConfigManager.getConfig().getInt
468                (CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT);
469        boolean isValidValue = carrierConfigValue >=0 && carrierConfigValue <=
470                MAX_BLOCKING_DISABLED_DURATION_SECONDS;
471        return isValidValue ? carrierConfigValue : CarrierConfigManager.getDefaultConfig().getInt(
472                CarrierConfigManager.KEY_DURATION_BLOCKING_DISABLED_AFTER_EMERGENCY_INT);
473    }
474
475    /**
476     * Returns {@code false} when the caller is not root, the user selected dialer, the
477     * default SMS app or a carrier app.
478     */
479    private boolean checkForPrivilegedApplications() {
480        if (Binder.getCallingUid() == Process.ROOT_UID) {
481            return true;
482        }
483
484        final String callingPackage = getCallingPackage();
485        if (TextUtils.isEmpty(callingPackage)) {
486            Log.w(TAG, "callingPackage not accessible");
487        } else {
488            final TelecomManager telecom = getContext().getSystemService(TelecomManager.class);
489
490            if (callingPackage.equals(telecom.getDefaultDialerPackage())
491                    || callingPackage.equals(telecom.getSystemDialerPackage())) {
492                return true;
493            }
494            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
495            if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS,
496                    Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) {
497                return true;
498            }
499
500            final TelephonyManager telephonyManager =
501                    getContext().getSystemService(TelephonyManager.class);
502            return telephonyManager.checkCarrierPrivilegesForPackage(callingPackage) ==
503                    TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS;
504        }
505        return false;
506    }
507
508    private void notifyBlockSuppressionStateChange() {
509        Intent intent = new Intent(SystemContract.ACTION_BLOCK_SUPPRESSION_STATE_CHANGED);
510        getContext().sendBroadcast(intent, Manifest.permission.READ_BLOCKED_NUMBERS);
511    }
512
513    private void enforceReadPermission() {
514        checkForPermission(android.Manifest.permission.READ_BLOCKED_NUMBERS);
515    }
516
517    private void enforceReadPermissionAndPrimaryUser() {
518        checkForPermissionAndPrimaryUser(android.Manifest.permission.READ_BLOCKED_NUMBERS);
519    }
520
521    private void enforceWritePermissionAndPrimaryUser() {
522        checkForPermissionAndPrimaryUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS);
523    }
524
525    private void checkForPermissionAndPrimaryUser(String permission) {
526        checkForPermission(permission);
527        if (!canCurrentUserBlockUsers()) {
528            throwCurrentUserNotPermittedSecurityException();
529        }
530    }
531
532    private void checkForPermission(String permission) {
533        boolean permitted = passesSystemPermissionCheck(permission)
534                || checkForPrivilegedApplications() || isSelf();
535        if (!permitted) {
536            throwSecurityException();
537        }
538    }
539
540    private void enforceSystemReadPermissionAndPrimaryUser() {
541        enforceSystemPermissionAndUser(android.Manifest.permission.READ_BLOCKED_NUMBERS);
542    }
543
544    private void enforceSystemWritePermissionAndPrimaryUser() {
545        enforceSystemPermissionAndUser(android.Manifest.permission.WRITE_BLOCKED_NUMBERS);
546    }
547
548    private void enforceSystemPermissionAndUser(String permission) {
549        if (!canCurrentUserBlockUsers()) {
550            throwCurrentUserNotPermittedSecurityException();
551        }
552
553        if (!passesSystemPermissionCheck(permission)) {
554            throwSecurityException();
555        }
556    }
557
558    private boolean passesSystemPermissionCheck(String permission) {
559        return getContext().checkCallingPermission(permission)
560                == PackageManager.PERMISSION_GRANTED;
561    }
562
563    private boolean isSelf() {
564        return ALLOW_SELF_CALL && Binder.getCallingPid() == Process.myPid();
565    }
566
567    private void throwSecurityException() {
568        throw new SecurityException("Caller must be system, default dialer or default SMS app");
569    }
570
571    private void throwCurrentUserNotPermittedSecurityException() {
572        throw new SecurityException("The current user cannot perform this operation");
573    }
574}
575