SmsUsageMonitor.java revision c38bb60d867c5d61d90b7179a9ed2b2d1848124f
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.internal.telephony; 18 19import android.content.ContentResolver; 20import android.content.Context; 21import android.content.res.XmlResourceParser; 22import android.database.ContentObserver; 23import android.os.Handler; 24import android.os.Message; 25import android.provider.Settings; 26import android.telephony.PhoneNumberUtils; 27import android.util.Log; 28 29import com.android.internal.util.XmlUtils; 30 31import org.xmlpull.v1.XmlPullParser; 32import org.xmlpull.v1.XmlPullParserException; 33import org.xmlpull.v1.XmlPullParserFactory; 34 35import java.io.IOException; 36import java.io.StringReader; 37import java.util.ArrayList; 38import java.util.HashMap; 39import java.util.HashSet; 40import java.util.Iterator; 41import java.util.Map; 42import java.util.regex.Pattern; 43 44/** 45 * Implement the per-application based SMS control, which limits the number of 46 * SMS/MMS messages an app can send in the checking period. 47 * 48 * This code was formerly part of {@link SMSDispatcher}, and has been moved 49 * into a separate class to support instantiation of multiple SMSDispatchers on 50 * dual-mode devices that require support for both 3GPP and 3GPP2 format messages. 51 */ 52public class SmsUsageMonitor { 53 private static final String TAG = "SmsUsageMonitor"; 54 private static final boolean DBG = true; 55 private static final boolean VDBG = false; 56 57 /** Default checking period for SMS sent without user permission. */ 58 private static final int DEFAULT_SMS_CHECK_PERIOD = 1800000; // 30 minutes 59 60 /** Default number of SMS sent in checking period without user permission. */ 61 private static final int DEFAULT_SMS_MAX_COUNT = 30; 62 63 /** Return value from {@link #checkDestination} for regular phone numbers. */ 64 static final int CATEGORY_NOT_SHORT_CODE = 0; 65 66 /** Return value from {@link #checkDestination} for free (no cost) short codes. */ 67 static final int CATEGORY_FREE_SHORT_CODE = 1; 68 69 /** Return value from {@link #checkDestination} for standard rate (non-premium) short codes. */ 70 static final int CATEGORY_STANDARD_SHORT_CODE = 2; 71 72 /** Return value from {@link #checkDestination} for possible premium short codes. */ 73 static final int CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE = 3; 74 75 /** Return value from {@link #checkDestination} for premium short codes. */ 76 static final int CATEGORY_PREMIUM_SHORT_CODE = 4; 77 78 private final int mCheckPeriod; 79 private final int mMaxAllowed; 80 81 private final HashMap<String, ArrayList<Long>> mSmsStamp = 82 new HashMap<String, ArrayList<Long>>(); 83 84 /** Context for retrieving regexes from XML resource. */ 85 private final Context mContext; 86 87 /** Country code for the cached short code pattern matcher. */ 88 private String mCurrentCountry; 89 90 /** Cached short code pattern matcher for {@link #mCurrentCountry}. */ 91 private ShortCodePatternMatcher mCurrentPatternMatcher; 92 93 /** Cached short code regex patterns from secure settings for {@link #mCurrentCountry}. */ 94 private String mSettingsShortCodePatterns; 95 96 /** Handler for responding to content observer updates. */ 97 private final SettingsObserverHandler mSettingsObserverHandler; 98 99 /** XML tag for root element. */ 100 private static final String TAG_SHORTCODES = "shortcodes"; 101 102 /** XML tag for short code patterns for a specific country. */ 103 private static final String TAG_SHORTCODE = "shortcode"; 104 105 /** XML attribute for the country code. */ 106 private static final String ATTR_COUNTRY = "country"; 107 108 /** XML attribute for the short code regex pattern. */ 109 private static final String ATTR_PATTERN = "pattern"; 110 111 /** XML attribute for the premium short code regex pattern. */ 112 private static final String ATTR_PREMIUM = "premium"; 113 114 /** XML attribute for the free short code regex pattern. */ 115 private static final String ATTR_FREE = "free"; 116 117 /** XML attribute for the standard rate short code regex pattern. */ 118 private static final String ATTR_STANDARD = "standard"; 119 120 /** 121 * SMS short code regex pattern matcher for a specific country. 122 */ 123 private static final class ShortCodePatternMatcher { 124 private final Pattern mShortCodePattern; 125 private final Pattern mPremiumShortCodePattern; 126 private final Pattern mFreeShortCodePattern; 127 private final Pattern mStandardShortCodePattern; 128 129 ShortCodePatternMatcher(String shortCodeRegex, String premiumShortCodeRegex, 130 String freeShortCodeRegex, String standardShortCodeRegex) { 131 mShortCodePattern = (shortCodeRegex != null ? Pattern.compile(shortCodeRegex) : null); 132 mPremiumShortCodePattern = (premiumShortCodeRegex != null ? 133 Pattern.compile(premiumShortCodeRegex) : null); 134 mFreeShortCodePattern = (freeShortCodeRegex != null ? 135 Pattern.compile(freeShortCodeRegex) : null); 136 mStandardShortCodePattern = (standardShortCodeRegex != null ? 137 Pattern.compile(standardShortCodeRegex) : null); 138 } 139 140 int getNumberCategory(String phoneNumber) { 141 if (mFreeShortCodePattern != null && mFreeShortCodePattern.matcher(phoneNumber) 142 .matches()) { 143 return CATEGORY_FREE_SHORT_CODE; 144 } 145 if (mStandardShortCodePattern != null && mStandardShortCodePattern.matcher(phoneNumber) 146 .matches()) { 147 return CATEGORY_STANDARD_SHORT_CODE; 148 } 149 if (mPremiumShortCodePattern != null && mPremiumShortCodePattern.matcher(phoneNumber) 150 .matches()) { 151 return CATEGORY_PREMIUM_SHORT_CODE; 152 } 153 if (mShortCodePattern != null && mShortCodePattern.matcher(phoneNumber).matches()) { 154 return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; 155 } 156 return CATEGORY_NOT_SHORT_CODE; 157 } 158 } 159 160 /** 161 * Observe the secure setting for updated regex patterns. 162 */ 163 private static class SettingsObserver extends ContentObserver { 164 private final int mWhat; 165 private final Handler mHandler; 166 167 SettingsObserver(Handler handler, int what) { 168 super(handler); 169 mHandler = handler; 170 mWhat = what; 171 } 172 173 @Override 174 public void onChange(boolean selfChange) { 175 mHandler.obtainMessage(mWhat).sendToTarget(); 176 } 177 } 178 179 /** 180 * Handler to update regex patterns when secure setting for the current country is updated. 181 */ 182 private class SettingsObserverHandler extends Handler { 183 /** Current content observer, or null. */ 184 SettingsObserver mSettingsObserver; 185 186 /** Current country code to watch for settings updates. */ 187 private String mCountryIso; 188 189 /** Request to start observing a secure setting. */ 190 static final int OBSERVE_SETTING = 1; 191 192 /** Handler event for updated secure settings. */ 193 static final int SECURE_SETTINGS_CHANGED = 2; 194 195 /** Send a message to this handler requesting to observe the setting for a new country. */ 196 void observeSettingForCountry(String countryIso) { 197 obtainMessage(OBSERVE_SETTING, countryIso).sendToTarget(); 198 } 199 200 @Override 201 public void handleMessage(Message msg) { 202 switch (msg.what) { 203 case OBSERVE_SETTING: 204 if (msg.obj != null && msg.obj instanceof String) { 205 mCountryIso = (String) msg.obj; 206 String settingName = getSettingNameForCountry(mCountryIso); 207 ContentResolver resolver = mContext.getContentResolver(); 208 209 if (mSettingsObserver != null) { 210 if (VDBG) log("Unregistering old content observer"); 211 resolver.unregisterContentObserver(mSettingsObserver); 212 } 213 214 mSettingsObserver = new SettingsObserver(this, SECURE_SETTINGS_CHANGED); 215 resolver.registerContentObserver( 216 Settings.Secure.getUriFor(settingName), false, mSettingsObserver); 217 if (VDBG) log("Registered content observer for " + settingName); 218 } 219 break; 220 221 case SECURE_SETTINGS_CHANGED: 222 loadPatternsFromSettings(mCountryIso); 223 break; 224 } 225 } 226 } 227 228 /** 229 * Create SMS usage monitor. 230 * @param context the context to use to load resources and get TelephonyManager service 231 */ 232 public SmsUsageMonitor(Context context) { 233 mContext = context; 234 ContentResolver resolver = context.getContentResolver(); 235 236 mMaxAllowed = Settings.Secure.getInt(resolver, 237 Settings.Secure.SMS_OUTGOING_CHECK_MAX_COUNT, 238 DEFAULT_SMS_MAX_COUNT); 239 240 mCheckPeriod = Settings.Secure.getInt(resolver, 241 Settings.Secure.SMS_OUTGOING_CHECK_INTERVAL_MS, 242 DEFAULT_SMS_CHECK_PERIOD); 243 244 mSettingsObserverHandler = new SettingsObserverHandler(); 245 } 246 247 /** 248 * Return a pattern matcher object for the specified country. 249 * @param country the country to search for 250 * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found 251 */ 252 private ShortCodePatternMatcher getPatternMatcher(String country) { 253 int id = com.android.internal.R.xml.sms_short_codes; 254 XmlResourceParser parser = mContext.getResources().getXml(id); 255 256 try { 257 return getPatternMatcher(country, parser); 258 } catch (XmlPullParserException e) { 259 Log.e(TAG, "XML parser exception reading short code pattern resource", e); 260 } catch (IOException e) { 261 Log.e(TAG, "I/O exception reading short code pattern resource", e); 262 } finally { 263 parser.close(); 264 } 265 return null; // country not found 266 } 267 268 /** 269 * Return a pattern matcher object for the specified country from a secure settings string. 270 * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found 271 */ 272 private static ShortCodePatternMatcher getPatternMatcher(String country, String settingsPattern) { 273 // embed pattern tag into an XML document. 274 String document = "<shortcodes>" + settingsPattern + "</shortcodes>"; 275 if (VDBG) log("loading updated patterns from: " + document); 276 277 try { 278 XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 279 XmlPullParser parser = factory.newPullParser(); 280 parser.setInput(new StringReader(document)); 281 return getPatternMatcher(country, parser); 282 } catch (XmlPullParserException e) { 283 Log.e(TAG, "XML parser exception reading short code pattern from settings", e); 284 } catch (IOException e) { 285 Log.e(TAG, "I/O exception reading short code pattern from settings", e); 286 } 287 return null; // country not found 288 } 289 290 /** 291 * Return a pattern matcher object for the specified country and pattern XML parser. 292 * @param country the country to search for 293 * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found 294 */ 295 private static ShortCodePatternMatcher getPatternMatcher(String country, XmlPullParser parser) 296 throws XmlPullParserException, IOException 297 { 298 XmlUtils.beginDocument(parser, TAG_SHORTCODES); 299 300 while (true) { 301 XmlUtils.nextElement(parser); 302 303 String element = parser.getName(); 304 if (element == null) break; 305 306 if (element.equals(TAG_SHORTCODE)) { 307 String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY); 308 if (country.equals(currentCountry)) { 309 String pattern = parser.getAttributeValue(null, ATTR_PATTERN); 310 String premium = parser.getAttributeValue(null, ATTR_PREMIUM); 311 String free = parser.getAttributeValue(null, ATTR_FREE); 312 String standard = parser.getAttributeValue(null, ATTR_STANDARD); 313 return new ShortCodePatternMatcher(pattern, premium, free, standard); 314 } 315 } else { 316 Log.e(TAG, "Error: skipping unknown XML tag " + element); 317 } 318 } 319 return null; // country not found 320 } 321 322 /** Clear the SMS application list for disposal. */ 323 void dispose() { 324 mSmsStamp.clear(); 325 } 326 327 /** 328 * Check to see if an application is allowed to send new SMS messages, and confirm with 329 * user if the send limit was reached or if a non-system app is potentially sending to a 330 * premium SMS short code or number. 331 * 332 * @param appName the package name of the app requesting to send an SMS 333 * @param smsWaiting the number of new messages desired to send 334 * @return true if application is allowed to send the requested number 335 * of new sms messages 336 */ 337 public boolean check(String appName, int smsWaiting) { 338 synchronized (mSmsStamp) { 339 removeExpiredTimestamps(); 340 341 ArrayList<Long> sentList = mSmsStamp.get(appName); 342 if (sentList == null) { 343 sentList = new ArrayList<Long>(); 344 mSmsStamp.put(appName, sentList); 345 } 346 347 return isUnderLimit(sentList, smsWaiting); 348 } 349 } 350 351 /** 352 * Check if the destination is a possible premium short code. 353 * NOTE: the caller is expected to strip non-digits from the destination number with 354 * {@link PhoneNumberUtils#extractNetworkPortion} before calling this method. 355 * This happens in {@link SMSDispatcher#sendRawPdu} so that we use the same phone number 356 * for testing and in the user confirmation dialog if the user needs to confirm the number. 357 * This makes it difficult for malware to fool the user or the short code pattern matcher 358 * by using non-ASCII characters to make the number appear to be different from the real 359 * destination phone number. 360 * 361 * @param destAddress the destination address to test for possible short code 362 * @return {@link #CATEGORY_NOT_SHORT_CODE}, {@link #CATEGORY_FREE_SHORT_CODE}, 363 * {@link #CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE}, or {@link #CATEGORY_PREMIUM_SHORT_CODE}. 364 */ 365 public int checkDestination(String destAddress, String countryIso) { 366 synchronized (mSettingsObserverHandler) { 367 // always allow emergency numbers 368 if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) { 369 return CATEGORY_NOT_SHORT_CODE; 370 } 371 372 ShortCodePatternMatcher patternMatcher = null; 373 374 if (countryIso != null) { 375 // query secure settings and initialize content observer for updated regex patterns 376 if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry)) { 377 loadPatternsFromSettings(countryIso); 378 mSettingsObserverHandler.observeSettingForCountry(countryIso); 379 } 380 381 if (countryIso.equals(mCurrentCountry)) { 382 patternMatcher = mCurrentPatternMatcher; 383 } else { 384 patternMatcher = getPatternMatcher(countryIso); 385 mCurrentCountry = countryIso; 386 mCurrentPatternMatcher = patternMatcher; // may be null if not found 387 } 388 } 389 390 if (patternMatcher != null) { 391 return patternMatcher.getNumberCategory(destAddress); 392 } else { 393 // Generic rule: numbers of 5 digits or less are considered potential short codes 394 Log.e(TAG, "No patterns for \"" + countryIso + "\": using generic short code rule"); 395 if (destAddress.length() <= 5) { 396 return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE; 397 } else { 398 return CATEGORY_NOT_SHORT_CODE; 399 } 400 } 401 } 402 } 403 404 private static String getSettingNameForCountry(String countryIso) { 405 return Settings.Secure.SMS_SHORT_CODES_PREFIX + countryIso; 406 } 407 408 /** 409 * Load regex patterns from secure settings if present. 410 * @param countryIso the country to search for 411 */ 412 void loadPatternsFromSettings(String countryIso) { 413 synchronized (mSettingsObserverHandler) { 414 if (VDBG) log("loadPatternsFromSettings(" + countryIso + ") called"); 415 String settingsPatterns = Settings.Secure.getString( 416 mContext.getContentResolver(), getSettingNameForCountry(countryIso)); 417 if (settingsPatterns != null && !settingsPatterns.equals( 418 mSettingsShortCodePatterns)) { 419 // settings pattern string has changed: update the pattern matcher 420 mSettingsShortCodePatterns = settingsPatterns; 421 ShortCodePatternMatcher matcher = getPatternMatcher(countryIso, settingsPatterns); 422 if (matcher != null) { 423 mCurrentCountry = countryIso; 424 mCurrentPatternMatcher = matcher; 425 } 426 } else if (settingsPatterns == null && mSettingsShortCodePatterns != null) { 427 // pattern string was removed: caller will load default patterns from XML resource 428 mCurrentCountry = null; 429 mCurrentPatternMatcher = null; 430 mSettingsShortCodePatterns = null; 431 } 432 } 433 } 434 435 /** 436 * Remove keys containing only old timestamps. This can happen if an SMS app is used 437 * to send messages and then uninstalled. 438 */ 439 private void removeExpiredTimestamps() { 440 long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod; 441 442 synchronized (mSmsStamp) { 443 Iterator<Map.Entry<String, ArrayList<Long>>> iter = mSmsStamp.entrySet().iterator(); 444 while (iter.hasNext()) { 445 Map.Entry<String, ArrayList<Long>> entry = iter.next(); 446 ArrayList<Long> oldList = entry.getValue(); 447 if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) { 448 iter.remove(); 449 } 450 } 451 } 452 } 453 454 private boolean isUnderLimit(ArrayList<Long> sent, int smsWaiting) { 455 Long ct = System.currentTimeMillis(); 456 long beginCheckPeriod = ct - mCheckPeriod; 457 458 if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct); 459 460 while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) { 461 sent.remove(0); 462 } 463 464 if ((sent.size() + smsWaiting) <= mMaxAllowed) { 465 for (int i = 0; i < smsWaiting; i++ ) { 466 sent.add(ct); 467 } 468 return true; 469 } 470 return false; 471 } 472 473 private static void log(String msg) { 474 Log.d(TAG, msg); 475 } 476} 477