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