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