diff options
| author | Jean-Baptiste Queru <jbq@google.com> | 2012-07-02 12:30:32 -0700 |
|---|---|---|
| committer | Jean-Baptiste Queru <jbq@google.com> | 2012-07-02 12:30:32 -0700 |
| commit | a02bce1d5b3aafe359465eceaf3fd2a5b5b3918e (patch) | |
| tree | 97d6169e8b988707ff1724680b9ac7285348f99e /telephony/java/com/android/internal | |
| parent | 53149fd1eb5c7dc0d2d3a00b72c4ebf986b4e36f (diff) | |
| download | frameworks_base-a02bce1d5b3aafe359465eceaf3fd2a5b5b3918e.zip frameworks_base-a02bce1d5b3aafe359465eceaf3fd2a5b5b3918e.tar.gz frameworks_base-a02bce1d5b3aafe359465eceaf3fd2a5b5b3918e.tar.bz2 | |
Revert "DO NOT MERGE: Remove SMS shortcode warning feature."
This reverts commit b1fc08ca082db8ecad54c792485ff7915e3513ce.
Diffstat (limited to 'telephony/java/com/android/internal')
| -rw-r--r-- | telephony/java/com/android/internal/telephony/SMSDispatcher.java | 61 | ||||
| -rw-r--r-- | telephony/java/com/android/internal/telephony/SmsUsageMonitor.java | 321 |
2 files changed, 373 insertions, 9 deletions
diff --git a/telephony/java/com/android/internal/telephony/SMSDispatcher.java b/telephony/java/com/android/internal/telephony/SMSDispatcher.java index 07d733e..c5af396 100644 --- a/telephony/java/com/android/internal/telephony/SMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/SMSDispatcher.java @@ -906,18 +906,61 @@ public abstract class SMSDispatcher extends Handler { SmsTracker tracker = new SmsTracker(map, sentIntent, deliveryIntent, appPackage, PhoneNumberUtils.extractNetworkPortion(destAddr)); - // 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; - } + // 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(); + int ss = mPhone.getServiceState().getState(); - if (ss != ServiceState.STATE_IN_SERVICE) { - handleNotInService(ss, tracker.mSentIntent); + if (ss != ServiceState.STATE_IN_SERVICE) { + handleNotInService(ss, tracker.mSentIntent); + } else { + sendSms(tracker); + } + } + } + + /** + * Check if destination is a potential premium short code and sender is not pre-approved to + * send to short codes. + * + * @param tracker the tracker for the SMS to send + * @return true if the destination is approved; false if user confirmation event was sent + */ + boolean checkDestination(SmsTracker tracker) { + if (mContext.checkCallingOrSelfPermission(SEND_SMS_NO_CONFIRMATION_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + return true; // app is pre-approved to send to short codes } else { - sendSms(tracker); + 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 + } } } diff --git a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java index 4a4485d..1804d97 100644 --- a/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java +++ b/telephony/java/com/android/internal/telephony/SmsUsageMonitor.java @@ -60,17 +60,177 @@ public class SmsUsageMonitor { /** Default number of SMS sent in checking period without user permission. */ 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; private final HashMap<String, ArrayList<Long>> mSmsStamp = new HashMap<String, ArrayList<Long>>(); + /** 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; + + /** 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"; + + /** 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; + } + } + + /** + * 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 */ public SmsUsageMonitor(Context context) { + mContext = context; ContentResolver resolver = context.getContentResolver(); mMaxAllowed = Settings.Secure.getInt(resolver, @@ -80,6 +240,83 @@ public class SmsUsageMonitor { mCheckPeriod = Settings.Secure.getInt(resolver, Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS, DEFAULT_SMS_CHECK_PERIOD); + + mSettingsObserverHandler = new SettingsObserverHandler(); + } + + /** + * 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 { + return getPatternMatcher(country, parser); + } 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 + } + + /** + * 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. */ @@ -112,6 +349,90 @@ public class SmsUsageMonitor { } /** + * 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) { + synchronized (mSettingsObserverHandler) { + // always allow emergency numbers + if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) { + return CATEGORY_NOT_SHORT_CODE; + } + + ShortCodePatternMatcher patternMatcher = null; + + 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 { + // 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; + } + } + } + + /** * Remove keys containing only old timestamps. This can happen if an SMS app is used * to send messages and then uninstalled. */ |
