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