OSDN Git Service

Add user confirmation dialog before sending SMS to short code.
authorJake Hamby <jhamby@google.com>
Mon, 2 Jan 2012 23:34:39 +0000 (15:34 -0800)
committerAndroid (Google) Code Review <android-gerrit@google.com>
Fri, 13 Apr 2012 18:50:10 +0000 (11:50 -0700)
Non-system apps now require user confirmation before sending an SMS
to a short code that may potentially cost the user money. The number
is tested against regex patterns for short codes for the country
matching the user's SIM card or network. The user is warned if the
phone number is potentially or definitely a premium SMS number.

The regex patterns are loaded from core/res/res/xml/sms_short_codes.xml.
If the user's country is not found, then phone numbers of 5 digits or less
(excluding known emergency phone numbers) are considered to be potential
SMS short codes.

Command to run test cases:
$ runtest -c com.android.internal.telephony.SmsUsageMonitorShortCodeTest frameworks-telephony

Bug: 5513975
Change-Id: Ic0b483153390e974c632302f3061300bc2a2274a

core/res/res/values/public.xml
core/res/res/values/strings.xml
core/res/res/xml/sms_short_codes.xml [new file with mode: 0644]
telephony/java/com/android/internal/telephony/PhoneBase.java
telephony/java/com/android/internal/telephony/SMSDispatcher.java
telephony/java/com/android/internal/telephony/SmsUsageMonitor.java
telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
telephony/tests/telephonytests/src/com/android/internal/telephony/SmsUsageMonitorShortCodeTest.java [new file with mode: 0644]

index d7def44..2006548 100644 (file)
   <java-symbol type="string" name="sipAddressTypeHome" />
   <java-symbol type="string" name="sipAddressTypeOther" />
   <java-symbol type="string" name="sipAddressTypeWork" />
-  <java-symbol type="string" name="sms_control_default_app_name" />
   <java-symbol type="string" name="sms_control_message" />
-  <java-symbol type="string" name="sms_control_no" />
   <java-symbol type="string" name="sms_control_title" />
+  <java-symbol type="string" name="sms_control_no" />
   <java-symbol type="string" name="sms_control_yes" />
+  <java-symbol type="string" name="sms_premium_short_code_confirm_message" />
+  <java-symbol type="string" name="sms_premium_short_code_confirm_title" />
+  <java-symbol type="string" name="sms_short_code_confirm_allow" />
+  <java-symbol type="string" name="sms_short_code_confirm_deny" />
+  <java-symbol type="string" name="sms_short_code_confirm_message" />
+  <java-symbol type="string" name="sms_short_code_confirm_report" />
+  <java-symbol type="string" name="sms_short_code_confirm_title" />
   <java-symbol type="string" name="submit" />
   <java-symbol type="string" name="sync_binding_label" />
   <java-symbol type="string" name="sync_do_nothing" />
   <java-symbol type="xml" name="password_kbd_symbols_shift" />
   <java-symbol type="xml" name="power_profile" />
   <java-symbol type="xml" name="time_zones_by_country" />
+  <java-symbol type="xml" name="sms_short_codes" />
 
   <java-symbol type="raw" name="incognito_mode_start_page" />
   <java-symbol type="raw" name="loaderror" />
index 968b51c..e00986c 100755 (executable)
     <string name="select_character">Insert character</string>
 
     <!-- SMS per-application rate control Dialog --> <skip />
-    <!-- See SMS_DIALOG.  This is shown if the current application's name cannot be figuerd out. -->
-    <string name="sms_control_default_app_name">Unknown app</string>
     <!-- SMS_DIALOG: An SMS dialog is shown if an application tries to send too many SMSes.  This is the title of that dialog. -->
     <string name="sms_control_title">Sending SMS messages</string>
-    <!-- See SMS_DIALOG.  This is the message shown in that dialog. -->
-    <string name="sms_control_message">A large number of SMS messages are being sent. Touch OK to continue, or Cancel to stop sending.</string>
-    <!-- See SMS_DIALOG.  This is a button choice to allow sending the SMSes. -->
-    <string name="sms_control_yes">OK</string>
-    <!-- See SMS_DIALOG.  This is a button choice to disallow sending the SMSes.. -->
-    <string name="sms_control_no">Cancel</string>
+    <!-- See SMS_DIALOG.  This is the message shown in that dialog. [CHAR LIMIT=NONE] -->
+    <string name="sms_control_message">&lt;b><xliff:g id="app_name">%1$s</xliff:g>&lt;/b> is sending a large number of SMS messages. Do you want to allow this app to continue sending messages?</string>
+    <!-- See SMS_DIALOG.  This is a button choice to allow sending the SMSes. [CHAR LIMIT=30] -->
+    <string name="sms_control_yes">Allow</string>
+    <!-- See SMS_DIALOG.  This is a button choice to disallow sending the SMSes. [CHAR LIMIT=30] -->
+    <string name="sms_control_no">Deny</string>
+
+    <!-- SMS short code verification dialog. --> <skip />
+    <!-- The dialog title for the SMS short code confirmation dialog. [CHAR LIMIT=30] -->
+    <string name="sms_short_code_confirm_title">Send SMS to short code?</string>
+    <!-- The dialog title for the SMS premium short code confirmation dialog. [CHAR LIMIT=30] -->
+    <string name="sms_premium_short_code_confirm_title">Send premium SMS?</string>
+    <!-- The message text for the SMS short code confirmation dialog. [CHAR LIMIT=NONE] -->
+    <string name="sms_short_code_confirm_message">&lt;b><xliff:g id="app_name">%1$s</xliff:g>&lt;/b> would like to send a text message to &lt;b><xliff:g id="dest_address">%2$s</xliff:g>&lt;/b>, which appears to be an SMS short code.&lt;p>Sending text messages to some short codes may cause your mobile account to be billed for premium services.&lt;p>Do you want to allow this app to send the message?</string>
+    <!-- The message text for the SMS short code confirmation dialog. [CHAR LIMIT=NONE] -->
+    <string name="sms_premium_short_code_confirm_message">&lt;b><xliff:g id="app_name">%1$s</xliff:g>&lt;/b> would like to send a text message to &lt;b><xliff:g id="dest_address">%2$s</xliff:g>&lt;/b>, which is a premium SMS short code.&lt;p>&lt;b>Sending a message to this destination will cause your mobile account to be billed for premium services.&lt;/b>&lt;p>Do you want to allow this app to send the message?</string>
+    <!-- Text of the approval button for the SMS short code confirmation dialog. [CHAR LIMIT=50] -->
+    <string name="sms_short_code_confirm_allow">Send message</string>
+    <!-- Text of the cancel button for the SMS short code confirmation dialog. [CHAR LIMIT=30] -->
+    <string name="sms_short_code_confirm_deny">Don\'t send</string>
+    <!-- Text of the button for the SMS short code confirmation dialog to report a malicious app. [CHAR LIMIT=30] -->
+    <string name="sms_short_code_confirm_report">Report malicious app</string>
 
     <!-- SIM swap and device reboot Dialog --> <skip />
     <!-- See SIM_REMOVED_DIALOG.  This is the title of that dialog. -->
diff --git a/core/res/res/xml/sms_short_codes.xml b/core/res/res/xml/sms_short_codes.xml
new file mode 100644 (file)
index 0000000..8b395af
--- /dev/null
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 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.
+*/
+-->
+
+<!-- Regex patterns for SMS short codes by country. -->
+<shortcodes>
+
+    <!-- The country attribute is the ISO country code of the user's account (from SIM card or NV).
+         The pattern attribute is a regex that matches all SMS short codes for the country.
+         The premium attribute is a regex that matches premium rate SMS short codes.
+         The free attribute matches short codes that we know will not cost the user, such as
+         emergency numbers. The standard attribute matches short codes that are billed at the
+         standard SMS rate. The user is warned when the destination phone number matches the
+         "pattern" or "premium" regexes, and does not match the "free" or "standard" regexes. -->
+
+    <!-- Harmonised European Short Codes are 6 digit numbers starting with 116 (free helplines).
+         Premium patterns include short codes from: http://aonebill.com/coverage&tariffs
+         and http://mobilcent.com/info-worldwide.asp and extracted from:
+         http://smscoin.net/software/engine/WordPress/Paid+SMS-registration/ -->
+
+    <!-- Albania: 5 digits, known short codes listed -->
+    <shortcode country="al" pattern="\\d{5}" premium="15191|55[56]00" />
+
+    <!-- Armenia: 3-4 digits, emergency numbers 10[123] -->
+    <shortcode country="am" pattern="\\d{3,4}" premium="11[2456]1|3024" free="10[123]" />
+
+    <!-- Austria: 10 digits, premium prefix 09xx, plus EU -->
+    <shortcode country="at" pattern="11\\d{4}" premium="09.*" free="116\\d{3}" />
+
+    <!-- Australia: 6 or 8 digits starting with "19" -->
+    <shortcode country="au" pattern="19(?:\\d{4}|\\d{6})" premium="19998882" />
+
+    <!-- Azerbaijan: 4-5 digits, known premium codes listed -->
+    <shortcode country="az" pattern="\\d{4,5}" premium="330[12]|87744|901[234]|93(?:94|101)|9426|9525" />
+
+    <!-- Belgium: 4 digits, plus EU: http://www.mobileweb.be/en/mobileweb/sms-numberplan.asp -->
+    <shortcode country="be" premium="\\d{4}" free="8\\d{3}|116\\d{3}" />
+
+    <!-- Bulgaria: 4-5 digits, plus EU -->
+    <shortcode country="bg" pattern="\\d{4,5}" premium="18(?:16|423)|19(?:1[56]|35)" free="116\\d{3}" />
+
+    <!-- Belarus: 4 digits -->
+    <shortcode country="by" pattern="\\d{4}" premium="3336|4161|444[4689]|501[34]|7781" />
+
+    <!-- Canada: 5-6 digits -->
+    <shortcode country="ca" pattern="\\d{5,6}" premium="60999|88188" />
+
+    <!-- Switzerland: 3-5 digits: http://www.swisscom.ch/fxres/kmu/thirdpartybusiness_code_of_conduct_en.pdf -->
+    <shortcode country="ch" pattern="[2-9]\\d{2,4}" premium="543|83111" />
+
+    <!-- China: premium shortcodes start with "1066", free shortcodes start with "1065":
+         http://clients.txtnation.com/entries/197192-china-premium-sms-short-code-requirements -->
+    <shortcode country="cn" premium="1066.*" free="1065.*" />
+
+    <!-- Cyprus: 4-6 digits (not confirmed), known premium codes listed, plus EU -->
+    <shortcode country="cy" pattern="\\d{4,6}" premium="7510" free="116\\d{3}" />
+
+    <!-- Czech Republic: 7-8 digits, starting with 9, plus EU:
+         http://www.o2.cz/osobni/en/services-by-alphabet/91670-premium_sms.html -->
+    <shortcode country="cz" premium="9\\d{6,7}" free="116\\d{3}" />
+
+    <!-- Germany: 4-5 digits plus 1232xxx (premium codes from http://www.vodafone.de/infofaxe/537.pdf and http://premiumdienste.eplus.de/pdf/kodex.pdf), plus EU. To keep the premium regex from being too large, it only includes payment processors that have been used by SMS malware, with the regular pattern matching the other premium short codes. -->
+    <shortcode country="de" pattern="\\d{4,5}|1232\\d{3}" premium="11(?:111|833)|1232(?:013|021|060|075|286|358)|118(?:44|80|86)|20[25]00|220(?:21|22|88|99)|221(?:14|21)|223(?:44|53|77)|224[13]0|225(?:20|59|90)|226(?:06|10|20|26|30|40|56|70)|227(?:07|33|39|66|76|78|79|88|99)|228(?:08|11|66|77)|23300|30030|3[12347]000|330(?:33|55|66)|33(?:233|331|366|533)|34(?:34|567)|37000|40(?:040|123|444|[3568]00)|41(?:010|414)|44(?:000|044|344|44[24]|544)|50005|50100|50123|50555|51000|52(?:255|783)|54(?:100|2542)|55(?:077|[24]00|222|333|55|[12369]55)|56(?:789|886)|60800|6[13]000|66(?:[12348]66|566|766|777|88|999)|68888|70(?:07|123|777)|76766|77(?:007|070|222|444|[567]77)|80(?:008|123|888)|82(?:002|[378]00|323|444|472|474|488|727)|83(?:005|[169]00|333|830)|84(?:141|300|32[34]|343|488|499|777|888)|85888|86(?:188|566|640|644|650|677|868|888)|870[24]9|871(?:23|[49]9)|872(?:1[0-8]|49|99)|87499|875(?:49|55|99)|876(?:0[1367]|1[1245678]|54|99)|877(?:00|99)|878(?:15|25|3[567]|8[12])|87999|880(?:08|44|55|77|99)|88688|888(?:03|10|8|89)|8899|90(?:009|999)|99999" free="116\\d{3}" />
+
+    <!-- Denmark: see http://iprs.webspacecommerce.com/Denmark-Premium-Rate-Numbers -->
+    <shortcode country="dk" pattern="\\d{4,5}" premium="1\\d{3}" free="116\\d{3}" />
+
+    <!-- Estonia: short codes 3-5 digits starting with 1, plus premium 7 digit numbers starting with 90, plus EU.
+         http://www.tja.ee/public/documents/Elektrooniline_side/Oigusaktid/ENG/Estonian_Numbering_Plan_annex_06_09_2010.mht -->
+    <shortcode country="ee" pattern="1\\d{2,4}" premium="90\\d{5}|15330|1701[0-3]" free="116\\d{3}" />
+
+    <!-- Spain: 5-6 digits: 25xxx, 27xxx, 280xx, 35xxx, 37xxx, 795xxx, 797xxx, 995xxx, 997xxx, plus EU.
+         http://www.legallink.es/?q=en/content/which-current-regulatory-status-premium-rate-services-spain -->
+    <shortcode country="es" premium="[23][57]\\d{3}|280\\d{2}|[79]9[57]\\d{3}" free="116\\d{3}" />
+
+    <!-- Finland: 5-6 digits, premium 0600, 0700: http://en.wikipedia.org/wiki/Telephone_numbers_in_Finland -->
+    <shortcode country="fi" pattern="\\d{5,6}" premium="0600.*|0700.*|171(?:59|63)" free="116\\d{3}" />
+
+    <!-- France: 5 digits, free: 3xxxx, premium [4-8]xxxx, plus EU:
+         http://clients.txtnation.com/entries/161972-france-premium-sms-short-code-requirements -->
+    <shortcode country="fr" premium="[4-8]\\d{4}" free="3\\d{4}|116\\d{3}" />
+
+    <!-- United Kingdom (Great Britain): 4-6 digits, common codes [5-8]xxxx, plus EU:
+         http://www.short-codes.com/media/Co-regulatoryCodeofPracticeforcommonshortcodes170206.pdf -->
+    <shortcode country="gb" pattern="\\d{4,6}" premium="[5-8]\\d{4}" free="116\\d{3}" />
+
+    <!-- Georgia: 4 digits, known premium codes listed -->
+    <shortcode country="ge" pattern="\\d{4}" premium="801[234]|888[239]" />
+
+    <!-- Greece: 5 digits (54xxx, 19yxx, x=0-9, y=0-5): http://www.cmtelecom.com/premium-sms/greece -->
+    <shortcode country="gr" pattern="\\d{5}" premium="54\\d{3}|19[0-5]\\d{2}" free="116\\d{3}" />
+
+    <!-- Hungary: 4 or 10 digits starting with 1 or 0, plus EU:
+         http://clients.txtnation.com/entries/209633-hungary-premium-sms-short-code-regulations -->
+    <shortcode country="hu" pattern="[01](?:\\d{3}|\\d{9})" premium="0691227910|1784" free="116\\d{3}" />
+
+    <!-- Ireland: 5 digits, 5xxxx (50xxx=free, 5[12]xxx=standard), plus EU:
+         http://www.comreg.ie/_fileupload/publications/ComReg1117.pdf -->
+    <shortcode country="ie" pattern="\\d{5}" premium="5[3-9]\\d{3}" free="50\\d{3}|116\\d{3}" standard="5[12]\\d{3}" />
+
+    <!-- Israel: 4 digits, known premium codes listed -->
+    <shortcode country="il" pattern="\\d{4}" premium="4422|4545" />
+
+    <!-- Italy: 5 digits (premium=4xxxx), plus EU:
+         http://clients.txtnation.com/attachments/token/di5kfblvubttvlw/?name=Italy_CASP_EN.pdf -->
+    <shortcode country="it" pattern="\\d{5}" premium="4\\d{4}" free="116\\d{3}" />
+
+    <!-- Kyrgyzstan: 4 digits, known premium codes listed -->
+    <shortcode country="kg" pattern="\\d{4}" premium="415[2367]|444[69]" />
+
+    <!-- Kazakhstan: 4 digits, known premium codes listed: http://smscoin.net/info/pricing-kazakhstan/ -->
+    <shortcode country="kz" pattern="\\d{4}" premium="335[02]|4161|444[469]|77[2359]0|8444|919[3-5]|968[2-5]" />
+
+    <!-- Lithuania: 3-5 digits, known premium codes listed, plus EU -->
+    <shortcode country="lt" pattern="\\d{3,5}" premium="13[89]1|1394|16[34]5" free="116\\d{3}" />
+
+    <!-- Luxembourg: 5 digits, 6xxxx, plus EU:
+         http://www.luxgsm.lu/assets/files/filepage/file_1253803400.pdf -->
+    <shortcode country="lu" premium="6\\d{4}" free="116\\d{3}" />
+
+    <!-- Latvia: 4 digits, known premium codes listed, plus EU -->
+    <shortcode country="lv" pattern="\\d{4}" premium="18(?:19|63|7[1-4])" free="116\\d{3}" />
+
+    <!-- Mexico: 4-5 digits (not confirmed), known premium codes listed -->
+    <shortcode country="mx" pattern="\\d{4,5}" premium="53035|7766" />
+
+    <!-- Malaysia: 5 digits: http://www.skmm.gov.my/attachment/Consumer_Regulation/Mobile_Content_Services_FAQs.pdf -->
+    <shortcode country="my" pattern="\\d{5}" premium="32298|33776" />
+
+    <!-- The Netherlands, 4 digits, known premium codes listed, plus EU -->
+    <shortcode country="nl" pattern="\\d{4}" premium="4466|5040" free="116\\d{3}" />
+
+    <!-- Norway: 4-5 digits (not confirmed), known premium codes listed -->
+    <shortcode country="no" pattern="\\d{4,5}" premium="2201|222[67]" />
+
+    <!-- New Zealand: 3-4 digits, known premium codes listed -->
+    <shortcode country="nz" pattern="\\d{3,4}" premium="3903|8995" />
+
+    <!-- Poland: 4-5 digits (not confirmed), known premium codes listed, plus EU -->
+    <shortcode country="pl" pattern="\\d{4,5}" premium="74240|79(?:10|866)|92525" free="116\\d{3}" />
+
+    <!-- Portugal: 5 digits, plus EU:
+         http://clients.txtnation.com/entries/158326-portugal-premium-sms-short-code-regulations -->
+    <shortcode country="pt" premium="6[1289]\\d{3}" free="116\\d{3}" />
+
+    <!-- Romania: 4 digits, plus EU: http://www.simplus.ro/en/resources/glossary-of-terms/ -->
+    <shortcode country="ro" pattern="\\d{4}" premium="12(?:63|66|88)|13(?:14|80)" free="116\\d{3}" />
+
+    <!-- Russia: 4 digits, known premium codes listed: http://smscoin.net/info/pricing-russia/ -->
+    <shortcode country="ru" pattern="\\d{4}" premium="1(?:1[56]1|899)|2(?:09[57]|322|47[46]|880|990)|3[589]33|4161|44(?:4[3-9]|81)|77(?:33|81)" />
+
+    <!-- Sweden: 5 digits (72xxx), plus EU: http://www.viatel.se/en/premium-sms/ -->
+    <shortcode country="se" premium="72\\d{3}" free="116\\d{3}" />
+
+    <!-- Singapore: 5 digits: http://clients.txtnation.com/entries/306442-singapore-premium-sms-short-code-requirements
+         Free government directory info at 74688: http://app.sgdi.gov.sg/sms_help.asp -->
+    <shortcode country="sg" pattern="7\\d{4}" premium="73800" standard="74688" />
+
+    <!-- Slovenia: 4 digits (premium=3xxx, 6xxx, 8xxx), plus EU: http://www.cmtelecom.com/premium-sms/slovenia -->
+    <shortcode country="si" pattern="\\d{4}" premium="[368]\\d{3}" free="116\\d{3}" />
+
+    <!-- Slovakia: 4 digits (premium), plus EU: http://www.cmtelecom.com/premium-sms/slovakia -->
+    <shortcode country="sk" premium="\\d{4}" free="116\\d{3}" />
+
+    <!-- Tajikistan: 4 digits, known premium codes listed -->
+    <shortcode country="tj" pattern="\\d{4}" premium="11[3-7]1|4161|4333|444[689]" />
+
+    <!-- Ukraine: 4 digits, known premium codes listed -->
+    <shortcode country="ua" pattern="\\d{4}" premium="444[3-9]|70[579]4|7540" />
+
+    <!-- USA: 5-6 digits (premium codes from https://www.premiumsmsrefunds.com/ShortCodes.htm) -->
+    <shortcode country="us" pattern="\\d{5,6}" premium="20433|21(?:344|472)|22715|23(?:333|847)|24(?:15|28)0|25209|27(?:449|606|663)|28498|305(?:00|83)|32(?:340|941)|33(?:166|786|849)|34746|35(?:182|564)|37975|38(?:135|146|254)|41(?:366|463)|42335|43(?:355|500)|44(?:578|711|811)|45814|46(?:157|173|327)|46666|47553|48(?:221|277|669)|50(?:844|920)|51(?:062|368)|52944|54(?:723|892)|55928|56483|57370|59(?:182|187|252|342)|60339|61(?:266|982)|62478|64(?:219|898)|65(?:108|500)|69(?:208|388)|70877|71851|72(?:078|087|465)|73(?:288|588|882|909|997)|74(?:034|332|815)|76426|79213|81946|83177|84(?:103|685)|85797|86(?:234|236|666)|89616|90(?:715|842|938)|91(?:362|958)|94719|95297|96(?:040|666|835|969)|97(?:142|294|688)|99(?:689|796|807)" />
+
+</shortcodes>
index 1b4cb15..2ac9365 100644 (file)
@@ -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() {
index d6f96ff..28b729d 100644 (file)
 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<SmsTracker> mSTrackers = new ArrayList<SmsTracker>(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 <code>Intent</code> 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<String, Object> 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
index bd2ae8b..07a0a28 100644 (file)
 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<String, ArrayList<Long>>();
 
     /**
+     * Hash of package names that are allowed to send to short codes.
+     * TODO: persist this across reboots.
+     */
+    private final HashSet<String> mApprovedShortCodeSenders = new HashSet<String>();
+
+    /** 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 resolver the ContentResolver to use to load from secure settings
+     * @param context the context to use to load resources and get TelephonyManager service
      */
-    public SmsUsageMonitor(ContentResolver resolver) {
+    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
@@ -89,6 +239,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.
      */
index bfa23e7..e6b45f6 100755 (executable)
@@ -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} */
index 931c662..bfa2bb1 100644 (file)
@@ -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 (file)
index 0000000..3bb7c06
--- /dev/null
@@ -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));
+        }
+    }
+}