summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMakoto Onuki <omakoto@google.com>2016-01-12 15:53:00 -0800
committerMakoto Onuki <omakoto@google.com>2016-01-14 17:43:50 -0800
commit28b6a0f277a997f5c117c3c5be3de604c7a38d12 (patch)
tree30c162929957234bc45ef6b98c233802b18d8b15
parent963604b4286899279832d0a83e2dd275a4ea6ff5 (diff)
downloadandroid_packages_providers_BlockedNumberProvider-28b6a0f277a997f5c117c3c5be3de604c7a38d12.tar.gz
android_packages_providers_BlockedNumberProvider-28b6a0f277a997f5c117c3c5be3de604c7a38d12.tar.bz2
android_packages_providers_BlockedNumberProvider-28b6a0f277a997f5c117c3c5be3de604c7a38d12.zip
Introduce "blocked phone number" provider
Bug 26232372 Change-Id: I735d2949f45f533c26063d413dd3dfb72f455711
-rw-r--r--Android.mk20
-rw-r--r--AndroidManifest.xml39
-rw-r--r--CleanSpec.mk49
-rw-r--r--MODULE_LICENSE_APACHE20
-rw-r--r--NOTICE190
-rw-r--r--res/values/strings.xml25
-rw-r--r--src/com/android/providers/blockednumber/BlockedNumberDatabaseHelper.java99
-rw-r--r--src/com/android/providers/blockednumber/BlockedNumberProvider.java392
-rw-r--r--src/com/android/providers/blockednumber/Utils.java94
-rw-r--r--tests/Android.mk23
-rw-r--r--tests/AndroidManifest.xml31
-rw-r--r--tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java243
-rw-r--r--tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java41
-rw-r--r--tests/src/com/android/providers/blockednumber/MyMockContext.java71
14 files changed, 1317 insertions, 0 deletions
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..3b3aa45
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,20 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES += android-common guava
+
+LOCAL_EMMA_COVERAGE_FILTER := +com.android.providers.blockednumber.*
+
+LOCAL_PACKAGE_NAME := BlockedNumberProvider
+LOCAL_CERTIFICATE := shared
+LOCAL_PRIVILEGED_MODULE := true
+
+include $(BUILD_PACKAGE)
+
+# Use the following include to make our test apk.
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..d9bd2c9
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.blockednumber"
+ android:sharedUserId="android.uid.shared">
+
+<!--
+ TODO: Make it DE. Add the following to <application>. See go/android-fbe-apis
+ android:encryptionAware=”true”
+ android:forceDeviceEncrypted=”true”
+-->
+
+ <application
+ android:process="android.process.acore"
+ android:label="@string/app_label"
+ android:allowBackup="false"
+ android:usesCleartextTraffic="false">
+
+ <provider android:name="BlockedNumberProvider"
+ android:authorities="com.android.blockednumber"
+ android:multiprocess="false"
+ android:exported="true">
+ </provider>
+ </application>
+</manifest>
diff --git a/CleanSpec.mk b/CleanSpec.mk
new file mode 100644
index 0000000..c087cb8
--- /dev/null
+++ b/CleanSpec.mk
@@ -0,0 +1,49 @@
+# 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.
+#
+
+# If you don't need to do a full clean build but would like to touch
+# a file or delete some intermediate files, add a clean step to the end
+# of the list. These steps will only be run once, if they haven't been
+# run before.
+#
+# E.g.:
+# $(call add-clean-step, touch -c external/sqlite/sqlite3.h)
+# $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libz_intermediates)
+#
+# Always use "touch -c" and "rm -f" or "rm -rf" to gracefully deal with
+# files that are missing or have been moved.
+#
+# Use $(PRODUCT_OUT) to get to the "out/target/product/blah/" directory.
+# Use $(OUT_DIR) to refer to the "out" directory.
+#
+# If you need to re-do something that's already mentioned, just copy
+# the command and add it to the bottom of the list. E.g., if a change
+# that you made last week required touching a file and a change you
+# made today requires touching the same file, just copy the old
+# touch step and add it to the end of the list.
+#
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
+
+# For example:
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/APPS/AndroidTests_intermediates)
+#$(call add-clean-step, rm -rf $(OUT_DIR)/target/common/obj/JAVA_LIBRARIES/core_intermediates)
+#$(call add-clean-step, find $(OUT_DIR) -type f -name "IGTalkSession*" -print0 | xargs -0 rm -f)
+#$(call add-clean-step, rm -rf $(PRODUCT_OUT)/data/*)
+
+# ************************************************
+# NEWER CLEAN STEPS MUST BE AT THE END OF THE LIST
+# ************************************************
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..b456062
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,190 @@
+
+ Copyright (c) 2005-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.
+
+ 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.
+
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..860e115
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- This is the label for the application that stores blocked phone numbers [CHAR LIMIT=NONE] -->
+ <string name="app_label">Blocked Numbers Storage</string>
+
+ <!-- This is the label for the provider that stores blocked phone numbers [CHAR LIMIT=NONE] -->
+ <string name="provider_label">Blocked Numbers</string>
+
+</resources>
diff --git a/src/com/android/providers/blockednumber/BlockedNumberDatabaseHelper.java b/src/com/android/providers/blockednumber/BlockedNumberDatabaseHelper.java
new file mode 100644
index 0000000..f3138fa
--- /dev/null
+++ b/src/com/android/providers/blockednumber/BlockedNumberDatabaseHelper.java
@@ -0,0 +1,99 @@
+/*
+ * 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.providers.blockednumber;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+public class BlockedNumberDatabaseHelper {
+ private static final String TAG = BlockedNumberProvider.TAG;
+
+ private static final int DATABASE_VERSION = 1;
+
+ private static final String DATABASE_NAME = "blockednumbers.db";
+
+ private static BlockedNumberDatabaseHelper sInstance;
+
+ private final Context mContext;
+
+ private final OpenHelper mOpenHelper;
+
+ public interface Tables {
+ String BLOCKED_NUMBERS = "blocked";
+ }
+
+ private static final class OpenHelper extends SQLiteOpenHelper {
+ public OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
+ int version) {
+ super(context, name, factory, version);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + Tables.BLOCKED_NUMBERS + " (" +
+ BlockedNumbers.COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ BlockedNumbers.COLUMN_ORIGINAL_NUMBER + " TEXT NOT NULL UNIQUE," +
+ BlockedNumbers.COLUMN_STRIPPED_NUMBER + " TEXT NOT NULL," +
+ BlockedNumbers.COLUMN_E164_NUMBER + " TEXT" +
+ ")");
+
+ db.execSQL("CREATE INDEX blocked_number_idx_stripped ON " + Tables.BLOCKED_NUMBERS +
+ " (" + BlockedNumbers.COLUMN_STRIPPED_NUMBER + ");");
+ db.execSQL("CREATE INDEX blocked_number_idx_e164 ON " + Tables.BLOCKED_NUMBERS + " (" +
+ BlockedNumbers.COLUMN_E164_NUMBER +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ }
+ }
+
+ @VisibleForTesting
+ public static BlockedNumberDatabaseHelper newInstanceForTest(Context context) {
+ return new BlockedNumberDatabaseHelper(context, /* instanceIsForTesting =*/ true);
+ }
+
+ private BlockedNumberDatabaseHelper(Context context, boolean instanceIsForTesting) {
+ mContext = context;
+ mOpenHelper = new OpenHelper(mContext,
+ instanceIsForTesting ? null : DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ public static synchronized BlockedNumberDatabaseHelper getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new BlockedNumberDatabaseHelper(context.getApplicationContext(),
+ /* instanceIsForTesting =*/ false);
+ }
+ return sInstance;
+ }
+
+ public SQLiteDatabase getReadableDatabase() {
+ return mOpenHelper.getReadableDatabase();
+ }
+
+ public SQLiteDatabase getWritableDatabase() {
+ return mOpenHelper.getWritableDatabase();
+ }
+
+ public void wipeForTest() {
+ getWritableDatabase().execSQL("DELETE FROM " + Tables.BLOCKED_NUMBERS);
+ }
+}
diff --git a/src/com/android/providers/blockednumber/BlockedNumberProvider.java b/src/com/android/providers/blockednumber/BlockedNumberProvider.java
new file mode 100644
index 0000000..13e977c
--- /dev/null
+++ b/src/com/android/providers/blockednumber/BlockedNumberProvider.java
@@ -0,0 +1,392 @@
+/*
+ * 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.providers.blockednumber;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.*;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.os.*;
+import android.os.Process;
+import android.provider.BlockedNumberContract;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import com.android.common.content.ProjectionMap;
+import com.android.providers.blockednumber.BlockedNumberDatabaseHelper.Tables;
+
+/**
+ * Blocked phone number provider.
+ *
+ * <p>Note the provider allows emergency numbers. The caller (telecom) should never call it with
+ * emergency numbers.
+ */
+public class BlockedNumberProvider extends ContentProvider {
+ static final String TAG = "BlockedNumbers";
+
+ private static final boolean DEBUG = true; // DO NOT SUBMIT WITH TRUE.
+
+ private static final int BLOCKED_LIST = 1000;
+ private static final int BLOCKED_ID = 1001;
+
+ private static final UriMatcher sUriMatcher;
+
+ static {
+ sUriMatcher = new UriMatcher(0);
+ sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked", BLOCKED_LIST);
+ sUriMatcher.addURI(BlockedNumberContract.AUTHORITY, "blocked/#", BLOCKED_ID);
+ }
+
+ private static final ProjectionMap sBlockedNumberColumns = ProjectionMap.builder()
+ .add(BlockedNumberContract.BlockedNumbers.COLUMN_ID)
+ .add(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER)
+ .add(BlockedNumberContract.BlockedNumbers.COLUMN_STRIPPED_NUMBER)
+ .add(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER)
+ .build();
+
+ private static final String ID_SELECTION =
+ BlockedNumberContract.BlockedNumbers.COLUMN_ID + "=?";
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ BlockedNumberDatabaseHelper getDbHelper() {
+ return BlockedNumberDatabaseHelper.getInstance(getContext());
+ }
+
+ /**
+ * TODO CTS:
+ * - BLOCKED_LIST
+ * - BLOCKED_ID
+ * - Other random URLs should fail
+ */
+ @Override
+ public String getType(@NonNull Uri uri) {
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case BLOCKED_LIST:
+ return BlockedNumberContract.BlockedNumbers.CONTENT_TYPE;
+ case BLOCKED_ID:
+ return BlockedNumberContract.BlockedNumbers.CONTENT_ITEM_TYPE;
+ default:
+ throw new IllegalArgumentException("Unsupported URI: " + uri);
+ }
+ }
+
+ /**
+ * TODO CTS:
+ * - BLOCKED_LIST
+ * With no columns should fail
+ * With COLUMN_INDEX_ORIGINAL only
+ * With COLUMN_INDEX_E164 only should fail
+ * With COLUMN_INDEX_ORIGINAL + COLUMN_INDEX_E164
+ * With with throwIfSpecified columns, should fail.
+ *
+ * - BLOCKED_ID should fail
+ * - Other random URLs should fail
+ */
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ enforceWritePermission();
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case BLOCKED_LIST:
+ return insertBlockedNumber(values);
+ default:
+ throw new IllegalArgumentException("Unsupported URI: " + uri);
+ }
+ }
+
+ /**
+ * Implements the "blocked/" insert.
+ */
+ private Uri insertBlockedNumber(ContentValues cv) {
+ throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_ID);
+ throwIfSpecified(cv, BlockedNumberContract.BlockedNumbers.COLUMN_STRIPPED_NUMBER);
+
+ final String phoneNumber = cv.getAsString(
+ BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
+
+ if (TextUtils.isEmpty(phoneNumber)) {
+ throw new IllegalArgumentException("Missing a required column " +
+ BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER);
+ }
+
+ // Sanitize the input and fill in with autogenerated columns.
+ final String strippedNumber = Utils.stripPhoneNumber(phoneNumber);
+ final String e164Number = Utils.getE164Number(getContext(), strippedNumber,
+ cv.getAsString(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER));
+
+ cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_STRIPPED_NUMBER, strippedNumber);
+ cv.put(BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER, e164Number);
+
+ // Then insert.
+ final long id = getDbHelper().getWritableDatabase().insertOrThrow(
+ BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS, null, cv);
+
+ return ContentUris.withAppendedId(BlockedNumberContract.BlockedNumbers.CONTENT_URI, id);
+ }
+
+ private static void throwIfSpecified(ContentValues cv, String column) {
+ if (cv.containsKey(column)) {
+ throw new IllegalArgumentException("Column " + column + " must not be specified");
+ }
+ }
+
+ /**
+ * TODO CTS:
+ * - Any call should fail
+ */
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ throw new UnsupportedOperationException(
+ "Update is not supported. Use delete + insert instead");
+ }
+
+ /**
+ * TODO CTS:
+ * - BLOCKED_LIST, with selection and without.
+ * - BLOCKED_ID , with selection and without. With should fail.
+ */
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection,
+ @Nullable String[] selectionArgs) {
+ enforceWritePermission();
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case BLOCKED_LIST:
+ return deleteBlockedNumber(selection, selectionArgs);
+ case BLOCKED_ID:
+ return deleteBlockedNumberWithId(ContentUris.parseId(uri), selection);
+ default:
+ throw new IllegalArgumentException("Unsupported URI: " + uri);
+ }
+ }
+
+ /**
+ * Implements the "blocked/#" delete.
+ */
+ private int deleteBlockedNumberWithId(long id, String selection) {
+ throwForNonEmptySelection(selection);
+
+ return deleteBlockedNumber(ID_SELECTION, new String[]{Long.toString(id)});
+ }
+
+ /**
+ * Implements the "blocked/" delete.
+ */
+ private int deleteBlockedNumber(String selection, String[] selectionArgs) {
+ final SQLiteDatabase db = getDbHelper().getWritableDatabase();
+
+ // When selection is specified, compile it within (...) to detect SQL injection.
+ if (!TextUtils.isEmpty(selection)) {
+ db.validateSql("select 1 FROM " + Tables.BLOCKED_NUMBERS + " WHERE " +
+ Utils.wrapSelectionWithParens(selection),
+ /* cancellationSignal =*/ null);
+ }
+
+ return getDbHelper().getWritableDatabase().delete(
+ BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS,
+ selection, selectionArgs);
+ }
+
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
+ @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ enforceReadPermission();
+
+ return query(uri, projection, selection, selectionArgs, sortOrder, null);
+ }
+
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
+ @Nullable String[] selectionArgs, @Nullable String sortOrder,
+ @Nullable CancellationSignal cancellationSignal) {
+ enforceReadPermission();
+
+ final int match = sUriMatcher.match(uri);
+ switch (match) {
+ case BLOCKED_LIST:
+ return queryBlockedList(projection, selection, selectionArgs, sortOrder,
+ cancellationSignal);
+ case BLOCKED_ID:
+ return queryBlockedListWithId(ContentUris.parseId(uri), projection, selection,
+ cancellationSignal);
+ default:
+ throw new IllegalArgumentException("Unsupported URI: " + uri);
+ }
+ }
+
+ /**
+ * Implements the "blocked/#" query.
+ */
+ private Cursor queryBlockedListWithId(long id, String[] projection, String selection,
+ CancellationSignal cancellationSignal) {
+ throwForNonEmptySelection(selection);
+
+ return queryBlockedList(projection, ID_SELECTION, new String[]{Long.toString(id)},
+ null, cancellationSignal);
+ }
+
+ /**
+ * Implements the "blocked/" query.
+ */
+ private Cursor queryBlockedList(String[] projection, String selection, String[] selectionArgs,
+ String sortOrder, CancellationSignal cancellationSignal) {
+ SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+ qb.setStrict(true);
+ qb.setTables(BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS);
+ qb.setProjectionMap(sBlockedNumberColumns);
+
+ return qb.query(getDbHelper().getReadableDatabase(), projection, selection, selectionArgs,
+ /* groupBy =*/ null, /* having =*/null, sortOrder,
+ /* limit =*/ null, cancellationSignal);
+ }
+
+ private void throwForNonEmptySelection(String selection) {
+ if (!TextUtils.isEmpty(selection)) {
+ throw new IllegalArgumentException(
+ "When ID is specified in URI, selection must be null");
+ }
+ }
+
+ /**
+ * TODO CTS:
+ * - METHOD_IS_BLOCKED with various matching / non-matching arguments.
+ *
+ * - other random methods should fail
+ */
+ @Override
+ public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) {
+ enforceReadPermission();
+
+ final Bundle res = new Bundle();
+ switch (method) {
+ case BlockedNumberContract.METHOD_IS_BLOCKED:
+ res.putBoolean(BlockedNumberContract.RES_NUMBER_IS_BLOCKED, isBlocked(arg));
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupported method " + method);
+ }
+ return res;
+ }
+
+ private boolean isBlocked(String phoneNumber) {
+ final String inStripped = Utils.stripPhoneNumber(phoneNumber);
+ if (TextUtils.isEmpty(inStripped)) {
+ return false;
+ }
+
+ final String inE164 = Utils.getE164Number(getContext(), inStripped, null); // may be empty.
+
+ if (DEBUG) {
+ Log.d(TAG, String.format("isBlocked: in=%s, stripped=%s, e164=%s", phoneNumber,
+ inStripped, inE164));
+ }
+
+ final Cursor c = getDbHelper().getReadableDatabase().rawQuery(
+ "SELECT " +
+ BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER + "," +
+ BlockedNumberContract.BlockedNumbers.COLUMN_STRIPPED_NUMBER + "," +
+ BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER +
+ " FROM " + BlockedNumberDatabaseHelper.Tables.BLOCKED_NUMBERS +
+ " WHERE " + BlockedNumberContract.BlockedNumbers.COLUMN_STRIPPED_NUMBER + "=?1" +
+ " OR (?2 != '' AND " +
+ BlockedNumberContract.BlockedNumbers.COLUMN_E164_NUMBER + "=?2)",
+ new String[] {inStripped, inE164}
+ );
+ try {
+ while (c.moveToNext()) {
+ if (DEBUG) {
+ final String original = c.getString(0);
+ final String stripped = c.getString(1);
+ final String e164 = c.getString(2);
+
+ Log.d(TAG, String.format(" match found: original=%s, stripped=%s, e164=%s",
+ original, stripped, e164));
+ }
+ return true;
+ }
+ } finally {
+ c.close();
+ }
+ // No match found.
+ return false;
+ }
+
+ /**
+ * Throws {@link SecurityException} when the caller is not root, system, the system dialer,
+ * the user selected dialer, or the default SMS app.
+ *
+ * NOT TESTED YET
+ *
+ * TODO CTS:
+ * - Call should fail for random 3p apps.
+ *
+ * TODO Add a permission to allow the contacts app to access?
+ * TODO Add a permission to allow carrier apps?
+ */
+ public void enforceReadPermission() {
+ final int callingUid = Binder.getCallingUid();
+
+ // System and root can always call it. (and myself)
+ if (UserHandle.isSameApp(callingUid, android.os.Process.SYSTEM_UID)
+ || (callingUid == Process.ROOT_UID)
+ || (callingUid == Process.myUid())) {
+ return;
+ }
+
+ final String callingPackage = getCallingPackage();
+ if (TextUtils.isEmpty(callingPackage)) {
+ Log.w(TAG, "callingPackage not accessible");
+ } else {
+
+ final TelecomManager telecom = getContext().getSystemService(TelecomManager.class);
+
+ if (callingPackage.equals(telecom.getDefaultDialerPackage())
+ || callingPackage.equals(telecom.getSystemDialerPackage())) {
+ return;
+ }
+
+ // Allow the default SMS app and the dialer app to access it.
+ final AppOpsManager appOps = getContext().getSystemService(AppOpsManager.class);
+
+ if (appOps.noteOp(AppOpsManager.OP_WRITE_SMS,
+ Binder.getCallingUid(), callingPackage) == AppOpsManager.MODE_ALLOWED) {
+ return;
+ }
+ }
+ throw new SecurityException("Caller must be system, default dialer or default SMS app");
+ }
+
+ /**
+ * TODO CTS:
+ * - Call should fail for random 3p apps.
+ */
+ public void enforceWritePermission() {
+ // Same check as read.
+ enforceReadPermission();
+ }
+}
diff --git a/src/com/android/providers/blockednumber/Utils.java b/src/com/android/providers/blockednumber/Utils.java
new file mode 100644
index 0000000..e890634
--- /dev/null
+++ b/src/com/android/providers/blockednumber/Utils.java
@@ -0,0 +1,94 @@
+/*
+ * 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.providers.blockednumber;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.location.Country;
+import android.location.CountryDetector;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+
+import java.util.Locale;
+
+public class Utils {
+ private Utils() {
+ }
+
+ public static final int MIN_INDEX_LEN = 8;
+
+ /**
+ * @return The current country code.
+ */
+ public static @NonNull String getCurrentCountryIso(@NonNull Context context) {
+ final CountryDetector detector = (CountryDetector) context.getSystemService(
+ Context.COUNTRY_DETECTOR);
+ if (detector != null) {
+ final Country country = detector.detectCountry();
+ if (country != null) {
+ return country.getCountryIso();
+ }
+ }
+ final Locale locale = context.getResources().getConfiguration().locale;
+ return locale.getCountry();
+ }
+
+ /**
+ * Strip formatting characters and the non-phone number portion from a phone number. e.g.
+ * "+1-408-123-4444;123" to "+14081234444".
+ *
+ * <p>Special case: if a number contains '@', it's considered as an email address and returned
+ * unmodified.
+ */
+ public static @NonNull String stripPhoneNumber(@Nullable String phoneNumber) {
+ if (TextUtils.isEmpty(phoneNumber)) {
+ return "";
+ }
+ if (phoneNumber.contains("@")) {
+ return phoneNumber;
+ }
+ return PhoneNumberUtils.extractNetworkPortion(phoneNumber);
+ }
+
+ /**
+ * Converts a phone number to an E164 number, assuming the current country. If {@code
+ * incomingE16Number} is provided, it'll just strip it and returns. If the number is not valid,
+ * it'll return "".
+ *
+ * <p>Special case: if {@code rawNumber} contains '@', it's considered as an email address and
+ * returned unmodified.
+ */
+ public static @NonNull String getE164Number(@NonNull Context context,
+ @Nullable String rawNumber, @Nullable String incomingE16Number) {
+ if (rawNumber != null && rawNumber.contains("@")) {
+ return rawNumber;
+ }
+ if (!TextUtils.isEmpty(incomingE16Number)) {
+ return stripPhoneNumber(incomingE16Number);
+ }
+ if (TextUtils.isEmpty(rawNumber)) {
+ return "";
+ }
+ final String e164 =
+ PhoneNumberUtils.formatNumberToE164(rawNumber, getCurrentCountryIso(context));
+ return e164 == null ? "" : e164;
+ }
+
+ public static @Nullable String wrapSelectionWithParens(@Nullable String selection) {
+ return TextUtils.isEmpty(selection) ? null : "(" + selection + ")";
+ }
+}
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..d85d7bb
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,23 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+# We only want this apk build for tests.
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ mockito-target \
+ android-support-test
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+# Only compile source java files in this apk.
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := BlockedNumberProviderTests
+
+LOCAL_INSTRUMENTATION_FOR := BlockedNumberProvider
+LOCAL_CERTIFICATE := shared
+
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml
new file mode 100644
index 0000000..0405820
--- /dev/null
+++ b/tests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.providers.blockednumber.tests"
+ android:sharedUserId="android.uid.shared">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.providers.blockednumber"
+ android:label="Blocked Numbers Tests">
+ </instrumentation>
+
+</manifest>
diff --git a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java
new file mode 100644
index 0000000..8ec71eb
--- /dev/null
+++ b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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.providers.blockednumber;
+
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteConstraintException;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.BlockedNumberContract;
+import android.provider.BlockedNumberContract.BlockedNumbers;
+import android.test.AndroidTestCase;
+import android.test.MoreAsserts;
+import junit.framework.Assert;
+
+/**
+ m BlockedNumberProviderTests &&
+ adb install -r \
+ ${ANDROID_PRODUCT_OUT}/data/app/BlockedNumberProviderTests/BlockedNumberProviderTests.apk &&
+ adb shell am instrument -e class com.android.providers.blockednumber.BlockedNumberProviderTest \
+ -w com.android.providers.blockednumber.tests/android.support.test.runner.AndroidJUnitRunner
+ */
+public class BlockedNumberProviderTest extends AndroidTestCase {
+ private Context mRealTestContext;
+ private MyMockContext mMockContext;
+ private ContentResolver mResolver;
+
+ /** Whether the country detector thinks the device is in USA. */
+ private boolean mInUSA;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+
+ mRealTestContext = getContext();
+ mMockContext = new MyMockContext(mRealTestContext);
+ mResolver = mMockContext.getContentResolver();
+ mInUSA = "US".equals(Utils.getCurrentCountryIso(mRealTestContext));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mMockContext.shutdown();
+
+ super.tearDown();
+ }
+
+ private static ContentValues cv(Object... namesAndValues) {
+ Assert.assertTrue((namesAndValues.length % 2) == 0);
+
+ final ContentValues ret = new ContentValues();
+ for (int i = 1; i < namesAndValues.length; i += 2) {
+ final String name = namesAndValues[i - 1].toString();
+ final Object value = namesAndValues[i];
+ if (value == null) {
+ ret.putNull(name);
+ } else if (value instanceof String) {
+ ret.put(name, (String) value);
+ } else if (value instanceof Integer) {
+ ret.put(name, (Integer) value);
+ } else if (value instanceof Long) {
+ ret.put(name, (Long) value);
+ } else {
+ Assert.fail("Unsupported type: " + value.getClass().getSimpleName());
+ }
+ }
+ return ret;
+ }
+
+ private void assertRowCount(int count, Uri uri) {
+ try (Cursor c = mResolver.query(uri, null, null, null, null)) {
+ assertEquals(count, c.getCount());
+ }
+ }
+
+ public void testGetType() {
+ assertEquals(BlockedNumbers.CONTENT_TYPE, mResolver.getType(
+ BlockedNumbers.CONTENT_URI));
+
+ assertEquals(BlockedNumbers.CONTENT_ITEM_TYPE, mResolver.getType(
+ ContentUris.withAppendedId(BlockedNumbers.CONTENT_URI, 1)));
+
+ assertNull(mResolver.getType(
+ Uri.withAppendedPath(BlockedNumberContract.AUTHORITY_URI, "invalid")));
+ }
+
+ public void testInsert() {
+ insertExpectingFailure(cv());
+ insertExpectingFailure(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, null));
+ insertExpectingFailure(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, ""));
+ insertExpectingFailure(cv(BlockedNumbers.COLUMN_ID, 1));
+ insertExpectingFailure(cv(BlockedNumbers.COLUMN_STRIPPED_NUMBER, 1));
+ insertExpectingFailure(cv(BlockedNumbers.COLUMN_E164_NUMBER, "1"));
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "123"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1-2-3"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1-408-454-1111"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1-408-454-2222"));
+
+ try {
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1-408-454-2222"));
+ fail();
+ } catch (SQLiteConstraintException expected) {
+ }
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1-408-4542222"));
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "045-381-1111",
+ BlockedNumbers.COLUMN_E164_NUMBER, "+81453811111"));
+
+ assertRowCount(6, BlockedNumbers.CONTENT_URI);
+
+ // TODO Check the table content.
+ }
+
+ private Uri insert(ContentValues cv) {
+ final Uri uri = mResolver.insert(BlockedNumbers.CONTENT_URI, cv);
+ assertNotNull(uri);
+
+ // Make sure the URI exists.
+ try (Cursor c = mResolver.query(uri, null, null, null, null)) {
+ assertEquals(1, c.getCount());
+ }
+ return uri;
+ }
+
+ private void insertExpectingFailure(ContentValues cv) {
+ try {
+ mResolver.insert(
+ BlockedNumbers.CONTENT_URI, cv());
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ public void testDelete() {
+ // Prepare test data
+ Uri u1 = insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "123"));
+ Uri u2 = insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1-2-3"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1-408-454-1111"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1-408-454-2222"));
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "045-381-1111",
+ BlockedNumbers.COLUMN_E164_NUMBER, "12345"));
+
+ assertRowCount(5, BlockedNumbers.CONTENT_URI);
+
+ // Delete and check the # of remaining rows.
+
+ mResolver.delete(u1, null, null);
+ assertRowCount(4, BlockedNumbers.CONTENT_URI);
+
+ try {
+ mResolver.delete(u2, "1=1", null);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ MoreAsserts.assertContainsRegex("selection must be null", expected.getMessage());
+ }
+
+ mResolver.delete(u2, null, null);
+ assertRowCount(3, BlockedNumbers.CONTENT_URI);
+
+ mResolver.delete(BlockedNumbers.CONTENT_URI,
+ BlockedNumbers.COLUMN_E164_NUMBER + "=?",
+ new String[]{"12345"});
+ assertRowCount(2, BlockedNumbers.CONTENT_URI);
+
+ // SQL injection should be detected.
+ try {
+ mResolver.delete(BlockedNumbers.CONTENT_URI, "; DROP TABLE blocked; ", null);
+ fail();
+ } catch (SQLiteException expected) {
+ }
+ assertRowCount(2, BlockedNumbers.CONTENT_URI);
+
+ mResolver.delete(BlockedNumbers.CONTENT_URI, null, null);
+ assertRowCount(0, BlockedNumbers.CONTENT_URI);
+ }
+
+ public void testUpdate() {
+ try {
+ mResolver.update(BlockedNumbers.CONTENT_URI, cv(),
+ /* selection =*/ null, /* args =*/ null);
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ MoreAsserts.assertContainsRegex("Update is not supported", expected.getMessage());
+ }
+ }
+
+ public void testIsBlocked() {
+ // Prepare test data
+ Uri u1 = insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "123"));
+ Uri u2 = insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1.2-3"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "+1-500-454-1111"));
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "1-500-454-2222"));
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "045-111-2222",
+ BlockedNumbers.COLUMN_E164_NUMBER, "+81-45-111-2222"));
+
+ insert(cv(BlockedNumbers.COLUMN_ORIGINAL_NUMBER, "abc.def@gmail.com"));
+
+ // Check
+ assertIsBlocked(true, "123");
+ assertIsBlocked(false, "1234");
+ assertIsBlocked(true, "+81451112222");
+ assertIsBlocked(true, "+81 45 111 2222");
+ assertIsBlocked(true, "045 111 2222");
+
+ if (mInUSA) {
+ // Probably won't work outside of the +1 region.
+ assertIsBlocked(true, "500-454 1111");
+ assertIsBlocked(true, "500-454 2222");
+ }
+ assertIsBlocked(true, "+1 500-454 1111");
+ assertIsBlocked(true, "1 500-454 1111");
+
+ assertIsBlocked(true, "abc.def@gmail.com");
+ assertIsBlocked(false, "abc.def@gmail.co");
+ assertIsBlocked(false, "bc.def@gmail.com");
+ assertIsBlocked(false, "abcdef@gmail.com");
+ }
+
+ private void assertIsBlocked(boolean expected, String phoneNumber) {
+ assertEquals(expected, BlockedNumberContract.isBlocked(mMockContext, phoneNumber));
+ }
+}
diff --git a/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java
new file mode 100644
index 0000000..3c95089
--- /dev/null
+++ b/tests/src/com/android/providers/blockednumber/BlockedNumberProviderTestable.java
@@ -0,0 +1,41 @@
+/*
+ * 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.providers.blockednumber;
+
+public class BlockedNumberProviderTestable extends BlockedNumberProvider {
+ private BlockedNumberDatabaseHelper mDbHelper;
+
+ @Override
+ public boolean onCreate() {
+ super.onCreate();
+
+ mDbHelper = BlockedNumberDatabaseHelper.newInstanceForTest(getContext());
+
+ return true;
+ }
+
+ @Override
+ BlockedNumberDatabaseHelper getDbHelper() {
+ return mDbHelper;
+ }
+
+ @Override
+ public void shutdown() {
+ mDbHelper.getReadableDatabase().close();
+
+ super.shutdown();
+ }
+}
diff --git a/tests/src/com/android/providers/blockednumber/MyMockContext.java b/tests/src/com/android/providers/blockednumber/MyMockContext.java
new file mode 100644
index 0000000..14d4f84
--- /dev/null
+++ b/tests/src/com/android/providers/blockednumber/MyMockContext.java
@@ -0,0 +1,71 @@
+/*
+ * 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.providers.blockednumber;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.ProviderInfo;
+import android.location.CountryDetector;
+import android.provider.BlockedNumberContract;
+import android.test.mock.MockContentResolver;
+import android.test.mock.MockContext;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class MyMockContext extends MockContext {
+ public final Context realTestContext;
+
+ @Mock
+ CountryDetector countryDetector;
+
+ MockContentResolver resolver;
+
+ BlockedNumberProviderTestable provider;
+
+ public MyMockContext(Context realTestContext) {
+ this.realTestContext = realTestContext;
+ MockitoAnnotations.initMocks(this);
+
+ resolver = new MockContentResolver();
+
+ provider = new BlockedNumberProviderTestable();
+
+ final ProviderInfo info = new ProviderInfo();
+ info.authority = BlockedNumberContract.AUTHORITY;
+ provider.attachInfoForTesting(realTestContext, info);
+
+ resolver.addProvider(BlockedNumberContract.AUTHORITY, provider);
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ switch (name) {
+ case Context.COUNTRY_DETECTOR:
+ return countryDetector;
+ }
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ContentResolver getContentResolver() {
+ return resolver;
+ }
+
+ public void shutdown() {
+ provider.shutdown();
+ }
+}
+