SmsUsageMonitor.java revision 10270d2586e2d940dea23b19f6ad733f702609be
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.app.AppGlobals;
20import android.content.ContentResolver;
21import android.content.Context;
22import android.content.pm.ApplicationInfo;
23import android.content.res.XmlResourceParser;
24import android.database.ContentObserver;
25import android.os.Binder;
26import android.os.Handler;
27import android.os.Message;
28import android.os.Process;
29import android.os.RemoteException;
30import android.os.UserHandle;
31import android.provider.Settings;
32import android.telephony.PhoneNumberUtils;
33import android.util.AtomicFile;
34import android.util.Log;
35import android.util.Xml;
36
37import com.android.internal.util.FastXmlSerializer;
38import com.android.internal.util.XmlUtils;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42import org.xmlpull.v1.XmlPullParserFactory;
43import org.xmlpull.v1.XmlSerializer;
44
45import java.io.File;
46import java.io.FileInputStream;
47import java.io.FileNotFoundException;
48import java.io.FileOutputStream;
49import java.io.FileReader;
50import java.io.IOException;
51import java.io.StringReader;
52import java.util.ArrayList;
53import java.util.concurrent.atomic.AtomicBoolean;
54import java.util.HashMap;
55import java.util.Iterator;
56import java.util.Map;
57import java.util.regex.Pattern;
58
59/**
60 * Implement the per-application based SMS control, which limits the number of
61 * SMS/MMS messages an app can send in the checking period.
62 *
63 * This code was formerly part of {@link SMSDispatcher}, and has been moved
64 * into a separate class to support instantiation of multiple SMSDispatchers on
65 * dual-mode devices that require support for both 3GPP and 3GPP2 format messages.
66 */
67public class SmsUsageMonitor {
68    private static final String TAG = "SmsUsageMonitor";
69    private static final boolean DBG = false;
70    private static final boolean VDBG = false;
71
72    private static final String SHORT_CODE_PATH = "/data/misc/sms/codes";
73
74    /** Default checking period for SMS sent without user permission. */
75    private static final int DEFAULT_SMS_CHECK_PERIOD = 1800000;    // 30 minutes
76
77    /** Default number of SMS sent in checking period without user permission. */
78    private static final int DEFAULT_SMS_MAX_COUNT = 30;
79
80    /** Return value from {@link #checkDestination} for regular phone numbers. */
81    static final int CATEGORY_NOT_SHORT_CODE = 0;
82
83    /** Return value from {@link #checkDestination} for free (no cost) short codes. */
84    static final int CATEGORY_FREE_SHORT_CODE = 1;
85
86    /** Return value from {@link #checkDestination} for standard rate (non-premium) short codes. */
87    static final int CATEGORY_STANDARD_SHORT_CODE = 2;
88
89    /** Return value from {@link #checkDestination} for possible premium short codes. */
90    static final int CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE = 3;
91
92    /** Return value from {@link #checkDestination} for premium short codes. */
93    static final int CATEGORY_PREMIUM_SHORT_CODE = 4;
94
95    /** @hide */
96    public static int mergeShortCodeCategories(int type1, int type2) {
97        if (type1 > type2) return type1;
98        return type2;
99    }
100
101    /** Premium SMS permission for a new package (ask user when first premium SMS sent). */
102    public static final int PREMIUM_SMS_PERMISSION_UNKNOWN = 0;
103
104    /** Default premium SMS permission (ask user for each premium SMS sent). */
105    public static final int PREMIUM_SMS_PERMISSION_ASK_USER = 1;
106
107    /** Premium SMS permission when the owner has denied the app from sending premium SMS. */
108    public static final int PREMIUM_SMS_PERMISSION_NEVER_ALLOW = 2;
109
110    /** Premium SMS permission when the owner has allowed the app to send premium SMS. */
111    public static final int PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW = 3;
112
113    private final int mCheckPeriod;
114    private final int mMaxAllowed;
115
116    private final HashMap<String, ArrayList<Long>> mSmsStamp =
117            new HashMap<String, ArrayList<Long>>();
118
119    /** Context for retrieving regexes from XML resource. */
120    private final Context mContext;
121
122    /** Country code for the cached short code pattern matcher. */
123    private String mCurrentCountry;
124
125    /** Cached short code pattern matcher for {@link #mCurrentCountry}. */
126    private ShortCodePatternMatcher mCurrentPatternMatcher;
127
128    /** Notice when the enabled setting changes - can be changed through gservices */
129    private final AtomicBoolean mCheckEnabled = new AtomicBoolean(true);
130
131    /** Cached short code regex patterns from secure settings for {@link #mCurrentCountry}. */
132    private String mSettingsShortCodePatterns;
133
134    /** Handler for responding to content observer updates. */
135    private final SettingsObserverHandler mSettingsObserverHandler;
136
137    /** File holding the patterns */
138    private final File mPatternFile = new File(SHORT_CODE_PATH);
139
140    /** Last modified time for pattern file */
141    private long mPatternFileLastModified = 0;
142
143    /** Directory for per-app SMS permission XML file. */
144    private static final String SMS_POLICY_FILE_DIRECTORY = "/data/radio";
145
146    /** Per-app SMS permission XML filename. */
147    private static final String SMS_POLICY_FILE_NAME = "premium_sms_policy.xml";
148
149    /** XML tag for root element. */
150    private static final String TAG_SHORTCODES = "shortcodes";
151
152    /** XML tag for short code patterns for a specific country. */
153    private static final String TAG_SHORTCODE = "shortcode";
154
155    /** XML attribute for the country code. */
156    private static final String ATTR_COUNTRY = "country";
157
158    /** XML attribute for the short code regex pattern. */
159    private static final String ATTR_PATTERN = "pattern";
160
161    /** XML attribute for the premium short code regex pattern. */
162    private static final String ATTR_PREMIUM = "premium";
163
164    /** XML attribute for the free short code regex pattern. */
165    private static final String ATTR_FREE = "free";
166
167    /** XML attribute for the standard rate short code regex pattern. */
168    private static final String ATTR_STANDARD = "standard";
169
170    /** Stored copy of premium SMS package permissions. */
171    private AtomicFile mPolicyFile;
172
173    /** Loaded copy of premium SMS package permissions. */
174    private final HashMap<String, Integer> mPremiumSmsPolicy = new HashMap<String, Integer>();
175
176    /** XML tag for root element of premium SMS permissions. */
177    private static final String TAG_SMS_POLICY_BODY = "premium-sms-policy";
178
179    /** XML tag for a package. */
180    private static final String TAG_PACKAGE = "package";
181
182    /** XML attribute for the package name. */
183    private static final String ATTR_PACKAGE_NAME = "name";
184
185    /** XML attribute for the package's premium SMS permission (integer type). */
186    private static final String ATTR_PACKAGE_SMS_POLICY = "sms-policy";
187
188    /**
189     * SMS short code regex pattern matcher for a specific country.
190     */
191    private static final class ShortCodePatternMatcher {
192        private final Pattern mShortCodePattern;
193        private final Pattern mPremiumShortCodePattern;
194        private final Pattern mFreeShortCodePattern;
195        private final Pattern mStandardShortCodePattern;
196
197        ShortCodePatternMatcher(String shortCodeRegex, String premiumShortCodeRegex,
198                String freeShortCodeRegex, String standardShortCodeRegex) {
199            mShortCodePattern = (shortCodeRegex != null ? Pattern.compile(shortCodeRegex) : null);
200            mPremiumShortCodePattern = (premiumShortCodeRegex != null ?
201                    Pattern.compile(premiumShortCodeRegex) : null);
202            mFreeShortCodePattern = (freeShortCodeRegex != null ?
203                    Pattern.compile(freeShortCodeRegex) : null);
204            mStandardShortCodePattern = (standardShortCodeRegex != null ?
205                    Pattern.compile(standardShortCodeRegex) : null);
206        }
207
208        int getNumberCategory(String phoneNumber) {
209            if (mFreeShortCodePattern != null && mFreeShortCodePattern.matcher(phoneNumber)
210                    .matches()) {
211                return CATEGORY_FREE_SHORT_CODE;
212            }
213            if (mStandardShortCodePattern != null && mStandardShortCodePattern.matcher(phoneNumber)
214                    .matches()) {
215                return CATEGORY_STANDARD_SHORT_CODE;
216            }
217            if (mPremiumShortCodePattern != null && mPremiumShortCodePattern.matcher(phoneNumber)
218                    .matches()) {
219                return CATEGORY_PREMIUM_SHORT_CODE;
220            }
221            if (mShortCodePattern != null && mShortCodePattern.matcher(phoneNumber).matches()) {
222                return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE;
223            }
224            return CATEGORY_NOT_SHORT_CODE;
225        }
226    }
227
228    /**
229     * Observe the secure setting for enable flag
230     */
231    private static class SettingsObserver extends ContentObserver {
232        private final Context mContext;
233        private final AtomicBoolean mEnabled;
234
235        SettingsObserver(Handler handler, Context context, AtomicBoolean enabled) {
236            super(handler);
237            mContext = context;
238            mEnabled = enabled;
239            onChange(false);
240        }
241
242        @Override
243        public void onChange(boolean selfChange) {
244            mEnabled.set(Settings.Global.getInt(mContext.getContentResolver(),
245                    Settings.Global.SMS_SHORT_CODE_CONFIRMATION, 1) != 0);
246        }
247    }
248
249    private static class SettingsObserverHandler extends Handler {
250        SettingsObserverHandler(Context context, AtomicBoolean enabled) {
251            ContentResolver resolver = context.getContentResolver();
252            ContentObserver globalObserver = new SettingsObserver(this, context, enabled);
253            resolver.registerContentObserver(Settings.Global.getUriFor(
254                    Settings.Global.SMS_SHORT_CODE_CONFIRMATION), false, globalObserver);
255        }
256    }
257
258    /**
259     * Create SMS usage monitor.
260     * @param context the context to use to load resources and get TelephonyManager service
261     */
262    public SmsUsageMonitor(Context context) {
263        mContext = context;
264        ContentResolver resolver = context.getContentResolver();
265
266        mMaxAllowed = Settings.Global.getInt(resolver,
267                Settings.Global.SMS_OUTGOING_CHECK_MAX_COUNT,
268                DEFAULT_SMS_MAX_COUNT);
269
270        mCheckPeriod = Settings.Global.getInt(resolver,
271                Settings.Global.SMS_OUTGOING_CHECK_INTERVAL_MS,
272                DEFAULT_SMS_CHECK_PERIOD);
273
274        mSettingsObserverHandler = new SettingsObserverHandler(mContext, mCheckEnabled);
275
276        loadPremiumSmsPolicyDb();
277    }
278
279    /**
280     * Return a pattern matcher object for the specified country.
281     * @param country the country to search for
282     * @return a {@link ShortCodePatternMatcher} for the specified country, or null if not found
283     */
284    private ShortCodePatternMatcher getPatternMatcherFromFile(String country) {
285        FileReader patternReader = null;
286        XmlPullParser parser = null;
287        try {
288            patternReader = new FileReader(mPatternFile);
289            parser = Xml.newPullParser();
290            parser.setInput(patternReader);
291            return getPatternMatcherFromXmlParser(parser, country);
292        } catch (FileNotFoundException e) {
293            Log.e(TAG, "Short Code Pattern File not found");
294        } catch (XmlPullParserException e) {
295            Log.e(TAG, "XML parser exception reading short code pattern file", e);
296        } finally {
297            mPatternFileLastModified = mPatternFile.lastModified();
298            if (patternReader != null) {
299                try {
300                    patternReader.close();
301                } catch (IOException e) {}
302            }
303        }
304        return null;
305    }
306
307    private ShortCodePatternMatcher getPatternMatcherFromResource(String country) {
308        int id = com.android.internal.R.xml.sms_short_codes;
309        XmlResourceParser parser = null;
310        try {
311            parser = mContext.getResources().getXml(id);
312            return getPatternMatcherFromXmlParser(parser, country);
313        } finally {
314            if (parser != null) parser.close();
315        }
316    }
317
318    private ShortCodePatternMatcher getPatternMatcherFromXmlParser(XmlPullParser parser,
319            String country) {
320        try {
321            XmlUtils.beginDocument(parser, TAG_SHORTCODES);
322
323            while (true) {
324                XmlUtils.nextElement(parser);
325                String element = parser.getName();
326                if (element == null) {
327                    Log.e(TAG, "Parsing pattern data found null");
328                    break;
329                }
330
331                if (element.equals(TAG_SHORTCODE)) {
332                    String currentCountry = parser.getAttributeValue(null, ATTR_COUNTRY);
333                    if (VDBG) Log.d(TAG, "Found country " + currentCountry);
334                    if (country.equals(currentCountry)) {
335                        String pattern = parser.getAttributeValue(null, ATTR_PATTERN);
336                        String premium = parser.getAttributeValue(null, ATTR_PREMIUM);
337                        String free = parser.getAttributeValue(null, ATTR_FREE);
338                        String standard = parser.getAttributeValue(null, ATTR_STANDARD);
339                        return new ShortCodePatternMatcher(pattern, premium, free, standard);
340                    }
341                } else {
342                    Log.e(TAG, "Error: skipping unknown XML tag " + element);
343                }
344            }
345        } catch (XmlPullParserException e) {
346            Log.e(TAG, "XML parser exception reading short code patterns", e);
347        } catch (IOException e) {
348            Log.e(TAG, "I/O exception reading short code patterns", e);
349        }
350        if (DBG) Log.d(TAG, "Country (" + country + ") not found");
351        return null;    // country not found
352    }
353
354    /** Clear the SMS application list for disposal. */
355    void dispose() {
356        mSmsStamp.clear();
357    }
358
359    /**
360     * Check to see if an application is allowed to send new SMS messages, and confirm with
361     * user if the send limit was reached or if a non-system app is potentially sending to a
362     * premium SMS short code or number.
363     *
364     * @param appName the package name of the app requesting to send an SMS
365     * @param smsWaiting the number of new messages desired to send
366     * @return true if application is allowed to send the requested number
367     *  of new sms messages
368     */
369    public boolean check(String appName, int smsWaiting) {
370        synchronized (mSmsStamp) {
371            removeExpiredTimestamps();
372
373            ArrayList<Long> sentList = mSmsStamp.get(appName);
374            if (sentList == null) {
375                sentList = new ArrayList<Long>();
376                mSmsStamp.put(appName, sentList);
377            }
378
379            return isUnderLimit(sentList, smsWaiting);
380        }
381    }
382
383    /**
384     * Check if the destination is a possible premium short code.
385     * NOTE: the caller is expected to strip non-digits from the destination number with
386     * {@link PhoneNumberUtils#extractNetworkPortion} before calling this method.
387     * This happens in {@link SMSDispatcher#sendRawPdu} so that we use the same phone number
388     * for testing and in the user confirmation dialog if the user needs to confirm the number.
389     * This makes it difficult for malware to fool the user or the short code pattern matcher
390     * by using non-ASCII characters to make the number appear to be different from the real
391     * destination phone number.
392     *
393     * @param destAddress the destination address to test for possible short code
394     * @return {@link #CATEGORY_NOT_SHORT_CODE}, {@link #CATEGORY_FREE_SHORT_CODE},
395     *  {@link #CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE}, or {@link #CATEGORY_PREMIUM_SHORT_CODE}.
396     */
397    public int checkDestination(String destAddress, String countryIso) {
398        synchronized (mSettingsObserverHandler) {
399            // always allow emergency numbers
400            if (PhoneNumberUtils.isEmergencyNumber(destAddress, countryIso)) {
401                if (DBG) Log.d(TAG, "isEmergencyNumber");
402                return CATEGORY_NOT_SHORT_CODE;
403            }
404            // always allow if the feature is disabled
405            if (!mCheckEnabled.get()) {
406                if (DBG) Log.e(TAG, "check disabled");
407                return CATEGORY_NOT_SHORT_CODE;
408            }
409
410            if (countryIso != null) {
411                if (mCurrentCountry == null || !countryIso.equals(mCurrentCountry) ||
412                        mPatternFile.lastModified() != mPatternFileLastModified) {
413                    if (mPatternFile.exists()) {
414                        if (DBG) Log.d(TAG, "Loading SMS Short Code patterns from file");
415                        mCurrentPatternMatcher = getPatternMatcherFromFile(countryIso);
416                    } else {
417                        if (DBG) Log.d(TAG, "Loading SMS Short Code patterns from resource");
418                        mCurrentPatternMatcher = getPatternMatcherFromResource(countryIso);
419                    }
420                    mCurrentCountry = countryIso;
421                }
422            }
423
424            if (mCurrentPatternMatcher != null) {
425                return mCurrentPatternMatcher.getNumberCategory(destAddress);
426            } else {
427                // Generic rule: numbers of 5 digits or less are considered potential short codes
428                Log.e(TAG, "No patterns for \"" + countryIso + "\": using generic short code rule");
429                if (destAddress.length() <= 5) {
430                    return CATEGORY_POSSIBLE_PREMIUM_SHORT_CODE;
431                } else {
432                    return CATEGORY_NOT_SHORT_CODE;
433                }
434            }
435        }
436    }
437
438    /**
439     * Load the premium SMS policy from an XML file.
440     * Based on code from NotificationManagerService.
441     */
442    private void loadPremiumSmsPolicyDb() {
443        synchronized (mPremiumSmsPolicy) {
444            if (mPolicyFile == null) {
445                File dir = new File(SMS_POLICY_FILE_DIRECTORY);
446                mPolicyFile = new AtomicFile(new File(dir, SMS_POLICY_FILE_NAME));
447
448                mPremiumSmsPolicy.clear();
449
450                FileInputStream infile = null;
451                try {
452                    infile = mPolicyFile.openRead();
453                    final XmlPullParser parser = Xml.newPullParser();
454                    parser.setInput(infile, null);
455
456                    XmlUtils.beginDocument(parser, TAG_SMS_POLICY_BODY);
457
458                    while (true) {
459                        XmlUtils.nextElement(parser);
460
461                        String element = parser.getName();
462                        if (element == null) break;
463
464                        if (element.equals(TAG_PACKAGE)) {
465                            String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
466                            String policy = parser.getAttributeValue(null, ATTR_PACKAGE_SMS_POLICY);
467                            if (packageName == null) {
468                                Log.e(TAG, "Error: missing package name attribute");
469                            } else if (policy == null) {
470                                Log.e(TAG, "Error: missing package policy attribute");
471                            } else try {
472                                mPremiumSmsPolicy.put(packageName, Integer.parseInt(policy));
473                            } catch (NumberFormatException e) {
474                                Log.e(TAG, "Error: non-numeric policy type " + policy);
475                            }
476                        } else {
477                            Log.e(TAG, "Error: skipping unknown XML tag " + element);
478                        }
479                    }
480                } catch (FileNotFoundException e) {
481                    // No data yet
482                } catch (IOException e) {
483                    Log.e(TAG, "Unable to read premium SMS policy database", e);
484                } catch (NumberFormatException e) {
485                    Log.e(TAG, "Unable to parse premium SMS policy database", e);
486                } catch (XmlPullParserException e) {
487                    Log.e(TAG, "Unable to parse premium SMS policy database", e);
488                } finally {
489                    if (infile != null) {
490                        try {
491                            infile.close();
492                        } catch (IOException ignored) {
493                        }
494                    }
495                }
496            }
497        }
498    }
499
500    /**
501     * Persist the premium SMS policy to an XML file.
502     * Based on code from NotificationManagerService.
503     */
504    private void writePremiumSmsPolicyDb() {
505        synchronized (mPremiumSmsPolicy) {
506            FileOutputStream outfile = null;
507            try {
508                outfile = mPolicyFile.startWrite();
509
510                XmlSerializer out = new FastXmlSerializer();
511                out.setOutput(outfile, "utf-8");
512
513                out.startDocument(null, true);
514
515                out.startTag(null, TAG_SMS_POLICY_BODY);
516
517                for (Map.Entry<String, Integer> policy : mPremiumSmsPolicy.entrySet()) {
518                    out.startTag(null, TAG_PACKAGE);
519                    out.attribute(null, ATTR_PACKAGE_NAME, policy.getKey());
520                    out.attribute(null, ATTR_PACKAGE_SMS_POLICY, policy.getValue().toString());
521                    out.endTag(null, TAG_PACKAGE);
522                }
523
524                out.endTag(null, TAG_SMS_POLICY_BODY);
525                out.endDocument();
526
527                mPolicyFile.finishWrite(outfile);
528            } catch (IOException e) {
529                Log.e(TAG, "Unable to write premium SMS policy database", e);
530                if (outfile != null) {
531                    mPolicyFile.failWrite(outfile);
532                }
533            }
534        }
535    }
536
537    /**
538     * Returns the premium SMS permission for the specified package. If the package has never
539     * been seen before, the default {@link #PREMIUM_SMS_PERMISSION_ASK_USER}
540     * will be returned.
541     * @param packageName the name of the package to query permission
542     * @return one of {@link #PREMIUM_SMS_PERMISSION_UNKNOWN},
543     *  {@link #PREMIUM_SMS_PERMISSION_ASK_USER},
544     *  {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
545     *  {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
546     * @throws SecurityException if the caller is not a system process
547     */
548    public int getPremiumSmsPermission(String packageName) {
549        checkCallerIsSystemOrSameApp(packageName);
550        synchronized (mPremiumSmsPolicy) {
551            Integer policy = mPremiumSmsPolicy.get(packageName);
552            if (policy == null) {
553                return PREMIUM_SMS_PERMISSION_UNKNOWN;
554            } else {
555                return policy;
556            }
557        }
558    }
559
560    /**
561     * Sets the premium SMS permission for the specified package and save the value asynchronously
562     * to persistent storage.
563     * @param packageName the name of the package to set permission
564     * @param permission one of {@link #PREMIUM_SMS_PERMISSION_ASK_USER},
565     *  {@link #PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
566     *  {@link #PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
567     * @throws SecurityException if the caller is not a system process
568     */
569    public void setPremiumSmsPermission(String packageName, int permission) {
570        checkCallerIsSystemOrPhoneApp();
571        if (permission < PREMIUM_SMS_PERMISSION_ASK_USER
572                || permission > PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW) {
573            throw new IllegalArgumentException("invalid SMS permission type " + permission);
574        }
575        synchronized (mPremiumSmsPolicy) {
576            mPremiumSmsPolicy.put(packageName, permission);
577        }
578        // write policy file in the background
579        new Thread(new Runnable() {
580            @Override
581            public void run() {
582                writePremiumSmsPolicyDb();
583            }
584        }).start();
585    }
586
587    private static void checkCallerIsSystemOrSameApp(String pkg) {
588        int uid = Binder.getCallingUid();
589        if (UserHandle.getAppId(uid) == Process.SYSTEM_UID || uid == 0) {
590            return;
591        }
592        try {
593            ApplicationInfo ai = AppGlobals.getPackageManager().getApplicationInfo(
594                    pkg, 0, UserHandle.getCallingUserId());
595            if (!UserHandle.isSameApp(ai.uid, uid)) {
596                throw new SecurityException("Calling uid " + uid + " gave package"
597                        + pkg + " which is owned by uid " + ai.uid);
598            }
599        } catch (RemoteException re) {
600            throw new SecurityException("Unknown package " + pkg + "\n" + re);
601        }
602    }
603
604    private static void checkCallerIsSystemOrPhoneApp() {
605        int uid = Binder.getCallingUid();
606        int appId = UserHandle.getAppId(uid);
607        if (appId == Process.SYSTEM_UID || appId == Process.PHONE_UID || uid == 0) {
608            return;
609        }
610        throw new SecurityException("Disallowed call for uid " + uid);
611    }
612
613    /**
614     * Remove keys containing only old timestamps. This can happen if an SMS app is used
615     * to send messages and then uninstalled.
616     */
617    private void removeExpiredTimestamps() {
618        long beginCheckPeriod = System.currentTimeMillis() - mCheckPeriod;
619
620        synchronized (mSmsStamp) {
621            Iterator<Map.Entry<String, ArrayList<Long>>> iter = mSmsStamp.entrySet().iterator();
622            while (iter.hasNext()) {
623                Map.Entry<String, ArrayList<Long>> entry = iter.next();
624                ArrayList<Long> oldList = entry.getValue();
625                if (oldList.isEmpty() || oldList.get(oldList.size() - 1) < beginCheckPeriod) {
626                    iter.remove();
627                }
628            }
629        }
630    }
631
632    private boolean isUnderLimit(ArrayList<Long> sent, int smsWaiting) {
633        Long ct = System.currentTimeMillis();
634        long beginCheckPeriod = ct - mCheckPeriod;
635
636        if (VDBG) log("SMS send size=" + sent.size() + " time=" + ct);
637
638        while (!sent.isEmpty() && sent.get(0) < beginCheckPeriod) {
639            sent.remove(0);
640        }
641
642        if ((sent.size() + smsWaiting) <= mMaxAllowed) {
643            for (int i = 0; i < smsWaiting; i++ ) {
644                sent.add(ct);
645            }
646            return true;
647        }
648        return false;
649    }
650
651    private static void log(String msg) {
652        Log.d(TAG, msg);
653    }
654}
655