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