diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index d7def44d64204..200654869c2c8 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -787,11 +787,17 @@ - - + + + + + + + + @@ -1095,6 +1101,7 @@ + diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 968b51c9288c4..e00986cf510bd 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2789,16 +2789,30 @@ Insert character - - Unknown app Sending SMS messages - - A large number of SMS messages are being sent. Touch OK to continue, or Cancel to stop sending. - - OK - - Cancel + + <b>%1$s</b> is sending a large number of SMS messages. Do you want to allow this app to continue sending messages? + + Allow + + Deny + + + + Send SMS to short code? + + Send premium SMS? + + <b>%1$s</b> would like to send a text message to <b>%2$s</b>, which appears to be an SMS short code.<p>Sending text messages to some short codes may cause your mobile account to be billed for premium services.<p>Do you want to allow this app to send the message? + + <b>%1$s</b> would like to send a text message to <b>%2$s</b>, which is a premium SMS short code.<p><b>Sending a message to this destination will cause your mobile account to be billed for premium services.</b><p>Do you want to allow this app to send the message? + + Send message + + Don\'t send + + Report malicious app diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml new file mode 100644 index 0000000000000..8b395af0a7b3c --- /dev/null +++ b/core/res/res/xml/sms_short_codes.xml @@ -0,0 +1,189 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/telephony/java/com/android/internal/telephony/PhoneBase.java b/telephony/java/com/android/internal/telephony/PhoneBase.java index 1b4cb157c61ee..2ac936593a429 100644 --- a/telephony/java/com/android/internal/telephony/PhoneBase.java +++ b/telephony/java/com/android/internal/telephony/PhoneBase.java @@ -250,7 +250,7 @@ public abstract class PhoneBase extends Handler implements Phone { // Initialize device storage and outgoing SMS usage monitors for SMSDispatchers. mSmsStorageMonitor = new SmsStorageMonitor(this); - mSmsUsageMonitor = new SmsUsageMonitor(context.getContentResolver()); + mSmsUsageMonitor = new SmsUsageMonitor(context); } public void dispose() { diff --git a/telephony/java/com/android/internal/telephony/SMSDispatcher.java b/telephony/java/com/android/internal/telephony/SMSDispatcher.java index d6f96ff6fe966..28b729d962b0a 100644 --- a/telephony/java/com/android/internal/telephony/SMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/SMSDispatcher.java @@ -17,34 +17,40 @@ package com.android.internal.telephony; import android.app.Activity; -import android.app.PendingIntent; import android.app.AlertDialog; +import android.app.PendingIntent; import android.app.PendingIntent.CanceledException; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; -import android.content.Intent; import android.content.DialogInterface; -import android.content.IntentFilter; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.database.SQLException; import android.net.Uri; import android.os.AsyncResult; +import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.PowerManager; import android.os.SystemProperties; import android.provider.Telephony; import android.provider.Telephony.Sms.Intents; -import android.provider.Settings; +import android.telephony.PhoneNumberUtils; +import android.telephony.ServiceState; import android.telephony.SmsCbMessage; import android.telephony.SmsMessage; -import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import android.text.Html; +import android.text.Spanned; import android.util.Log; import android.view.WindowManager; +import com.android.internal.R; import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; import com.android.internal.util.HexDump; @@ -54,22 +60,17 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Random; -import com.android.internal.R; - +import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE; import static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE; +import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED; import static android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE; import static android.telephony.SmsManager.RESULT_ERROR_NULL_PDU; import static android.telephony.SmsManager.RESULT_ERROR_RADIO_OFF; -import static android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED; -import static android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE; public abstract class SMSDispatcher extends Handler { static final String TAG = "SMS"; // accessed from inner class private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg"; - /** Default timeout for SMS sent query */ - private static final int DEFAULT_SMS_TIMEOUT = 6000; - /** Permission required to receive SMS and SMS-CB messages. */ public static final String RECEIVE_SMS_PERMISSION = "android.permission.RECEIVE_SMS"; @@ -102,23 +103,27 @@ public abstract class SMSDispatcher extends Handler { /** Retry sending a previously failed SMS message */ private static final int EVENT_SEND_RETRY = 3; - /** SMS confirm required */ - private static final int EVENT_POST_ALERT = 4; + /** Confirmation required for sending a large number of messages. */ + private static final int EVENT_SEND_LIMIT_REACHED_CONFIRMATION = 4; /** Send the user confirmed SMS */ static final int EVENT_SEND_CONFIRMED_SMS = 5; // accessed from inner class - /** Alert is timeout */ - private static final int EVENT_ALERT_TIMEOUT = 6; - - /** Stop the sending */ + /** Don't send SMS (user did not confirm). */ static final int EVENT_STOP_SENDING = 7; // accessed from inner class + /** Confirmation required for third-party apps sending to an SMS short code. */ + private static final int EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE = 8; + + /** Confirmation required for third-party apps sending to an SMS short code. */ + private static final int EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE = 9; + protected final Phone mPhone; protected final Context mContext; protected final ContentResolver mResolver; protected final CommandsInterface mCm; protected final SmsStorageMonitor mStorageMonitor; + protected final TelephonyManager mTelephonyManager; protected final WapPushOverSms mWapPush; @@ -144,7 +149,8 @@ public abstract class SMSDispatcher extends Handler { /** Outgoing message counter. Shared by all dispatchers. */ private final SmsUsageMonitor mUsageMonitor; - private final ArrayList mSTrackers = new ArrayList(MO_MSG_QUEUE_LIMIT); + /** Number of outgoing SmsTrackers waiting for user confirmation. */ + private int mPendingTrackerCount; /** Wake lock to ensure device stays awake while dispatching the SMS intent. */ private PowerManager.WakeLock mWakeLock; @@ -182,6 +188,7 @@ public abstract class SMSDispatcher extends Handler { mCm = phone.mCM; mStorageMonitor = storageMonitor; mUsageMonitor = usageMonitor; + mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); createWakelock(); @@ -279,51 +286,44 @@ public abstract class SMSDispatcher extends Handler { sendSms((SmsTracker) msg.obj); break; - case EVENT_POST_ALERT: + case EVENT_SEND_LIMIT_REACHED_CONFIRMATION: handleReachSentLimit((SmsTracker)(msg.obj)); break; - case EVENT_ALERT_TIMEOUT: - ((AlertDialog)(msg.obj)).dismiss(); - msg.obj = null; - if (mSTrackers.isEmpty() == false) { - try { - SmsTracker sTracker = mSTrackers.remove(0); - sTracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); - } catch (CanceledException ex) { - Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); - } - } - if (false) { - Log.d(TAG, "EVENT_ALERT_TIMEOUT, message stop sending"); - } + case EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE: + handleConfirmShortCode(false, (SmsTracker)(msg.obj)); + break; + + case EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE: + handleConfirmShortCode(true, (SmsTracker)(msg.obj)); break; case EVENT_SEND_CONFIRMED_SMS: - if (mSTrackers.isEmpty() == false) { - SmsTracker sTracker = mSTrackers.remove(mSTrackers.size() - 1); - if (sTracker.isMultipart()) { - sendMultipartSms(sTracker); - } else { - sendSms(sTracker); - } - removeMessages(EVENT_ALERT_TIMEOUT, msg.obj); + { + SmsTracker tracker = (SmsTracker) msg.obj; + if (tracker.isMultipart()) { + sendMultipartSms(tracker); + } else { + sendSms(tracker); } + mPendingTrackerCount--; break; + } case EVENT_STOP_SENDING: - if (mSTrackers.isEmpty() == false) { - // Remove the latest one. + { + SmsTracker tracker = (SmsTracker) msg.obj; + if (tracker.mSentIntent != null) { try { - SmsTracker sTracker = mSTrackers.remove(mSTrackers.size() - 1); - sTracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); + tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { - Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); + Log.e(TAG, "failed to send RESULT_ERROR_LIMIT_EXCEEDED"); } - removeMessages(EVENT_ALERT_TIMEOUT, msg.obj); } + mPendingTrackerCount--; break; } + } } private void createWakelock() { @@ -397,7 +397,7 @@ public abstract class SMSDispatcher extends Handler { int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { - handleNotInService(ss, tracker); + handleNotInService(ss, tracker.mSentIntent); } else if ((((CommandException)(ar.exception)).getCommandError() == CommandException.Error.SMS_FAIL_RETRY) && tracker.mRetryCount < MAX_SEND_RETRIES) { @@ -446,15 +446,15 @@ public abstract class SMSDispatcher extends Handler { * OUT_OF_SERVICE * EMERGENCY_ONLY * POWER_OFF - * @param tracker An SmsTracker for the current message. + * @param sentIntent the PendingIntent to send the error to */ - protected static void handleNotInService(int ss, SmsTracker tracker) { - if (tracker.mSentIntent != null) { + protected static void handleNotInService(int ss, PendingIntent sentIntent) { + if (sentIntent != null) { try { if (ss == ServiceState.STATE_POWER_OFF) { - tracker.mSentIntent.send(RESULT_ERROR_RADIO_OFF); + sentIntent.send(RESULT_ERROR_RADIO_OFF); } else { - tracker.mSentIntent.send(RESULT_ERROR_NO_SERVICE); + sentIntent.send(RESULT_ERROR_NO_SERVICE); } } catch (CanceledException ex) {} } @@ -865,9 +865,10 @@ public abstract class SMSDispatcher extends Handler { * @param deliveryIntent if not NULL this Intent is * broadcast when the message is delivered to the recipient. The * raw pdu of the status report is in the extended data ("pdu"). + * @param destAddr the destination phone number (for short code confirmation) */ protected void sendRawPdu(byte[] smsc, byte[] pdu, PendingIntent sentIntent, - PendingIntent deliveryIntent) { + PendingIntent deliveryIntent, String destAddr) { if (mSmsSendDisabled) { if (sentIntent != null) { try { @@ -891,61 +892,190 @@ public abstract class SMSDispatcher extends Handler { map.put("smsc", smsc); map.put("pdu", pdu); - SmsTracker tracker = new SmsTracker(map, sentIntent, - deliveryIntent); - int ss = mPhone.getServiceState().getState(); + // Get calling app package name via UID from Binder call + PackageManager pm = mContext.getPackageManager(); + String[] packageNames = pm.getPackagesForUid(Binder.getCallingUid()); - if (ss != ServiceState.STATE_IN_SERVICE) { - handleNotInService(ss, tracker); - } else { - String appName = getAppNameByIntent(sentIntent); - if (mUsageMonitor.check(appName, SINGLE_PART_SMS)) { - sendSms(tracker); + if (packageNames == null || packageNames.length == 0) { + // Refuse to send SMS if we can't get the calling package name. + Log.e(TAG, "Can't get calling app package name: refusing to send SMS"); + if (sentIntent != null) { + try { + sentIntent.send(RESULT_ERROR_GENERIC_FAILURE); + } catch (CanceledException ex) { + Log.e(TAG, "failed to send error result"); + } + } + return; + } + + String appPackage = packageNames[0]; + + // Strip non-digits from destination phone number before checking for short codes + // and before displaying the number to the user if confirmation is required. + SmsTracker tracker = new SmsTracker(map, sentIntent, deliveryIntent, appPackage, + PhoneNumberUtils.extractNetworkPortion(destAddr)); + + // checkDestination() returns true if the destination is not a premium short code or the + // sending app is approved to send to short codes. Otherwise, a message is sent to our + // handler with the SmsTracker to request user confirmation before sending. + if (checkDestination(tracker)) { + // check for excessive outgoing SMS usage by this app + if (!mUsageMonitor.check(appPackage, SINGLE_PART_SMS)) { + sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker)); + return; + } + + int ss = mPhone.getServiceState().getState(); + + if (ss != ServiceState.STATE_IN_SERVICE) { + handleNotInService(ss, tracker.mSentIntent); } else { - sendMessage(obtainMessage(EVENT_POST_ALERT, tracker)); + sendSms(tracker); } } } /** - * Post an alert while SMS needs user confirm. + * Check if destination is a potential premium short code and sender is not pre-approved to + * send to short codes. * - * An SmsTracker for the current message. + * @param tracker the tracker for the SMS to send + * @return true if the destination is approved; false if user confirmation event was sent */ - protected void handleReachSentLimit(SmsTracker tracker) { - if (mSTrackers.size() >= MO_MSG_QUEUE_LIMIT) { - // Deny the sending when the queue limit is reached. + boolean checkDestination(SmsTracker tracker) { + if (mUsageMonitor.isApprovedShortCodeSender(tracker.mAppPackage)) { + return true; // app is pre-approved to send to short codes + } else { + String countryIso = mTelephonyManager.getSimCountryIso(); + if (countryIso == null || countryIso.length() != 2) { + Log.e(TAG, "Can't get SIM country code: trying network country code"); + countryIso = mTelephonyManager.getNetworkCountryIso(); + } + + switch (mUsageMonitor.checkDestination(tracker.mDestAddress, countryIso)) { + case SmsUsageMonitor.CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE: + sendMessage(obtainMessage(EVENT_CONFIRM_SEND_TO_POSSIBLE_PREMIUM_SHORT_CODE, + tracker)); + return false; // wait for user confirmation before sending + + case SmsUsageMonitor.CATEGORY_PREMIUM_SHORT_CODE: + sendMessage(obtainMessage(EVENT_CONFIRM_SEND_TO_PREMIUM_SHORT_CODE, + tracker)); + return false; // wait for user confirmation before sending + + case SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE: + case SmsUsageMonitor.CATEGORY_FREE_SHORT_CODE: + case SmsUsageMonitor.CATEGORY_STANDARD_SHORT_CODE: + default: + return true; // destination is not a premium short code + } + } + } + + /** + * Deny sending an SMS if the outgoing queue limit is reached. Used when the message + * must be confirmed by the user due to excessive usage or potential premium SMS detected. + * @param tracker the SmsTracker for the message to send + * @return true if the message was denied; false to continue with send confirmation + */ + private boolean denyIfQueueLimitReached(SmsTracker tracker) { + if (mPendingTrackerCount >= MO_MSG_QUEUE_LIMIT) { + // Deny sending message when the queue limit is reached. try { tracker.mSentIntent.send(RESULT_ERROR_LIMIT_EXCEEDED); } catch (CanceledException ex) { Log.e(TAG, "failed to send back RESULT_ERROR_LIMIT_EXCEEDED"); } - return; + return true; + } + mPendingTrackerCount++; + return false; + } + + /** + * Returns the label for the specified app package name. + * @param appPackage the package name of the app requesting to send an SMS + * @return the label for the specified app, or the package name if getApplicationInfo() fails + */ + private CharSequence getAppLabel(String appPackage) { + PackageManager pm = mContext.getPackageManager(); + try { + ApplicationInfo appInfo = pm.getApplicationInfo(appPackage, 0); + return appInfo.loadLabel(pm); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "PackageManager Name Not Found for package " + appPackage); + return appPackage; // fall back to package name if we can't get app label + } + } + + /** + * Post an alert when SMS needs confirmation due to excessive usage. + * @param tracker an SmsTracker for the current message. + */ + protected void handleReachSentLimit(SmsTracker tracker) { + if (denyIfQueueLimitReached(tracker)) { + return; // queue limit reached; error was returned to caller } + CharSequence appLabel = getAppLabel(tracker.mAppPackage); Resources r = Resources.getSystem(); + Spanned messageText = Html.fromHtml(r.getString(R.string.sms_control_message, appLabel)); - String appName = getAppNameByIntent(tracker.mSentIntent); + ConfirmDialogListener listener = new ConfirmDialogListener(tracker); AlertDialog d = new AlertDialog.Builder(mContext) - .setTitle(r.getString(R.string.sms_control_title)) - .setMessage(appName + " " + r.getString(R.string.sms_control_message)) - .setPositiveButton(r.getString(R.string.sms_control_yes), mListener) - .setNegativeButton(r.getString(R.string.sms_control_no), mListener) + .setTitle(R.string.sms_control_title) + .setIcon(R.drawable.stat_sys_warning) + .setMessage(messageText) + .setPositiveButton(r.getString(R.string.sms_control_yes), listener) + .setNegativeButton(r.getString(R.string.sms_control_no), listener) + .setOnCancelListener(listener) .create(); d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); d.show(); - - mSTrackers.add(tracker); - sendMessageDelayed ( obtainMessage(EVENT_ALERT_TIMEOUT, d), - DEFAULT_SMS_TIMEOUT); } - protected static String getAppNameByIntent(PendingIntent intent) { + /** + * Post an alert for user confirmation when sending to a potential short code. + * @param isPremium true if the destination is known to be a premium short code + * @param tracker the SmsTracker for the current message. + */ + protected void handleConfirmShortCode(boolean isPremium, SmsTracker tracker) { + if (denyIfQueueLimitReached(tracker)) { + return; // queue limit reached; error was returned to caller + } + + int messageId; + int titleId; + if (isPremium) { + messageId = R.string.sms_premium_short_code_confirm_message; + titleId = R.string.sms_premium_short_code_confirm_title; + } else { + messageId = R.string.sms_short_code_confirm_message; + titleId = R.string.sms_short_code_confirm_title; + } + + CharSequence appLabel = getAppLabel(tracker.mAppPackage); Resources r = Resources.getSystem(); - return (intent != null) ? intent.getTargetPackage() - : r.getString(R.string.sms_control_default_app_name); + Spanned messageText = Html.fromHtml(r.getString(messageId, appLabel, tracker.mDestAddress)); + + ConfirmDialogListener listener = new ConfirmDialogListener(tracker); + + AlertDialog d = new AlertDialog.Builder(mContext) + .setTitle(titleId) + .setIcon(R.drawable.stat_sys_warning) + .setMessage(messageText) + .setPositiveButton(r.getString(R.string.sms_short_code_confirm_allow), listener) + .setNegativeButton(r.getString(R.string.sms_short_code_confirm_deny), listener) +// TODO: add third button for "Report malicious app" feature +// .setNeutralButton(r.getString(R.string.sms_short_code_confirm_report), listener) + .setOnCancelListener(listener) + .create(); + + d.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); + d.show(); } /** @@ -982,7 +1112,7 @@ public abstract class SMSDispatcher extends Handler { if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } - handleNotInService(ss, new SmsTracker(null, sentIntent, null)); + handleNotInService(ss, sentIntent); } return; } @@ -1032,12 +1162,17 @@ public abstract class SMSDispatcher extends Handler { public final PendingIntent mSentIntent; public final PendingIntent mDeliveryIntent; + public final String mAppPackage; + public final String mDestAddress; + public SmsTracker(HashMap data, PendingIntent sentIntent, - PendingIntent deliveryIntent) { + PendingIntent deliveryIntent, String appPackage, String destAddr) { mData = data; mSentIntent = sentIntent; mDeliveryIntent = deliveryIntent; mRetryCount = 0; + mAppPackage = appPackage; + mDestAddress = destAddr; } /** @@ -1050,19 +1185,35 @@ public abstract class SMSDispatcher extends Handler { } } - private final DialogInterface.OnClickListener mListener = - new DialogInterface.OnClickListener() { + /** + * Dialog listener for SMS confirmation dialog. + */ + private final class ConfirmDialogListener + implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener { - public void onClick(DialogInterface dialog, int which) { - if (which == DialogInterface.BUTTON_POSITIVE) { - Log.d(TAG, "click YES to send out sms"); - sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS)); - } else if (which == DialogInterface.BUTTON_NEGATIVE) { - Log.d(TAG, "click NO to stop sending"); - sendMessage(obtainMessage(EVENT_STOP_SENDING)); - } + private final SmsTracker mTracker; + + ConfirmDialogListener(SmsTracker tracker) { + mTracker = tracker; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which == DialogInterface.BUTTON_POSITIVE) { + Log.d(TAG, "CONFIRM sending SMS"); + sendMessage(obtainMessage(EVENT_SEND_CONFIRMED_SMS, mTracker)); + } else if (which == DialogInterface.BUTTON_NEGATIVE) { + Log.d(TAG, "DENY sending SMS"); + sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); } - }; + } + + @Override + public void onCancel(DialogInterface dialog) { + Log.d(TAG, "dialog dismissed: don't send SMS"); + sendMessage(obtainMessage(EVENT_STOP_SENDING, mTracker)); + } + } private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() { @Override diff --git a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java index bd2ae8b423e42..07a0a281ee8af 100644 --- a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java +++ b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java @@ -17,13 +17,23 @@ package com.android.internal.telephony; import android.content.ContentResolver; +import android.content.Context; +import android.content.res.XmlResourceParser; import android.provider.Settings; +import android.telephony.PhoneNumberUtils; import android.util.Log; +import com.android.internal.util.XmlUtils; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; +import java.util.regex.Pattern; /** * Implement the per-application based SMS control, which limits the number of @@ -34,13 +44,28 @@ import java.util.Map; * dual-mode devices that require support for both 3GPP and 3GPP2 format messages. */ public class SmsUsageMonitor { - private static final String TAG = "SmsStorageMonitor"; + private static final String TAG = "SmsUsageMonitor"; /** Default checking period for SMS sent without user permission. */ - private static final int DEFAULT_SMS_CHECK_PERIOD = 3600000; + private static final int DEFAULT_SMS_CHECK_PERIOD = 1800000; // 30 minutes /** Default number of SMS sent in checking period without user permission. */ - private static final int DEFAULT_SMS_MAX_COUNT = 100; + private static final int DEFAULT_SMS_MAX_COUNT = 30; + + /** Return value from {@link #checkDestination} for regular phone numbers. */ + static final int CATEGORY_NOT_SHORT_CODE = 0; + + /** Return value from {@link #checkDestination} for free (no cost) short codes. */ + static final int CATEGORY_FREE_SHORT_CODE = 1; + + /** Return value from {@link #checkDestination} for standard rate (non-premium) short codes. */ + static final int CATEGORY_STANDARD_SHORT_CODE = 2; + + /** Return value from {@link #checkDestination} for possible premium short codes. */ + static final int CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE = 3; + + /** Return value from {@link #checkDestination} for premium short codes. */ + static final int CATEGORY_PREMIUM_SHORT_CODE = 4; private final int mCheckPeriod; private final int mMaxAllowed; @@ -48,10 +73,89 @@ public class SmsUsageMonitor { new HashMap>(); /** - * Create SMS usage monitor. - * @param resolver the ContentResolver to use to load from secure settings + * Hash of package names that are allowed to send to short codes. + * TODO: persist this across reboots. */ - public SmsUsageMonitor(ContentResolver resolver) { + private final HashSet mApprovedShortCodeSenders = new HashSet(); + + /** Context for retrieving regexes from XML resource. */ + private final Context mContext; + + /** Country code for the cached short code pattern matcher. */ + private String mCurrentCountry; + + /** Cached short code pattern matcher for {@link #mCurrentCountry}. */ + private ShortCodePatternMatcher mCurrentPatternMatcher; + + /** XML tag for root element. */ + private static final String TAG_SHORTCODES = "shortcodes"; + + /** XML tag for short code patterns for a specific country. */ + private static final String TAG_SHORTCODE = "shortcode"; + + /** XML attribute for the country code. */ + private static final String ATTR_COUNTRY = "country"; + + /** XML attribute for the short code regex pattern. */ + private static final String ATTR_PATTERN = "pattern"; + + /** XML attribute for the premium short code regex pattern. */ + private static final String ATTR_PREMIUM = "premium"; + + /** XML attribute for the free short code regex pattern. */ + private static final String ATTR_FREE = "free"; + + /** XML attribute for the standard rate short code regex pattern. */ + private static final String ATTR_STANDARD = "standard"; + + /** + * SMS short code regex pattern matcher for a specific country. + */ + private static final class ShortCodePatternMatcher { + private final Pattern mShortCodePattern; + private final Pattern mPremiumShortCodePattern; + private final Pattern mFreeShortCodePattern; + private final Pattern mStandardShortCodePattern; + + ShortCodePatternMatcher(String shortCodeRegex, String premiumShortCodeRegex, + String freeShortCodeRegex, String standardShortCodeRegex) { + mShortCodePattern = (shortCodeRegex != null ? Pattern.compile(shortCodeRegex) : null); + mPremiumShortCodePattern = (premiumShortCodeRegex != null ? + Pattern.compile(premiumShortCodeRegex) : null); + mFreeShortCodePattern = (freeShortCodeRegex != null ? + Pattern.compile(freeShortCodeRegex) : null); + mStandardShortCodePattern = (standardShortCodeRegex != null ? + Pattern.compile(standardShortCodeRegex) : null); + } + + int getNumberCategory(String phoneNumber) { + if (mFreeShortCodePattern != null && mFreeShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_FREE_SHORT_CODE; + } + if (mStandardShortCodePattern != null && mStandardShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_STANDARD_SHORT_CODE; + } + if (mPremiumShortCodePattern != null && mPremiumShortCodePattern.matcher(phoneNumber) + .matches()) { + return CATEGORY_PREMIUM_SHORT_CODE; + } + if (mShortCodePattern != null && mShortCodePattern.matcher(phoneNumber).matches()) { + return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; + } + return CATEGORY_NOT_SHORT_CODE; + } + } + + /** + * Create SMS usage monitor. + * @param context the context to use to load resources and get TelephonyManager service + */ + public SmsUsageMonitor(Context context) { + mContext = context; + ContentResolver resolver = context.getContentResolver(); + mMaxAllowed = Settings.Secure.getInt(resolver, Settings.Secure.SMS_OUTGOING_CHECK_MAX_COUNT, DEFAULT_SMS_MAX_COUNT); @@ -59,6 +163,50 @@ public class SmsUsageMonitor { mCheckPeriod = Settings.Secure.getInt(resolver, Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS, DEFAULT_SMS_CHECK_PERIOD); + + // system MMS app is always allowed to send to short codes + mApprovedShortCodeSenders.add("com.android.mms"); + } + + /** + * Return a pattern matcher object for the specified country. + * @param country the country to search for + * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found + */ + private ShortCodePatternMatcher getPatternMatcher(String country) { + int id = com.android.internal.R.xml.sms_short_codes; + XmlResourceParser parser = mContext.getResources().getXml(id); + + try { + XmlUtils.beginDocument(parser, TAG_SHORTCODES); + + while (true) { + XmlUtils.nextElement(parser); + + String element = parser.getName(); + if (element == null) break; + + if (element.equals(TAG_SHORTCODE)) { + String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY); + if (country.equals(currentCountry)) { + String pattern = parser.getAttributeValue(null, ATTR_PATTERN); + String premium = parser.getAttributeValue(null, ATTR_PREMIUM); + String free = parser.getAttributeValue(null, ATTR_FREE); + String standard = parser.getAttributeValue(null, ATTR_STANDARD); + return new ShortCodePatternMatcher(pattern, premium, free, standard); + } + } else { + Log.e(TAG, "Error: skipping unknown XML tag " + element); + } + } + } catch (XmlPullParserException e) { + Log.e(TAG, "XML parser exception reading short code pattern resource", e); + } catch (IOException e) { + Log.e(TAG, "I/O exception reading short code pattern resource", e); + } finally { + parser.close(); + } + return null; // country not found } /** Clear the SMS application list for disposal. */ @@ -67,9 +215,11 @@ public class SmsUsageMonitor { } /** - * Check to see if an application is allowed to send new SMS messages. + * Check to see if an application is allowed to send new SMS messages, and confirm with + * user if the send limit was reached or if a non-system app is potentially sending to a + * premium SMS short code or number. * - * @param appName the application sending sms + * @param appName the package name of the app requesting to send an SMS * @param smsWaiting the number of new messages desired to send * @return true if application is allowed to send the requested number * of new sms messages @@ -88,6 +238,69 @@ public class SmsUsageMonitor { } } + /** + * Return whether the app is approved to send to any short code. + * @param appName the package name of the app requesting to send an SMS + * @return true if the app is approved; false if we need to confirm short code destinations + */ + public boolean isApprovedShortCodeSender(String appName) { + return mApprovedShortCodeSenders.contains(appName); + } + + /** + * Add app package name to the list of approved short code senders. + * @param appName the package name of the app to add + */ + public void addApprovedShortCodeSender(String appName) { + Log.d(TAG, "Adding " + appName + " to list of approved short code senders."); + mApprovedShortCodeSenders.add(appName); + } + + /** + * Check if the destination is a possible premium short code. + * NOTE: the caller is expected to strip non-digits from the destination number with + * {@link PhoneNumberUtils#extractNetworkPortion} before calling this method. + * This happens in {@link SMSDispatcher#sendRawPdu} so that we use the same phone number + * for testing and in the user confirmation dialog if the user needs to confirm the number. + * This makes it difficult for malware to fool the user or the short code pattern matcher + * by using non-ASCII characters to make the number appear to be different from the real + * destination phone number. + * + * @param destAddress the destination address to test for possible short code + * @return {@link #CATEGORY_NOT_SHORT_CODE}, {@link #CATEGORY_FREE_SHORT_CODE}, + * {@link #CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE}, or {@link #CATEGORY_PREMIUM_SHORT_CODE}. + */ + public int checkDestination(String destAddress, String countryIso) { + // always allow emergency numbers + if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) { + return CATEGORY_NOT_SHORT_CODE; + } + + ShortCodePatternMatcher patternMatcher = null; + + if (countryIso != null) { + if (countryIso.equals(mCurrentCountry)) { + patternMatcher = mCurrentPatternMatcher; + } else { + patternMatcher = getPatternMatcher(countryIso); + mCurrentCountry = countryIso; + mCurrentPatternMatcher = patternMatcher; // may be null if not found + } + } + + if (patternMatcher != null) { + return patternMatcher.getNumberCategory(destAddress); + } else { + // Generic rule: numbers of 5 digits or less are considered potential short codes + Log.e(TAG, "No patterns for \"" + countryIso + "\": using generic short code rule"); + if (destAddress.length() <= 5) { + return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; + } else { + return CATEGORY_NOT_SHORT_CODE; + } + } + } + /** * Remove keys containing only old timestamps. This can happen if an SMS app is used * to send messages and then uninstalled. diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java index bfa23e7be8f3a..e6b45f6940e83 100755 --- a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java @@ -283,7 +283,7 @@ final class CdmaSMSDispatcher extends SMSDispatcher { byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( scAddr, destAddr, destPort, data, (deliveryIntent != null)); - sendSubmitPdu(pdu, sentIntent, deliveryIntent); + sendSubmitPdu(pdu, sentIntent, deliveryIntent, destAddr); } /** {@inheritDoc} */ @@ -292,7 +292,7 @@ final class CdmaSMSDispatcher extends SMSDispatcher { PendingIntent sentIntent, PendingIntent deliveryIntent) { SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( scAddr, destAddr, text, (deliveryIntent != null), null); - sendSubmitPdu(pdu, sentIntent, deliveryIntent); + sendSubmitPdu(pdu, sentIntent, deliveryIntent, destAddr); } /** {@inheritDoc} */ @@ -324,11 +324,11 @@ final class CdmaSMSDispatcher extends SMSDispatcher { SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destinationAddress, uData, (deliveryIntent != null) && lastPart); - sendSubmitPdu(submitPdu, sentIntent, deliveryIntent); + sendSubmitPdu(submitPdu, sentIntent, deliveryIntent, destinationAddress); } protected void sendSubmitPdu(SmsMessage.SubmitPdu pdu, - PendingIntent sentIntent, PendingIntent deliveryIntent) { + PendingIntent sentIntent, PendingIntent deliveryIntent, String destAddr) { if (SystemProperties.getBoolean(TelephonyProperties.PROPERTY_INECM_MODE, false)) { if (sentIntent != null) { try { @@ -340,7 +340,7 @@ final class CdmaSMSDispatcher extends SMSDispatcher { } return; } - sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, destAddr); } /** {@inheritDoc} */ diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java index 931c662d27e96..bfa2bb1d4b400 100644 --- a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java @@ -241,7 +241,8 @@ public final class GsmSMSDispatcher extends SMSDispatcher { SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( scAddr, destAddr, destPort, data, (deliveryIntent != null)); if (pdu != null) { - sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destAddr); } else { Log.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null"); } @@ -254,7 +255,8 @@ public final class GsmSMSDispatcher extends SMSDispatcher { SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( scAddr, destAddr, text, (deliveryIntent != null)); if (pdu != null) { - sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destAddr); } else { Log.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null"); } @@ -276,7 +278,8 @@ public final class GsmSMSDispatcher extends SMSDispatcher { message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader), encoding, smsHeader.languageTable, smsHeader.languageShiftTable); if (pdu != null) { - sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); + sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, + destinationAddress); } else { Log.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null"); } diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsUsageMonitorShortCodeTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsUsageMonitorShortCodeTest.java new file mode 100644 index 0000000000000..3bb7c060e9e37 --- /dev/null +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/SmsUsageMonitorShortCodeTest.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2012 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.internal.telephony; + +import android.test.AndroidTestCase; +import android.test.suitebuilder.annotation.SmallTest; + +import static com.android.internal.telephony.SmsUsageMonitor.CATEGORY_FREE_SHORT_CODE; +import static com.android.internal.telephony.SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE; +import static com.android.internal.telephony.SmsUsageMonitor.CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; +import static com.android.internal.telephony.SmsUsageMonitor.CATEGORY_PREMIUM_SHORT_CODE; +import static com.android.internal.telephony.SmsUsageMonitor.CATEGORY_STANDARD_SHORT_CODE; + +/** + * Test cases for SMS short code pattern matching in SmsUsageMonitor. + */ +public class SmsUsageMonitorShortCodeTest extends AndroidTestCase { + + private static final class ShortCodeTest { + final String countryIso; + final String address; + final int category; + + ShortCodeTest(String countryIso, String destAddress, int category) { + this.countryIso = countryIso; + this.address = destAddress; + this.category = category; + } + } + + /** + * List of short code test cases. + */ + private static final ShortCodeTest[] sShortCodeTests = new ShortCodeTest[] { + new ShortCodeTest("al", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("al", "4321", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("al", "54321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("al", "15191", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("al", "55500", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("al", "55600", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("al", "654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("am", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("am", "101", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("am", "102", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("am", "103", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("am", "222", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "1111", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "9999", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "1121", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "1141", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "1161", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("am", "3024", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("at", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("at", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("at", "0901234", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("at", "0900666266", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("au", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("au", "180000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("au", "190000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("au", "1900000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("au", "19000000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("au", "19998882", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("az", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("az", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "12345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "87744", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "3301", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "3302", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "9012", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "9014", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "9394", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "87744", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "93101", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("az", "123456", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("be", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("be", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("be", "567890", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("be", "8000", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("be", "6566", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("be", "7777", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("bg", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("bg", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("bg", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "12345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "1816", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "1915", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "1916", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "1935", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("bg", "18423", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("by", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("by", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("by", "3336", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("by", "5013", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("by", "5014", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("by", "7781", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("ca", "911", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ca", "+18005551234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ca", "8005551234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ca", "20000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ca", "200000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ca", "2000000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ca", "60999", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ca", "88188", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("ch", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ch", "123", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ch", "234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ch", "3456", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ch", "98765", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ch", "543", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ch", "83111", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ch", "234567", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ch", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("cn", "120", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("cn", "1062503000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("cn", "1065123456", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("cn", "1066335588", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("cy", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("cy", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("cy", "4321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("cy", "54321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("cy", "654321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("cy", "7510", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("cy", "987654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("cz", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("cz", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("cz", "9090150", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("cz", "90901599", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("cz", "987654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("de", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("de", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("de", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "12345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "8888", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "11111", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "11886", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "22022", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "23300", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "3434", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "34567", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "41414", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "55655", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "66766", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "66777", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "77677", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "80888", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "1232286", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("de", "987654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("dk", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("dk", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("dk", "1259", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("dk", "16123", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("dk", "987654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ee", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ee", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("ee", "123", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "1259", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "15330", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "17999", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "17010", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "17013", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "9034567", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ee", "34567890", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("es", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("es", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("es", "25165", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("es", "27333", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("es", "995399", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("es", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("fi", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("fi", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("fi", "12345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "123456", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "17159", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "17163", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "0600123", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "070012345", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fi", "987654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("fr", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("fr", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("fr", "34567", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("fr", "45678", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fr", "81185", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("fr", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("gb", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("gb", "999", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("gb", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("gb", "4567", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "45678", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "56789", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "79067", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "80079", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "654321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("gb", "7654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ge", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ge", "8765", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ge", "2345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ge", "8012", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ge", "8013", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ge", "8014", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ge", "8889", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("gr", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("gr", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("gr", "54321", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("gr", "19567", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("gr", "19678", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("gr", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("hu", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("hu", "012", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "0123", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "1784", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "2345", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "01234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "012345678", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "0123456789", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "1234567890", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "0691227910", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("hu", "2345678901", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("hu", "01234567890", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ie", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ie", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("ie", "50123", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("ie", "51234", CATEGORY_STANDARD_SHORT_CODE), + new ShortCodeTest("ie", "52345", CATEGORY_STANDARD_SHORT_CODE), + new ShortCodeTest("ie", "57890", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ie", "67890", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ie", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("il", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("il", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("il", "4422", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("il", "4545", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("il", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("it", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("it", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("it", "4567", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("it", "48000", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("it", "45678", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("it", "56789", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("it", "456789", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("kg", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("kg", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("kg", "4152", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("kg", "4157", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("kg", "4449", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("kg", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("kz", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("kz", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("kz", "9194", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("kz", "7790", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("kz", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("lt", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("lt", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("lt", "123", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "1381", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "1394", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "1645", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "12345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("lt", "123456", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("lu", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("lu", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("lu", "1234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("lu", "12345", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("lu", "64747", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lu", "678901", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("lv", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("lv", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("lv", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("lv", "1819", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lv", "1863", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lv", "1874", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("lv", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("mx", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("mx", "2345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("mx", "7766", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("mx", "23456", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("mx", "53035", CATEGORY_PREMIUM_SHORT_CODE), + + new ShortCodeTest("my", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("my", "1234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("my", "23456", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("my", "32298", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("my", "33776", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("my", "345678", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("nl", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("nl", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("nl", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("nl", "4466", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("nl", "5040", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("nl", "23456", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("no", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("no", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("no", "2201", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("no", "2226", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("no", "2227", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("no", "23456", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("no", "234567", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("nz", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("nz", "123", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("nz", "2345", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("nz", "3903", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("nz", "8995", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("nz", "23456", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("pl", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("pl", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("pl", "7890", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "34567", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "7910", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "74240", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "79866", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "92525", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pl", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("pt", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("pt", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("pt", "61000", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pt", "62345", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pt", "68304", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pt", "69876", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("pt", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ro", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ro", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("ro", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "1263", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "1288", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "1314", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "1380", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "7890", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ro", "12345", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ru", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ru", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ru", "1161", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ru", "2097", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ru", "3933", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ru", "7781", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ru", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("se", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("se", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("se", "1234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("se", "72345", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("se", "72999", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("se", "123456", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("se", "87654321", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("sg", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("sg", "1234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("sg", "70000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("sg", "79999", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("sg", "73800", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("sg", "74688", CATEGORY_STANDARD_SHORT_CODE), + new ShortCodeTest("sg", "987654", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("si", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("si", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("si", "1234", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("si", "3838", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("si", "72999", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("sk", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("sk", "116117", CATEGORY_FREE_SHORT_CODE), + new ShortCodeTest("sk", "1234", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("sk", "6674", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("sk", "7604", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("sk", "72999", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("tj", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("tj", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("tj", "1161", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("tj", "1171", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("tj", "4161", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("tj", "4449", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("tj", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("ua", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("ua", "5432", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("ua", "4448", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ua", "7094", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ua", "7540", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("ua", "98765", CATEGORY_NOT_SHORT_CODE), + + new ShortCodeTest("us", "911", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("us", "+18005551234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("us", "8005551234", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("us", "20000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("us", "200000", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("us", "2000000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("us", "20433", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("us", "21472", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("us", "23333", CATEGORY_PREMIUM_SHORT_CODE), + new ShortCodeTest("us", "99807", CATEGORY_PREMIUM_SHORT_CODE), + + // generic rules for other countries: 5 digits or less considered potential short code + new ShortCodeTest("zz", "2000000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest("zz", "54321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("zz", "4321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("zz", "321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest("zz", "112", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest(null, "2000000", CATEGORY_NOT_SHORT_CODE), + new ShortCodeTest(null, "54321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest(null, "4321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest(null, "321", CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE), + new ShortCodeTest(null, "112", CATEGORY_NOT_SHORT_CODE), + }; + + @SmallTest + public void testSmsUsageMonitor() { + SmsUsageMonitor monitor = new SmsUsageMonitor(getContext()); + for (ShortCodeTest test : sShortCodeTests) { + assertEquals("country: " + test.countryIso + " number: " + test.address, + test.category, monitor.checkDestination(test.address, test.countryIso)); + } + } +}