BlockedNumberProvider.java revision 35676f9095853d4a5ac15875a9ddb1aea6b0b809
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.annotation.NonNull;
19import android.annotation.Nullable;
20import android.app.AppOpsManager;
21import android.content.*;
22import android.content.pm.ApplicationInfo;
23import android.content.pm.PackageManager;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteQueryBuilder;
27import android.net.Uri;
28import android.os.*;
29import android.os.Process;
30import android.provider.BlockedNumberContract;
31import android.telecom.TelecomManager;
32import android.text.TextUtils;
33import android.util.Log;
34import com.android.common.content.ProjectionMap;
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables;
37
38/**
39 * Blocked phone number provider.
40 *
41 * <p>Note the provider allows emergency numbers.  The caller (telecom) should never call it with
42 * emergency numbers.
43 */
44public class BlockedNumberProvider extends ContentProvider {
45    static final String TAG = "BlockedNumbers";
46
47    private static final boolean DEBUG = true; // DO NOT SUBMIT WITH TRUE.
48
49    private static final int BLOCKED_LIST = 1000;
50    private static final int BLOCKED_ID = 1001;
51
52    private static final UriMatcher sUriMatcher;
53
54    static {
55        sUriMatcher = new UriMatcher(0);
56        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST);
57        sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID);
58    }
59
60    private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder()
61            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID)
62            .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER)
63            .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)
64            .build();
65
66    private static final String ID_SELECTION =
67            BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?";
68
69    @VisibleForTesting
70    protected BlockedNumberDatabaseHelper mDbHelper;
71
72    @Override
73    public boolean onCreate() {
74        mDbHelper = BlockedNumberDatabaseHelper.getInstance(getContext());
75        return true;
76    }
77
78    /**
79     * TODO CTS:
80     * - BLOCKED_LIST
81     * - BLOCKED_ID
82     * - Other random URLs should fail
83     */
84    @Override
85    public String getType(@NonNull Uri uri) {
86        final int match = sUriMatcher.match(uri);
87        switch (match) {
88            case BLOCKED_LIST:
89                return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE;
90            case BLOCKED_ID:
91                return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE;
92            default:
93                throw new IllegalArgumentException("Unsupported URI: " + uri);
94        }
95    }
96
97    /**
98     * TODO CTS:
99     * - BLOCKED_LIST
100     *   With no columns should fail
101     *   With COLUMN_INDEX_ORIGINAL only
102     *   With COLUMN_INDEX_E164 only should fail
103     *   With COLUMN_INDEX_ORIGINAL + COLUMN_INDEX_E164
104     *   With with throwIfSpecified columns, should fail.
105     *
106     * - BLOCKED_ID should fail
107     * - Other random URLs should fail
108     */
109    @Override
110    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
111        enforceWritePermission();
112
113        final int match = sUriMatcher.match(uri);
114        switch (match) {
115            case BLOCKED_LIST:
116                Uri blockedUri = insertBlockedNumber(values);
117                getContext().getContentResolver().notifyChange(blockedUri, null);
118                return blockedUri;
119            default:
120                throw new IllegalArgumentException("Unsupported URI: " + uri);
121        }
122    }
123
124    /**
125     * Implements the "blocked/" insert.
126     */
127    private Uri insertBlockedNumber(ContentValues cv) {
128        throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID);
129
130        final String phoneNumber = cv.getAsString(
131                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
132
133        if (TextUtils.isEmpty(phoneNumber)) {
134            throw new IllegalArgumentException("Missing a required column " +
135                    BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
136        }
137
138        // Fill in with autogenerated columns.
139        final String e164Number = Utils.getE164Number(getContext(), phoneNumber,
140                cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER));
141        cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number);
142
143        if (DEBUG) {
144            Log.d(TAG, String.format("inserted blocked number: %s", cv));
145        }
146
147        // Then insert.
148        final long id = mDbHelper.getWritableDatabase().insertOrThrow(
149                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv);
150
151        return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id);
152    }
153
154    private static void throwIfSpecified(ContentValues cv, String column) {
155        if (cv.containsKey(column)) {
156            throw new IllegalArgumentException("Column " + column + " must not be specified");
157        }
158    }
159
160    /**
161     * TODO CTS:
162     * - Any call should fail
163     */
164    @Override
165    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
166            @Nullable String[] selectionArgs) {
167        throw new UnsupportedOperationException(
168                "Update is not supported.  Use delete + insert instead");
169    }
170
171    /**
172     * TODO CTS:
173     * - BLOCKED_LIST, with selection and without.
174     * - BLOCKED_ID , with selection and without.  With should fail.
175     */
176    @Override
177    public int delete(@NonNull Uri uri, @Nullable String selection,
178            @Nullable String[] selectionArgs) {
179        enforceWritePermission();
180
181        final int match = sUriMatcher.match(uri);
182        int numRows;
183        switch (match) {
184            case BLOCKED_LIST:
185                numRows = deleteBlockedNumber(selection, selectionArgs);
186                break;
187            case BLOCKED_ID:
188                numRows = deleteBlockedNumberWithId(ContentUris.parseId(uri), selection);
189                break;
190            default:
191                throw new IllegalArgumentException("Unsupported URI: " + uri);
192        }
193        getContext().getContentResolver().notifyChange(uri, null);
194        return numRows;
195    }
196
197    /**
198     * Implements the "blocked/#" delete.
199     */
200    private int deleteBlockedNumberWithId(long id, String selection) {
201        throwForNonEmptySelection(selection);
202
203        return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)});
204    }
205
206    /**
207     * Implements the "blocked/" delete.
208     */
209    private int deleteBlockedNumber(String selection, String[] selectionArgs) {
210        final SQLiteDatabase db = mDbHelper.getWritableDatabase();
211
212        // When selection is specified, compile it within (...) to detect SQL injection.
213        if (!TextUtils.isEmpty(selection)) {
214            db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " +
215                    Utils.wrapSelectionWithParens(selection),
216                    /* cancellationSignal =*/ null);
217        }
218
219        return db.delete(
220                BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS,
221                selection, selectionArgs);
222    }
223
224    @Override
225    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
226            @Nullable String[] selectionArgs, @Nullable String sortOrder) {
227        enforceReadPermission();
228
229        return query(uri, projection, selection, selectionArgs, sortOrder, null);
230    }
231
232    @Override
233    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
234            @Nullable String[] selectionArgs, @Nullable String sortOrder,
235            @Nullable CancellationSignal cancellationSignal) {
236        enforceReadPermission();
237
238        final int match = sUriMatcher.match(uri);
239        Cursor cursor;
240        switch (match) {
241            case BLOCKED_LIST:
242                cursor = queryBlockedList(projection, selection, selectionArgs, sortOrder,
243                        cancellationSignal);
244                break;
245            case BLOCKED_ID:
246                cursor = queryBlockedListWithId(ContentUris.parseId(uri), projection, selection,
247                        cancellationSignal);
248                break;
249            default:
250                throw new IllegalArgumentException("Unsupported URI: " + uri);
251        }
252        // Tell the cursor what uri to watch, so it knows when its source data changes
253        cursor.setNotificationUri(getContext().getContentResolver(), uri);
254        return cursor;
255    }
256
257    /**
258     * Implements the "blocked/#" query.
259     */
260    private Cursor queryBlockedListWithId(long id, String[] projection, String selection,
261            CancellationSignal cancellationSignal) {
262        throwForNonEmptySelection(selection);
263
264        return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)},
265                null, cancellationSignal);
266    }
267
268    /**
269     * Implements the "blocked/" query.
270     */
271    private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs,
272            String sortOrder, CancellationSignal cancellationSignal) {
273        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
274        qb.setStrict(true);
275        qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS);
276        qb.setProjectionMap(sBlockedNumberColumns);
277
278        return qb.query(mDbHelper.getReadableDatabase(), projection, selection, selectionArgs,
279                /* groupBy =*/ null, /* having =*/null, sortOrder,
280                /* limit =*/ null, cancellationSignal);
281    }
282
283    private void throwForNonEmptySelection(String selection) {
284        if (!TextUtils.isEmpty(selection)) {
285            throw new IllegalArgumentException(
286                    "When ID is specified in URI, selection must be null");
287        }
288    }
289
290    /**
291     * TODO CTS:
292     * - METHOD_IS_BLOCKED with various matching / non-matching arguments.
293     *
294     * - other random methods should fail
295     */
296    @Override
297    public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
298        enforceReadPermission();
299
300        final Bundle res = new Bundle();
301        switch (method) {
302            case BlockedNumberContract.METHOD_IS_BLOCKED:
303                res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg));
304                break;
305            default:
306                throw new IllegalArgumentException("Unsupported method " + method);
307        }
308        return res;
309    }
310
311    private boolean isBlocked(String phoneNumber) {
312        if (TextUtils.isEmpty(phoneNumber)) {
313            return false;
314        }
315
316        final String inE164 = Utils.getE164Number(getContext(), phoneNumber, null); // may be empty.
317
318        if (DEBUG) {
319            Log.d(TAG, String.format("isBlocked: in=%s, e164=%s", phoneNumber, inE164));
320        }
321
322        final Cursor c = mDbHelper.getReadableDatabase().rawQuery(
323                "SELECT " +
324                BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," +
325                BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER +
326                " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS +
327                " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "=?1" +
328                " OR (?2 != '' AND " +
329                        BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)",
330                new String[] {phoneNumber, inE164}
331                );
332        try {
333            while (c.moveToNext()) {
334                if (DEBUG) {
335                    final String original = c.getString(0);
336                    final String e164 = c.getString(1);
337
338                    Log.d(TAG, String.format("match found: original=%s, e164=%s", original, e164));
339                }
340                return true;
341            }
342        } finally {
343            c.close();
344        }
345        // No match found.
346        return false;
347    }
348
349    /**
350     * Throws {@link SecurityException} when the caller is not root, system, the system dialer,
351     * the user selected dialer, or the default SMS app.
352     *
353     * NOT TESTED YET
354     *
355     * TODO CTS:
356     * - Call should fail for random 3p apps.
357     *
358     * TODO Add a permission to allow the contacts app to access?
359     * TODO Add a permission to allow carrier apps?
360     */
361    public void enforceReadPermission() {
362        final int callingUid = Binder.getCallingUid();
363
364        // System and root can always call it. (and myself)
365        if (UserHandle.isSameApp(callingUid, android.os.Process.SYSTEM_UID)
366                || (callingUid == Process.ROOT_UID)
367                || (callingUid == Process.myUid())) {
368            return;
369        }
370
371        final String callingPackage = getCallingPackage();
372        if (TextUtils.isEmpty(callingPackage)) {
373            Log.w(TAG, "callingPackage not accessible");
374        } else {
375
376            final TelecomManager telecom = getContext().getSystemService(TelecomManager.class);
377
378            if (callingPackage.equals(telecom.getDefaultDialerPackage())
379                || callingPackage.equals(telecom.getSystemDialerPackage())) {
380                return;
381            }
382
383            // Allow the default SMS app and the dialer app to access it.
384            final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
385
386            if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS,
387                    Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) {
388                return;
389            }
390
391            // TODO: Add an explicit permission instead.
392            try {
393                ApplicationInfo applicationInfo = getContext().
394                        getPackageManager().getPackageInfo(callingPackage, 0).applicationInfo;
395                if (applicationInfo.isPrivilegedApp() || applicationInfo.isSystemApp()) {
396                    return;
397                }
398            } catch (PackageManager.NameNotFoundException e) {
399                Log.w(TAG, "package not found: " + e);
400            }
401        }
402        throw new SecurityException("Caller must be system, default dialer or default SMS app");
403    }
404
405    /**
406     * TODO CTS:
407     * - Call should fail for random 3p apps.
408     */
409    public void enforceWritePermission() {
410        // Same check as read.
411        enforceReadPermission();
412    }
413}
414