SmsUsageMonitorpublic 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_PERIODDefault checking period for SMS sent without user permission. | private static final int | DEFAULT_SMS_MAX_COUNTDefault number of SMS sent in checking period without user permission. | static final int | CATEGORY_NOT_SHORT_CODEReturn value from {@link #checkDestination} for regular phone numbers. | static final int | CATEGORY_FREE_SHORT_CODEReturn value from {@link #checkDestination} for free (no cost) short codes. | static final int | CATEGORY_STANDARD_SHORT_CODEReturn value from {@link #checkDestination} for standard rate (non-premium) short codes. | static final int | CATEGORY_POSSIBLE_PREMIUM_SHORT_CODEReturn value from {@link #checkDestination} for possible premium short codes. | static final int | CATEGORY_PREMIUM_SHORT_CODEReturn value from {@link #checkDestination} for premium short codes. | public static final int | PREMIUM_SMS_PERMISSION_UNKNOWNPremium SMS permission for a new package (ask user when first premium SMS sent). | public static final int | PREMIUM_SMS_PERMISSION_ASK_USERDefault premium SMS permission (ask user for each premium SMS sent). | public static final int | PREMIUM_SMS_PERMISSION_NEVER_ALLOWPremium SMS permission when the owner has denied the app from sending premium SMS. | public static final int | PREMIUM_SMS_PERMISSION_ALWAYS_ALLOWPremium 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 | mContextContext for retrieving regexes from XML resource. | private String | mCurrentCountryCountry code for the cached short code pattern matcher. | private ShortCodePatternMatcher | mCurrentPatternMatcherCached short code pattern matcher for {@link #mCurrentCountry}. | private final AtomicBoolean | mCheckEnabledNotice when the enabled setting changes - can be changed through gservices | private final SettingsObserverHandler | mSettingsObserverHandlerHandler for responding to content observer updates. | private final File | mPatternFileFile holding the patterns | private long | mPatternFileLastModifiedLast modified time for pattern file | private static final String | SMS_POLICY_FILE_DIRECTORYDirectory for per-app SMS permission XML file. | private static final String | SMS_POLICY_FILE_NAMEPer-app SMS permission XML filename. | private static final String | TAG_SHORTCODESXML tag for root element. | private static final String | TAG_SHORTCODEXML tag for short code patterns for a specific country. | private static final String | ATTR_COUNTRYXML attribute for the country code. | private static final String | ATTR_PATTERNXML attribute for the short code regex pattern. | private static final String | ATTR_PREMIUMXML attribute for the premium short code regex pattern. | private static final String | ATTR_FREEXML attribute for the free short code regex pattern. | private static final String | ATTR_STANDARDXML attribute for the standard rate short code regex pattern. | private android.util.AtomicFile | mPolicyFileStored copy of premium SMS package permissions. | private final HashMap | mPremiumSmsPolicyLoaded copy of premium SMS package permissions. | private static final String | TAG_SMS_POLICY_BODYXML tag for root element of premium SMS permissions. | private static final String | TAG_PACKAGEXML tag for a package. | private static final String | ATTR_PACKAGE_NAMEXML attribute for the package name. | private static final String | ATTR_PACKAGE_SMS_POLICYXML attribute for the package's premium SMS permission (integer type). |
Constructors Summary |
---|
public SmsUsageMonitor(android.content.Context context)Create SMS usage monitor.
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 boolean | check(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.
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 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);
| private static void | checkCallerIsSystemOrPhoneOrSameApp(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 int | checkDestination(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.
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;
}
}
}
| void | dispose()Clear the SMS application list for disposal.
mSmsStamp.clear();
| private com.android.internal.telephony.SmsUsageMonitor$ShortCodePatternMatcher | getPatternMatcherFromFile(java.lang.String country)Return a pattern matcher object for the specified 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) {
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$ShortCodePatternMatcher | getPatternMatcherFromResource(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$ShortCodePatternMatcher | getPatternMatcherFromXmlParser(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 int | getPremiumSmsPermission(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.
checkCallerIsSystemOrPhoneOrSameApp(packageName);
synchronized (mPremiumSmsPolicy) {
Integer policy = mPremiumSmsPolicy.get(packageName);
if (policy == null) {
return PREMIUM_SMS_PERMISSION_UNKNOWN;
} else {
return policy;
}
}
| private boolean | isUnderLimit(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 void | loadPremiumSmsPolicyDb()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 void | log(java.lang.String msg)
Rlog.d(TAG, msg);
| public static int | mergeShortCodeCategories(int type1, int type2)
if (type1 > type2) return type1;
return type2;
| private void | removeExpiredTimestamps()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 void | setPremiumSmsPermission(java.lang.String packageName, int permission)Sets the premium SMS permission for the specified package and save the value asynchronously
to persistent storage.
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 void | writePremiumSmsPolicyDb()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);
}
}
}
|
|