/* * Copyright (C) 2011 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.app.AppGlobals; import android.content.ContentResolver; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.res.XmlResourceParser; import android.database.ContentObserver; import android.net.Uri; import android.os.Binder; import android.os.Handler; import android.os.Message; import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.provider.Settings; import android.telephony.PhoneNumberUtils; import android.util.AtomicFile; import android.util.Log; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; import java.util.HashMap; 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 * SMS/MMS messages an app can send in the checking period. * * This code was formerly part of {@link SMSDispatcher}, and has been moved * into a separate class to support instantiation of multiple SMSDispatchers on * dual-mode devices that require support for both 3GPP and 3GPP2 format messages. */ public class SmsUsageMonitor { private static final String TAG = "SmsUsageMonitor"; private static final boolean DBG = false; private static final boolean VDBG = false; private static final String SHORT_CODE_PATH = "/data/misc/sms/codes"; /** Default checking period for SMS sent without user permission. */ private static final int DEFAULT_SMS_CHECK_PERIOD = 60000; // 1 minute /** 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; /** @hide */ public static int mergeShortCodeCategories(int type1, int type2) { if (type1 > type2) return type1; return type2; } /** Premium SMS permission for a new package (ask user when first premium SMS sent). */ public static final int PREMIUM_SMS_PERMISSION_UNKNOWN = 0; /** Default premium SMS permission (ask user for each premium SMS sent). */ public static final int PREMIUM_SMS_PERMISSION_ASK_USER = 1; /** Premium SMS permission when the owner has denied the app from sending premium SMS. */ public static final int PREMIUM_SMS_PERMISSION_NEVER_ALLOW = 2; /** Premium SMS permission when the owner has allowed the app to send premium SMS. */ public static final int PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW = 3; private final int mCheckPeriod; private int mMaxAllowed; private final HashMap> mSmsStamp = new HashMap>(); /** 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; /** Notice when the enabled setting changes - can be changed through gservices */ private final AtomicBoolean mCheckEnabled = new AtomicBoolean(true); /** 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; /** File holding the patterns */ private final File mPatternFile = new File(SHORT_CODE_PATH); /** Last modified time for pattern file */ private long mPatternFileLastModified = 0; /** Directory for per-app SMS permission XML file. */ private static final String SMS_POLICY_FILE_DIRECTORY = "/data/misc/sms"; /** Per-app SMS permission XML filename. */ private static final String SMS_POLICY_FILE_NAME = "premium_sms_policy.xml"; /** 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"; /** Stored copy of premium SMS package permissions. */ private AtomicFile mPolicyFile; /** Loaded copy of premium SMS package permissions. */ private final HashMap mPremiumSmsPolicy = new HashMap(); /** XML tag for root element of premium SMS permissions. */ private static final String TAG_SMS_POLICY_BODY = "premium-sms-policy"; /** XML tag for a package. */ private static final String TAG_PACKAGE = "package"; /** XML attribute for the package name. */ private static final String ATTR_PACKAGE_NAME = "name"; /** XML attribute for the package's premium SMS permission (integer type). */ private static final String ATTR_PACKAGE_SMS_POLICY = "sms-policy"; /** * 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 enable flag */ private class SettingsObserver extends ContentObserver { SettingsObserver(Handler handler) { super(handler); } @Override public void onChange(boolean selfChange, Uri uri) { if (uri.equals(Settings.Global.getUriFor(Settings.Global.SMS_SHORT_CODE_CONFIRMATION))) { mCheckEnabled.set(Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.SMS_SHORT_CODE_CONFIRMATION, 1) != 0); } else if (uri.equals(Settings.Global.getUriFor(Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT))) { mMaxAllowed = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT, DEFAULT_SMS_MAX_COUNT); } } } private class SettingsObserverHandler extends Handler { SettingsObserverHandler(Context context) { ContentResolver resolver = context.getContentResolver(); ContentObserver globalObserver = new SettingsObserver(this); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.SMS_SHORT_CODE_CONFIRMATION), false, globalObserver); resolver.registerContentObserver(Settings.Global.getUriFor( Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT), false, globalObserver); } } /** * 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.Global.getInt(resolver, Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT, DEFAULT_SMS_MAX_COUNT); mCheckPeriod = Settings.Global.getInt(resolver, Settings.Global.SMS_OUTGOING_CHECK_INTERVAL_MS, DEFAULT_SMS_CHECK_PERIOD); mCheckEnabled.set(Settings.Global.getInt(resolver, Settings.Global.SMS_SHORT_CODE_CONFIRMATION, 1) != 0); mSettingsObserverHandler = new SettingsObserverHandler(mContext); loadPremiumSmsPolicyDb(); } /** * 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 getPatternMatcherFromFile(String country) { FileReader patternReader = null; XmlPullParser parser = null; try { patternReader = new FileReader(mPatternFile); parser = Xml.newPullParser(); parser.setInput(patternReader); return getPatternMatcherFromXmlParser(parser, country); } catch (FileNotFoundException e) { Log.e(TAG, "Short Code Pattern File not found"); } catch (XmlPullParserException e) { Log.e(TAG, "XML parser exception reading short code pattern file", e); } finally { mPatternFileLastModified = mPatternFile.lastModified(); if (patternReader != null) { try { patternReader.close(); } catch (IOException e) {} } } return null; } private ShortCodePatternMatcher getPatternMatcherFromResource(String country) { int id = com.android.internal.R.xml.sms_short_codes; XmlResourceParser parser = null; try { parser = mContext.getResources().getXml(id); return getPatternMatcherFromXmlParser(parser, country); } finally { if (parser != null) parser.close(); } } private ShortCodePatternMatcher getPatternMatcherFromXmlParser(XmlPullParser parser, String country) { try { XmlUtils.beginDocument(parser, TAG_SHORTCODES); while (true) { XmlUtils.nextElement(parser); String element = parser.getName(); if (element == null) { Log.e(TAG, "Parsing pattern data found null"); break; } if (element.equals(TAG_SHORTCODE)) { String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY); if (VDBG) Log.d(TAG, "Found country " + currentCountry); 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 patterns", e); } catch (IOException e) { Log.e(TAG, "I/O exception reading short code patterns", e); } if (DBG) Log.d(TAG, "Country (" + country + ") not found"); return null; // country not found } /** Clear the SMS application list for disposal. */ void dispose() { mSmsStamp.clear(); } /** * 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 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 */ public boolean check(String appName, int smsWaiting) { synchronized (mSmsStamp) { removeExpiredTimestamps(); ArrayList sentList = mSmsStamp.get(appName); if (sentList == null) { sentList = new ArrayList(); mSmsStamp.put(appName, sentList); } return isUnderLimit(sentList, smsWaiting); } } /** * 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)) { if (DBG) Log.d(TAG, "isEmergencyNumber"); return CATEGORY_NOT_SHORT_CODE; } // always allow if the feature is disabled if (!mCheckEnabled.get()) { if (DBG) Log.e(TAG, "check disabled"); return CATEGORY_NOT_SHORT_CODE; } if (countryIso != null) { if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry) || mPatternFile.lastModified() != mPatternFileLastModified) { if (mPatternFile.exists()) { if (DBG) Log.d(TAG, "Loading SMS Short Code patterns from file"); mCurrentPatternMatcher = getPatternMatcherFromFile(countryIso); } else { if (DBG) Log.d(TAG, "Loading SMS Short Code patterns from resource"); mCurrentPatternMatcher = getPatternMatcherFromResource(countryIso); } mCurrentCountry = countryIso; } } if (mCurrentPatternMatcher != null) { return mCurrentPatternMatcher.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; } } } } /** * Load the premium SMS policy from an XML file. * Based on code from NotificationManagerService. */ private void loadPremiumSmsPolicyDb() { synchronized (mPremiumSmsPolicy) { if (mPolicyFile == null) { File dir = new File(SMS_POLICY_FILE_DIRECTORY); mPolicyFile = new AtomicFile(new File(dir, SMS_POLICY_FILE_NAME)); mPremiumSmsPolicy.clear(); FileInputStream infile = null; try { infile = mPolicyFile.openRead(); final XmlPullParser parser = Xml.newPullParser(); parser.setInput(infile, null); XmlUtils.beginDocument(parser, TAG_SMS_POLICY_BODY); while (true) { XmlUtils.nextElement(parser); String element = parser.getName(); if (element == null) break; if (element.equals(TAG_PACKAGE)) { String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME); String policy = parser.getAttributeValue(null, ATTR_PACKAGE_SMS_POLICY); if (packageName == null) { Log.e(TAG, "Error: missing package name attribute"); } else if (policy == null) { Log.e(TAG, "Error: missing package policy attribute"); } else try { mPremiumSmsPolicy.put(packageName, Integer.parseInt(policy)); } catch (NumberFormatException e) { Log.e(TAG, "Error: non-numeric policy type " + policy); } } else { Log.e(TAG, "Error: skipping unknown XML tag " + element); } } } catch (FileNotFoundException e) { // No data yet } catch (IOException e) { Log.e(TAG, "Unable to read premium SMS policy database", e); } catch (NumberFormatException e) { Log.e(TAG, "Unable to parse premium SMS policy database", e); } catch (XmlPullParserException e) { Log.e(TAG, "Unable to parse premium SMS policy database", e); } finally { if (infile != null) { try { infile.close(); } catch (IOException ignored) { } } } } } } /** * Persist the premium SMS policy to an XML file. * Based on code from NotificationManagerService. */ private void writePremiumSmsPolicyDb() { synchronized (mPremiumSmsPolicy) { FileOutputStream outfile = null; try { outfile = mPolicyFile.startWrite(); XmlSerializer out = new FastXmlSerializer(); out.setOutput(outfile, "utf-8"); out.startDocument(null, true); out.startTag(null, TAG_SMS_POLICY_BODY); for (Map.Entry policy : mPremiumSmsPolicy.entrySet()) { out.startTag(null, TAG_PACKAGE); out.attribute(null, ATTR_PACKAGE_NAME, policy.getKey()); out.attribute(null, ATTR_PACKAGE_SMS_POLICY, policy.getValue().toString()); out.endTag(null, TAG_PACKAGE); } out.endTag(null, TAG_SMS_POLICY_BODY); out.endDocument(); mPolicyFile.finishWrite(outfile); } catch (IOException e) { Log.e(TAG, "Unable to write premium SMS policy database", e); if (outfile != null) { mPolicyFile.failWrite(outfile); } } } } /** * Returns the premium SMS permission for the specified package. If the package has never * been seen before, the default {@link #PREMIUM_SMS_PERMISSION_ASK_USER} * will be returned. * @param packageName the name of the package to query permission * @return one of {@link #PREMIUM_SMS_PERMISSION_UNKNOWN}, * {@link #PREMIUM_SMS_PERMISSION_ASK_USER}, * {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or * {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW} * @throws SecurityException if the caller is not a system process */ public int getPremiumSmsPermission(String packageName) { checkCallerIsSystemOrSameApp(packageName); synchronized (mPremiumSmsPolicy) { Integer policy = mPremiumSmsPolicy.get(packageName); if (policy == null) { return PREMIUM_SMS_PERMISSION_UNKNOWN; } else { return policy; } } } /** * Sets the premium SMS permission for the specified package and save the value asynchronously * to persistent storage. * @param packageName the name of the package to set permission * @param permission one of {@link #PREMIUM_SMS_PERMISSION_ASK_USER}, * {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or * {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW} * @throws SecurityException if the caller is not a system process */ public void setPremiumSmsPermission(String packageName, int permission) { checkCallerIsSystemOrPhoneApp(); if (permission < PREMIUM_SMS_PERMISSION_ASK_USER || permission > PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW) { throw new IllegalArgumentException("invalid SMS permission type " + permission); } synchronized (mPremiumSmsPolicy) { mPremiumSmsPolicy.put(packageName, permission); } // write policy file in the background new Thread(new Runnable() { @Override public void run() { writePremiumSmsPolicyDb(); } }).start(); } private static void checkCallerIsSystemOrSameApp(String pkg) { int uid = Binder.getCallingUid(); if (UserHandle.getAppId(uid) == Process.SYSTEM_UID || uid == 0) { return; } try { ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo( pkg, 0, UserHandle.getCallingUserId()); if (!UserHandle.isSameApp(ai.uid, uid)) { throw new SecurityException("Calling uid " + uid + " gave package" + pkg + " which is owned by uid " + ai.uid); } } catch (RemoteException re) { throw new SecurityException("Unknown package " + pkg + "\n" + re); } } private static void checkCallerIsSystemOrPhoneApp() { int uid = Binder.getCallingUid(); int appId = UserHandle.getAppId(uid); if (appId == Process.SYSTEM_UID || appId == Process.PHONE_UID || uid == 0) { return; } throw new SecurityException("Disallowed call for uid " + uid); } /** * Remove keys containing only old timestamps. This can happen if an SMS app is used * to send messages and then uninstalled. */ private void removeExpiredTimestamps() { long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod; synchronized (mSmsStamp) { Iterator>> iter = mSmsStamp.entrySet().iterator(); while (iter.hasNext()) { Map.Entry> entry = iter.next(); ArrayList oldList = entry.getValue(); if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) { iter.remove(); } } } } private boolean isUnderLimit(ArrayList sent, int smsWaiting) { Long ct = System.currentTimeMillis(); long beginCheckPeriod = ct - mCheckPeriod; if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct); while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) { sent.remove(0); } if ((sent.size() + smsWaiting) <= mMaxAllowed) { for (int i = 0; i < smsWaiting; i++ ) { sent.add(ct); } return true; } return false; } private static void log(String msg) { Log.d(TAG, msg); } }