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