Enable SMS short code patterns to be updated from secure settings.

The premium SMS short code detector loads patterns from an XML resource
by default (frameworks/base/core/res/res/xml/sms_short_codes.xml).
Add support for updated patterns to be loaded on a per-country basis
from a secure setting named "sms_short_codes_" + the country code.
Updated patterns can be pushed to Google devices via Gservices.

Bug: 5513975
Change-Id: Ibfc0be4f12227ba45c28396ec7cdbc307442af81
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index ea3cab4..4c86d9c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4266,6 +4266,13 @@
                 "contacts_preauth_uri_expiration";
 
         /**
+         * Prefix for SMS short code regex patterns (country code is appended).
+         * @see com.android.internal.telephony.SmsUsageMonitor
+         * @hide
+         */
+        public static final String SMS_SHORT_CODES_PREFIX = "sms_short_codes_";
+
+        /**
          * This are the settings to be backed up.
          *
          * NOTE: Settings are backed up and restored in the order they appear
diff --git a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java
index 07a0a28..f40958d 100644
--- a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java
+++ b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java
@@ -19,15 +19,21 @@
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.XmlResourceParser;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.Message;
 import android.provider.Settings;
 import android.telephony.PhoneNumberUtils;
 import android.util.Log;
 
 import com.android.internal.util.XmlUtils;
 
+import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
 
 import java.io.IOException;
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -45,6 +51,8 @@
  */
 public class SmsUsageMonitor {
     private static final String TAG = "SmsUsageMonitor";
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
 
     /** Default checking period for SMS sent without user permission. */
     private static final int DEFAULT_SMS_CHECK_PERIOD = 1800000;    // 30 minutes
@@ -69,6 +77,7 @@
 
     private final int mCheckPeriod;
     private final int mMaxAllowed;
+
     private final HashMap<String, ArrayList<Long>> mSmsStamp =
             new HashMap<String, ArrayList<Long>>();
 
@@ -87,6 +96,12 @@
     /** Cached short code pattern matcher for {@link #mCurrentCountry}. */
     private ShortCodePatternMatcher mCurrentPatternMatcher;
 
+    /** Cached short code regex patterns from secure settings for {@link #mCurrentCountry}. */
+    private String mSettingsShortCodePatterns;
+
+    /** Handler for responding to content observer updates. */
+    private final SettingsObserverHandler mSettingsObserverHandler;
+
     /** XML tag for root element. */
     private static final String TAG_SHORTCODES = "shortcodes";
 
@@ -149,6 +164,74 @@
     }
 
     /**
+     * Observe the secure setting for updated regex patterns.
+     */
+    private static class SettingsObserver extends ContentObserver {
+        private final int mWhat;
+        private final Handler mHandler;
+
+        SettingsObserver(Handler handler, int what) {
+            super(handler);
+            mHandler = handler;
+            mWhat = what;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            mHandler.obtainMessage(mWhat).sendToTarget();
+        }
+    }
+
+    /**
+     * Handler to update regex patterns when secure setting for the current country is updated.
+     */
+    private class SettingsObserverHandler extends Handler {
+        /** Current content observer, or null. */
+        SettingsObserver mSettingsObserver;
+
+        /** Current country code to watch for settings updates. */
+        private String mCountryIso;
+
+        /** Request to start observing a secure setting. */
+        static final int OBSERVE_SETTING = 1;
+
+        /** Handler event for updated secure settings. */
+        static final int SECURE_SETTINGS_CHANGED = 2;
+
+        /** Send a message to this handler requesting to observe the setting for a new country. */
+        void observeSettingForCountry(String countryIso) {
+            obtainMessage(OBSERVE_SETTING, countryIso).sendToTarget();
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case OBSERVE_SETTING:
+                    if (msg.obj != null && msg.obj instanceof String) {
+                        mCountryIso = (String) msg.obj;
+                        String settingName = getSettingNameForCountry(mCountryIso);
+                        ContentResolver resolver = mContext.getContentResolver();
+
+                        if (mSettingsObserver != null) {
+                            if (VDBG) log("Unregistering old content observer");
+                            resolver.unregisterContentObserver(mSettingsObserver);
+                        }
+
+                        mSettingsObserver = new SettingsObserver(this, SECURE_SETTINGS_CHANGED);
+                        resolver.registerContentObserver(
+                                Settings.Secure.getUriFor(settingName), false, mSettingsObserver);
+                        if (VDBG) log("Registered content observer for " + settingName);
+                    }
+                    break;
+
+                case SECURE_SETTINGS_CHANGED:
+                    loadPatternsFromSettings(mCountryIso);
+                    break;
+            }
+        }
+    }
+
+    /**
      * Create SMS usage monitor.
      * @param context the context to use to load resources and get TelephonyManager service
      */
@@ -164,6 +247,8 @@
                 Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS,
                 DEFAULT_SMS_CHECK_PERIOD);
 
+        mSettingsObserverHandler = new SettingsObserverHandler();
+
         // system MMS app is always allowed to send to short codes
         mApprovedShortCodeSenders.add("com.android.mms");
     }
@@ -178,27 +263,7 @@
         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);
-                }
-            }
+            return getPatternMatcher(country, parser);
         } catch (XmlPullParserException e) {
             Log.e(TAG, "XML parser exception reading short code pattern resource", e);
         } catch (IOException e) {
@@ -209,6 +274,60 @@
         return null;    // country not found
     }
 
+    /**
+     * Return a pattern matcher object for the specified country from a secure settings string.
+     * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found
+     */
+    private static ShortCodePatternMatcher getPatternMatcher(String country, String settingsPattern) {
+        // embed pattern tag into an XML document.
+        String document = "<shortcodes>" + settingsPattern + "</shortcodes>";
+        if (VDBG) log("loading updated patterns from: " + document);
+
+        try {
+            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+            XmlPullParser parser = factory.newPullParser();
+            parser.setInput(new StringReader(document));
+            return getPatternMatcher(country, parser);
+        } catch (XmlPullParserException e) {
+            Log.e(TAG, "XML parser exception reading short code pattern from settings", e);
+        } catch (IOException e) {
+            Log.e(TAG, "I/O exception reading short code pattern from settings", e);
+        }
+        return null;    // country not found
+    }
+
+    /**
+     * Return a pattern matcher object for the specified country and pattern XML parser.
+     * @param country the country to search for
+     * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found
+     */
+    private static ShortCodePatternMatcher getPatternMatcher(String country, XmlPullParser parser)
+            throws XmlPullParserException, IOException
+    {
+        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);
+            }
+        }
+        return null;    // country not found
+    }
+
     /** Clear the SMS application list for disposal. */
     void dispose() {
         mSmsStamp.clear();
@@ -244,7 +363,9 @@
      * @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);
+        synchronized (mApprovedShortCodeSenders) {
+            return mApprovedShortCodeSenders.contains(appName);
+        }
     }
 
     /**
@@ -252,8 +373,10 @@
      * @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);
+        if (DBG) log("Adding " + appName + " to list of approved short code senders.");
+        synchronized (mApprovedShortCodeSenders) {
+            mApprovedShortCodeSenders.add(appName);
+        }
     }
 
     /**
@@ -271,32 +394,71 @@
      *  {@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;
-        }
+        synchronized (mSettingsObserverHandler) {
+            // always allow emergency numbers
+            if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) {
+                return CATEGORY_NOT_SHORT_CODE;
+            }
 
-        ShortCodePatternMatcher patternMatcher = null;
+            ShortCodePatternMatcher patternMatcher = null;
 
-        if (countryIso != null) {
-            if (countryIso.equals(mCurrentCountry)) {
-                patternMatcher = mCurrentPatternMatcher;
+            if (countryIso != null) {
+                // query secure settings and initialize content observer for updated regex patterns
+                if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry)) {
+                    loadPatternsFromSettings(countryIso);
+                    mSettingsObserverHandler.observeSettingForCountry(countryIso);
+                }
+
+                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 {
-                patternMatcher = getPatternMatcher(countryIso);
-                mCurrentCountry = countryIso;
-                mCurrentPatternMatcher = patternMatcher;    // may be null if not found
+                // 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;
+                }
             }
         }
+    }
 
-        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;
+    private static String getSettingNameForCountry(String countryIso) {
+        return Settings.Secure.SMS_SHORT_CODES_PREFIX + countryIso;
+    }
+
+    /**
+     * Load regex patterns from secure settings if present.
+     * @param countryIso the country to search for
+     */
+    void loadPatternsFromSettings(String countryIso) {
+        synchronized (mSettingsObserverHandler) {
+            if (VDBG) log("loadPatternsFromSettings(" + countryIso + ") called");
+            String settingsPatterns = Settings.Secure.getString(
+                    mContext.getContentResolver(), getSettingNameForCountry(countryIso));
+            if (settingsPatterns != null && !settingsPatterns.equals(
+                    mSettingsShortCodePatterns)) {
+                // settings pattern string has changed: update the pattern matcher
+                mSettingsShortCodePatterns = settingsPatterns;
+                ShortCodePatternMatcher matcher = getPatternMatcher(countryIso, settingsPatterns);
+                if (matcher != null) {
+                    mCurrentCountry = countryIso;
+                    mCurrentPatternMatcher = matcher;
+                }
+            } else if (settingsPatterns == null && mSettingsShortCodePatterns != null) {
+                // pattern string was removed: caller will load default patterns from XML resource
+                mCurrentCountry = null;
+                mCurrentPatternMatcher = null;
+                mSettingsShortCodePatterns = null;
             }
         }
     }
@@ -324,7 +486,7 @@
         Long ct = System.currentTimeMillis();
         long beginCheckPeriod = ct - mCheckPeriod;
 
-        Log.d(TAG, "SMS send size=" + sent.size() + " time=" + ct);
+        if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct);
 
         while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) {
             sent.remove(0);
@@ -338,4 +500,8 @@
         }
         return false;
     }
+
+    private static void log(String msg) {
+        Log.d(TAG, msg);
+    }
 }