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));
+ }
+ }
+}