FileDocCategorySizeDatePackage
SmsUsageMonitor.javaAPI DocAndroid 5.1 API26785Thu Mar 12 22:22:54 GMT 2015com.android.internal.telephony

SmsUsageMonitor

public class SmsUsageMonitor extends Object
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.

Fields Summary
private static final String
TAG
private static final boolean
DBG
private static final boolean
VDBG
private static final String
SHORT_CODE_PATH
private static final int
DEFAULT_SMS_CHECK_PERIOD
Default checking period for SMS sent without user permission.
private static final int
DEFAULT_SMS_MAX_COUNT
Default number of SMS sent in checking period without user permission.
static final int
CATEGORY_NOT_SHORT_CODE
Return value from {@link #checkDestination} for regular phone numbers.
static final int
CATEGORY_FREE_SHORT_CODE
Return value from {@link #checkDestination} for free (no cost) short codes.
static final int
CATEGORY_STANDARD_SHORT_CODE
Return value from {@link #checkDestination} for standard rate (non-premium) short codes.
static final int
CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE
Return value from {@link #checkDestination} for possible premium short codes.
static final int
CATEGORY_PREMIUM_SHORT_CODE
Return value from {@link #checkDestination} for premium short codes.
public static final int
PREMIUM_SMS_PERMISSION_UNKNOWN
Premium SMS permission for a new package (ask user when first premium SMS sent).
public static final int
PREMIUM_SMS_PERMISSION_ASK_USER
Default premium SMS permission (ask user for each premium SMS sent).
public static final int
PREMIUM_SMS_PERMISSION_NEVER_ALLOW
Premium SMS permission when the owner has denied the app from sending premium SMS.
public static final int
PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW
Premium SMS permission when the owner has allowed the app to send premium SMS.
private final int
mCheckPeriod
private final int
mMaxAllowed
private final HashMap
mSmsStamp
private final android.content.Context
mContext
Context for retrieving regexes from XML resource.
private String
mCurrentCountry
Country code for the cached short code pattern matcher.
private ShortCodePatternMatcher
mCurrentPatternMatcher
Cached short code pattern matcher for {@link #mCurrentCountry}.
private final AtomicBoolean
mCheckEnabled
Notice when the enabled setting changes - can be changed through gservices
private final SettingsObserverHandler
mSettingsObserverHandler
Handler for responding to content observer updates.
private final File
mPatternFile
File holding the patterns
private long
mPatternFileLastModified
Last modified time for pattern file
private static final String
SMS_POLICY_FILE_DIRECTORY
Directory for per-app SMS permission XML file.
private static final String
SMS_POLICY_FILE_NAME
Per-app SMS permission XML filename.
private static final String
TAG_SHORTCODES
XML tag for root element.
private static final String
TAG_SHORTCODE
XML tag for short code patterns for a specific country.
private static final String
ATTR_COUNTRY
XML attribute for the country code.
private static final String
ATTR_PATTERN
XML attribute for the short code regex pattern.
private static final String
ATTR_PREMIUM
XML attribute for the premium short code regex pattern.
private static final String
ATTR_FREE
XML attribute for the free short code regex pattern.
private static final String
ATTR_STANDARD
XML attribute for the standard rate short code regex pattern.
private android.util.AtomicFile
mPolicyFile
Stored copy of premium SMS package permissions.
private final HashMap
mPremiumSmsPolicy
Loaded copy of premium SMS package permissions.
private static final String
TAG_SMS_POLICY_BODY
XML tag for root element of premium SMS permissions.
private static final String
TAG_PACKAGE
XML tag for a package.
private static final String
ATTR_PACKAGE_NAME
XML attribute for the package name.
private static final String
ATTR_PACKAGE_SMS_POLICY
XML attribute for the package's premium SMS permission (integer type).
Constructors Summary
public SmsUsageMonitor(android.content.Context context)
Create SMS usage monitor.

param
context the context to use to load resources and get TelephonyManager service

        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);

        mSettingsObserverHandler = new SettingsObserverHandler(mContext, mCheckEnabled);

        loadPremiumSmsPolicyDb();
    
Methods Summary
public booleancheck(java.lang.String appName, int smsWaiting)
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

        synchronized (mSmsStamp) {
            removeExpiredTimestamps();

            ArrayList<Long> sentList = mSmsStamp.get(appName);
            if (sentList == null) {
                sentList = new ArrayList<Long>();
                mSmsStamp.put(appName, sentList);
            }

            return isUnderLimit(sentList, smsWaiting);
        }
    
private static voidcheckCallerIsSystemOrPhoneApp()

        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);
    
private static voidcheckCallerIsSystemOrPhoneOrSameApp(java.lang.String pkg)

        int uid = Binder.getCallingUid();
        int appId = UserHandle.getAppId(uid);
        if (appId == Process.SYSTEM_UID || appId == Process.PHONE_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);
        }
    
public intcheckDestination(java.lang.String destAddress, java.lang.String countryIso)
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}.

        synchronized (mSettingsObserverHandler) {
            // always allow emergency numbers
            if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) {
                if (DBG) Rlog.d(TAG, "isEmergencyNumber");
                return CATEGORY_NOT_SHORT_CODE;
            }
            // always allow if the feature is disabled
            if (!mCheckEnabled.get()) {
                if (DBG) Rlog.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) Rlog.d(TAG, "Loading SMS Short Code patterns from file");
                        mCurrentPatternMatcher = getPatternMatcherFromFile(countryIso);
                    } else {
                        if (DBG) Rlog.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
                Rlog.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;
                }
            }
        }
    
voiddispose()
Clear the SMS application list for disposal.

        mSmsStamp.clear();
    
private com.android.internal.telephony.SmsUsageMonitor$ShortCodePatternMatchergetPatternMatcherFromFile(java.lang.String country)
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

        FileReader patternReader = null;
        XmlPullParser parser = null;
        try {
            patternReader = new FileReader(mPatternFile);
            parser = Xml.newPullParser();
            parser.setInput(patternReader);
            return getPatternMatcherFromXmlParser(parser, country);
        } catch (FileNotFoundException e) {
            Rlog.e(TAG, "Short Code Pattern File not found");
        } catch (XmlPullParserException e) {
            Rlog.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 com.android.internal.telephony.SmsUsageMonitor$ShortCodePatternMatchergetPatternMatcherFromResource(java.lang.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 com.android.internal.telephony.SmsUsageMonitor$ShortCodePatternMatchergetPatternMatcherFromXmlParser(org.xmlpull.v1.XmlPullParser parser, java.lang.String country)

        try {
            XmlUtils.beginDocument(parser, TAG_SHORTCODES);

            while (true) {
                XmlUtils.nextElement(parser);
                String element = parser.getName();
                if (element == null) {
                    Rlog.e(TAG, "Parsing pattern data found null");
                    break;
                }

                if (element.equals(TAG_SHORTCODE)) {
                    String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY);
                    if (VDBG) Rlog.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 {
                    Rlog.e(TAG, "Error: skipping unknown XML tag " + element);
                }
            }
        } catch (XmlPullParserException e) {
            Rlog.e(TAG, "XML parser exception reading short code patterns", e);
        } catch (IOException e) {
            Rlog.e(TAG, "I/O exception reading short code patterns", e);
        }
        if (DBG) Rlog.d(TAG, "Country (" + country + ") not found");
        return null;    // country not found
    
public intgetPremiumSmsPermission(java.lang.String packageName)
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

        checkCallerIsSystemOrPhoneOrSameApp(packageName);
        synchronized (mPremiumSmsPolicy) {
            Integer policy = mPremiumSmsPolicy.get(packageName);
            if (policy == null) {
                return PREMIUM_SMS_PERMISSION_UNKNOWN;
            } else {
                return policy;
            }
        }
    
private booleanisUnderLimit(java.util.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 voidloadPremiumSmsPolicyDb()
Load the premium SMS policy from an XML file. Based on code from NotificationManagerService.

        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) {
                                Rlog.e(TAG, "Error: missing package name attribute");
                            } else if (policy == null) {
                                Rlog.e(TAG, "Error: missing package policy attribute");
                            } else try {
                                mPremiumSmsPolicy.put(packageName, Integer.parseInt(policy));
                            } catch (NumberFormatException e) {
                                Rlog.e(TAG, "Error: non-numeric policy type " + policy);
                            }
                        } else {
                            Rlog.e(TAG, "Error: skipping unknown XML tag " + element);
                        }
                    }
                } catch (FileNotFoundException e) {
                    // No data yet
                } catch (IOException e) {
                    Rlog.e(TAG, "Unable to read premium SMS policy database", e);
                } catch (NumberFormatException e) {
                    Rlog.e(TAG, "Unable to parse premium SMS policy database", e);
                } catch (XmlPullParserException e) {
                    Rlog.e(TAG, "Unable to parse premium SMS policy database", e);
                } finally {
                    if (infile != null) {
                        try {
                            infile.close();
                        } catch (IOException ignored) {
                        }
                    }
                }
            }
        }
    
private static voidlog(java.lang.String msg)

        Rlog.d(TAG, msg);
    
public static intmergeShortCodeCategories(int type1, int type2)

hide


      
           
        if (type1 > type2) return type1;
        return type2;
    
private voidremoveExpiredTimestamps()
Remove keys containing only old timestamps. This can happen if an SMS app is used to send messages and then uninstalled.

        long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod;

        synchronized (mSmsStamp) {
            Iterator<Map.Entry<String, ArrayList<Long>>> iter = mSmsStamp.entrySet().iterator();
            while (iter.hasNext()) {
                Map.Entry<String, ArrayList<Long>> entry = iter.next();
                ArrayList<Long> oldList = entry.getValue();
                if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) {
                    iter.remove();
                }
            }
        }
    
public voidsetPremiumSmsPermission(java.lang.String packageName, int permission)
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

        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 voidwritePremiumSmsPolicyDb()
Persist the premium SMS policy to an XML file. Based on code from NotificationManagerService.

        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<String, Integer> 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) {
                Rlog.e(TAG, "Unable to write premium SMS policy database", e);
                if (outfile != null) {
                    mPolicyFile.failWrite(outfile);
                }
            }
        }