diff options
3 files changed, 414 insertions, 5 deletions
diff --git a/src/java/android/provider/Telephony.java b/src/java/android/provider/Telephony.java index a7e779d6c..bcff354e5 100644 --- a/src/java/android/provider/Telephony.java +++ b/src/java/android/provider/Telephony.java @@ -832,6 +832,27 @@ public final class Telephony { public static final int RESULT_SMS_DUPLICATED = 5; /** + * Used internally: The sender of the SMS was blacklisted + * for not being listed in the contact list + * @hide + */ + public static final int RESULT_SMS_BLACKLISTED_UNKNOWN = 6; + + /** + * Used internally: The sender of the SMS was blacklisted + * for being listed in the blacklist + * @hide + */ + public static final int RESULT_SMS_BLACKLISTED_LIST = 7; + + /** + * Used internally: The sender of the SMS was blacklisted + * for matching a blacklist regex entry + * @hide + */ + public static final int RESULT_SMS_BLACKLISTED_REGEX = 8; + + /** * Activity action: Ask the user to change the default * SMS application. This will show a dialog that asks the * user whether they want to replace the current default @@ -3023,4 +3044,76 @@ public final class Telephony { CMAS_CERTAINTY }; } + + /** + * Contains phone numbers that are blacklisted + * for phone and/or message purposes. + * @hide + */ + public static final class Blacklist implements BaseColumns { + /** + * The content:// style URL for this table + */ + public static final Uri CONTENT_URI = + + Uri.parse("content://blacklist"); + + /** + * The content:// style URL for filtering this table by number. + * When using this, make sure the number is correctly encoded + * when appended to the Uri. + */ + public static final Uri CONTENT_FILTER_BYNUMBER_URI = + Uri.parse("content://blacklist/bynumber"); + + /** + * The content:// style URL for filtering this table on phone numbers + */ + public static final Uri CONTENT_PHONE_URI = + Uri.parse("content://blacklist/phone"); + + /** + * The content:// style URL for filtering this table on message numbers + */ + public static final Uri CONTENT_MESSAGE_URI = + Uri.parse("content://blacklist/message"); + + + /** + * Query parameter used to match numbers by regular-expression like + * matching. Supported are the '*' and the '.' operators. + * <p> + * TYPE: boolean + */ + public static final String REGEX_KEY = "regex"; + + /** + * The default sort order for this table + */ + public static final String DEFAULT_SORT_ORDER = "number ASC"; + + /** + * The phone number as the user entered it. + * <P>Type: TEXT</P> + */ + public static final String NUMBER = "number"; + + /** + * Whether the number contains a regular expression pattern + * <P>Type: BOOLEAN (read only)</P> + */ + public static final String IS_REGEX = "is_regex"; + + /** + * Blacklisting mode for phone calls + * <P>Type: INTEGER (int)</P> + */ + public static final String PHONE_MODE = "phone"; + + /** + * Blacklisting mode for messages + * <P>Type: INTEGER (int)</P> + */ + public static final String MESSAGE_MODE = "message"; + } } diff --git a/src/java/com/android/internal/telephony/InboundSmsHandler.java b/src/java/com/android/internal/telephony/InboundSmsHandler.java index dde1c4d51..50bc6b37c 100644 --- a/src/java/com/android/internal/telephony/InboundSmsHandler.java +++ b/src/java/com/android/internal/telephony/InboundSmsHandler.java @@ -72,6 +72,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.uicc.UiccCard; import com.android.internal.telephony.uicc.UiccController; +import com.android.internal.telephony.util.BlacklistUtils; import com.android.internal.util.HexDump; import com.android.internal.util.State; import com.android.internal.util.StateMachine; @@ -558,20 +559,40 @@ public abstract class InboundSmsHandler extends StateMachine { return; } - int result; + int result, blacklistMatchType = -1; + SmsMessage sms = null; + try { - SmsMessage sms = (SmsMessage) ar.result; + sms = (SmsMessage) ar.result; result = dispatchMessage(sms.mWrappedSmsMessage); } catch (RuntimeException ex) { loge("Exception dispatching message", ex); result = Intents.RESULT_SMS_GENERIC_ERROR; } + // Translate (internal) blacklist check results to + // RESULT_SMS_HANDLED + match type + switch (result) { + case Intents.RESULT_SMS_BLACKLISTED_UNKNOWN: + blacklistMatchType = BlacklistUtils.MATCH_UNKNOWN; + result = Intents.RESULT_SMS_HANDLED; + break; + case Intents.RESULT_SMS_BLACKLISTED_LIST: + blacklistMatchType = BlacklistUtils.MATCH_LIST; + result = Intents.RESULT_SMS_HANDLED; + break; + case Intents.RESULT_SMS_BLACKLISTED_REGEX: + blacklistMatchType = BlacklistUtils.MATCH_REGEX; + result = Intents.RESULT_SMS_HANDLED; + break; + } + + // RESULT_OK means that the SMS will be acknowledged by special handling, // e.g. for SMS-PP data download. Any other result, we should ack here. if (result != Activity.RESULT_OK) { boolean handled = (result == Intents.RESULT_SMS_HANDLED); - notifyAndAcknowledgeLastIncomingSms(handled, result, null); + notifyAndAcknowledgeLastIncomingSms(handled, result, blacklistMatchType, sms, null); } } @@ -678,14 +699,26 @@ public abstract class InboundSmsHandler extends StateMachine { * and send an acknowledge message to the network. * @param success indicates that last message was successfully received. * @param result result code indicating any error + * @param blacklistMatchType blacklist type if the message was blacklisted, + * -1 if it wasn't blacklisted + * @param sms incoming SMS * @param response callback message sent when operation completes. */ private void notifyAndAcknowledgeLastIncomingSms(boolean success, - int result, Message response) { - if (!success) { + int result, int blacklistMatchType, SmsMessage sms, Message response) { + if (!success || blacklistMatchType >= 0) { // broadcast SMS_REJECTED_ACTION intent Intent intent = new Intent(Intents.SMS_REJECTED_ACTION); intent.putExtra("result", result); + intent.putExtra("blacklisted", blacklistMatchType >= 0); + if (blacklistMatchType >= 0) { + intent.putExtra("blacklistMatchType", blacklistMatchType); + } + if (sms != null) { + intent.putExtra("sender", sms.getOriginatingAddress()); + intent.putExtra("timestamp", sms.getTimestampMillis()); + } + if (DBG) log("notifyAndAcknowledgeLastIncomingSms(): reject intent= " + intent); mContext.sendBroadcast(intent, android.Manifest.permission.RECEIVE_SMS); } acknowledgeLastIncomingSms(success, result, response); @@ -707,6 +740,11 @@ public abstract class InboundSmsHandler extends StateMachine { * @return {@link Intents#RESULT_SMS_HANDLED} if the message was accepted, or an error status */ protected int dispatchNormalMessage(SmsMessageBase sms) { + int blacklistResult = checkIfBlacklisted(sms); + if (blacklistResult != Intents.RESULT_SMS_HANDLED) { + return blacklistResult; + } + SmsHeader smsHeader = sms.getUserDataHeader(); InboundSmsTracker tracker; @@ -742,6 +780,22 @@ public abstract class InboundSmsHandler extends StateMachine { tracker.getDestPort() == -1 /* de-dup if text message */); } + private int checkIfBlacklisted(SmsMessageBase sms) { + int result = BlacklistUtils.isListed(mContext, + sms.getOriginatingAddress(), BlacklistUtils.BLOCK_MESSAGES); + + switch (result) { + case BlacklistUtils.MATCH_UNKNOWN: + return Intents.RESULT_SMS_BLACKLISTED_UNKNOWN; + case BlacklistUtils.MATCH_LIST: + return Intents.RESULT_SMS_BLACKLISTED_LIST; + case BlacklistUtils.MATCH_REGEX: + return Intents.RESULT_SMS_BLACKLISTED_REGEX; + } + + return Intents.RESULT_SMS_HANDLED; + } + /** * Helper to add the tracker to the raw table and then send a message to broadcast it, if * successful. Returns the SMS intent status to return to the SMSC. diff --git a/src/java/com/android/internal/telephony/util/BlacklistUtils.java b/src/java/com/android/internal/telephony/util/BlacklistUtils.java new file mode 100644 index 000000000..f264f8d50 --- /dev/null +++ b/src/java/com/android/internal/telephony/util/BlacklistUtils.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2013 The CyanogenMod 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.internal.telephony.util; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.Telephony.Blacklist; +import android.telephony.PhoneNumberUtils; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.Pair; + +import java.util.Locale; + +import com.android.internal.telephony.CallerInfo; + +/** + * Blacklist Utility Class + * @hide + */ +public class BlacklistUtils { + private static final String TAG = "BlacklistUtils"; + private static final boolean DEBUG = false; + + // Blacklist matching type + public final static int MATCH_NONE = 0; + public final static int MATCH_PRIVATE = 1; + public final static int MATCH_UNKNOWN = 2; + public final static int MATCH_LIST = 3; + public final static int MATCH_REGEX = 4; + + public final static int BLOCK_CALLS = + Settings.System.BLACKLIST_BLOCK << Settings.System.BLACKLIST_PHONE_SHIFT; + public final static int BLOCK_MESSAGES = + Settings.System.BLACKLIST_BLOCK << Settings.System.BLACKLIST_MESSAGE_SHIFT; + + public static boolean addOrUpdate(Context context, String number, int flags, int valid) { + ContentValues cv = new ContentValues(); + + if ((valid & BLOCK_CALLS) != 0) { + cv.put(Blacklist.PHONE_MODE, (flags & BLOCK_CALLS) != 0 ? 1 : 0); + } + if ((valid & BLOCK_MESSAGES) != 0) { + cv.put(Blacklist.MESSAGE_MODE, (flags & BLOCK_MESSAGES) != 0 ? 1 : 0); + } + + Uri uri = Uri.withAppendedPath(Blacklist.CONTENT_FILTER_BYNUMBER_URI, number); + int count = context.getContentResolver().update(uri, cv, null, null); + + return count > 0; + } + + /** + * Check if the number is in the blacklist + * @param number: Number to check + * @return one of: MATCH_NONE, MATCH_PRIVATE, MATCH_UNKNOWN, MATCH_LIST or MATCH_REGEX + */ + public static int isListed(Context context, String number, int mode) { + if (!isBlacklistEnabled(context)) { + return MATCH_NONE; + } + + if (DEBUG) { + Log.d(TAG, "Checking number " + number + " against the Blacklist for mode " + mode); + } + + final String type; + + if (mode == BLOCK_CALLS) { + if (DEBUG) Log.d(TAG, "Checking if an incoming call should be blocked"); + type = Blacklist.PHONE_MODE; + } else if (mode == BLOCK_MESSAGES) { + if (DEBUG) Log.d(TAG, "Checking if an incoming message should be blocked"); + type = Blacklist.MESSAGE_MODE; + } else { + Log.e(TAG, "Invalid mode " + mode); + return MATCH_NONE; + } + + if (isBlacklistUnknownNumberEnabled(context, mode)) { + CallerInfo ci = CallerInfo.getCallerInfo(context, number); + if (ci == null || !ci.contactExists) { + if (DEBUG) Log.d(TAG, "Blacklist matched due to unknown number"); + return MATCH_UNKNOWN; + } + } + + // Private and unknown number matching + if (TextUtils.isEmpty(number)) { + if (isBlacklistPrivateNumberEnabled(context, mode)) { + if (DEBUG) Log.d(TAG, "Blacklist matched due to private number"); + return MATCH_PRIVATE; + } + return MATCH_NONE; + } + + Uri.Builder builder = Blacklist.CONTENT_FILTER_BYNUMBER_URI.buildUpon(); + builder.appendPath(number); + if (isBlacklistRegexEnabled(context)) { + builder.appendQueryParameter(Blacklist.REGEX_KEY, "1"); + } + + int result = MATCH_NONE; + Cursor c = context.getContentResolver().query(builder.build(), + new String[]{Blacklist.IS_REGEX, type}, null, null, null); + + if (c != null) { + if (DEBUG) Log.d(TAG, "Blacklist query successful, " + c.getCount() + " matches"); + int regexColumnIndex = c.getColumnIndexOrThrow(Blacklist.IS_REGEX); + int modeColumnIndex = c.getColumnIndexOrThrow(type); + boolean whitelisted = false; + + c.moveToPosition(-1); + while (c.moveToNext()) { + boolean isRegex = c.getInt(regexColumnIndex) != 0; + boolean blocked = c.getInt(modeColumnIndex) != 0; + + if (!isRegex) { + whitelisted = !blocked; + result = MATCH_LIST; + if (blocked) { + break; + } + } else if (blocked) { + result = MATCH_REGEX; + } + } + if (whitelisted) { + result = MATCH_NONE; + } + c.close(); + } + + if (DEBUG) Log.d(TAG, "Blacklist check result for number " + number + " is " + result); + return result; + } + + public static boolean isBlacklistEnabled(Context context) { + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.PHONE_BLACKLIST_ENABLED, 1, + UserHandle.USER_CURRENT_OR_SELF) != 0; + } + + public static boolean isBlacklistNotifyEnabled(Context context) { + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.PHONE_BLACKLIST_NOTIFY_ENABLED, 1, + UserHandle.USER_CURRENT_OR_SELF) != 0; + } + + public static boolean isBlacklistPrivateNumberEnabled(Context context, int mode) { + return (Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.PHONE_BLACKLIST_PRIVATE_NUMBER_MODE, 0, + UserHandle.USER_CURRENT_OR_SELF) & mode) != 0; + } + + public static boolean isBlacklistUnknownNumberEnabled(Context context, int mode) { + return (Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.PHONE_BLACKLIST_UNKNOWN_NUMBER_MODE, 0, + UserHandle.USER_CURRENT_OR_SELF) & mode) != 0; + } + + public static boolean isBlacklistRegexEnabled(Context context) { + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.PHONE_BLACKLIST_REGEX_ENABLED, 0, + UserHandle.USER_CURRENT_OR_SELF) != 0; + } + + public static Pair<String, Boolean> isValidBlacklistInput(Context context, String number) { + final Pair<String, Boolean> normalizeResult = BlacklistUtils.normalizeNumber( + context, number); + final String normalizedNumber = normalizeResult.first; + boolean isRegex = normalizedNumber.indexOf('%') >= 0 || + normalizedNumber.indexOf('_') >= 0; + // For non-regex numbers, apply additional validity checking if + // they didn't pass e164 normalization + if (!isRegex && !normalizeResult.second && !BlacklistUtils.isValidPhoneNumber(number)) { + // number was invalid + return new Pair<String, Boolean>(normalizedNumber, false); + } + return new Pair<String, Boolean>(normalizedNumber, true); + } + + /** + * Normalizes the passed in number and tries to format it according to E164. + * Returns a pair of + * - normalized number + * - boolean indicating whether the number is a E164 number or not + */ + public static Pair<String, Boolean> normalizeNumber(Context context, String number) { + int len = number.length(); + StringBuilder ret = new StringBuilder(len); + + for (int i = 0; i < len; i++) { + char c = number.charAt(i); + // Character.digit() supports ASCII and Unicode digits (fullwidth, Arabic-Indic, etc.) + int digit = Character.digit(c, 10); + if (digit != -1) { + ret.append(digit); + } else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + String actualNumber = PhoneNumberUtils.convertKeypadLettersToDigits(number); + return normalizeNumber(context, actualNumber); + } else if (i == 0 && c == '+') { + ret.append(c); + } else if (c == '*') { + // replace regex match-multiple character by SQL equivalent + ret.append('%'); + } else if (c == '.') { + // replace regex-match-single character by SQL equivalent + ret.append('_'); + } + } + + String normalizedNumber = ret.toString(); + String e164Number = toE164Number(context, normalizedNumber); + return Pair.create(e164Number != null ? e164Number : normalizedNumber, e164Number != null); + } + + public static String toE164Number(Context context, String src) { + // Try to retrieve the current ISO Country code + TelephonyManager tm = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + String countryCode = tm.getSimCountryIso(); + Locale numberLocale = TextUtils.isEmpty(countryCode) + ? context.getResources().getConfiguration().locale + : new Locale("", countryCode); + + return PhoneNumberUtils.formatNumberToE164(src, numberLocale.getCountry()); + } + + public static boolean isValidPhoneNumber(String address) { + for (int i = 0, count = address.length(); i < count; i++) { + if (!PhoneNumberUtils.isISODigit(address.charAt(i))) { + return false; + } + } + return true; + } + + public static boolean isInputRegex(String input) { + return input.indexOf('%') >= 0 || + input.indexOf('_') >= 0; + } +} |