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