1/*
2 * Copyright (C) 2015 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.messaging.receiver;
18
19import android.app.Notification;
20import android.app.PendingIntent;
21import android.content.BroadcastReceiver;
22import android.content.ComponentName;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.res.Resources;
28import android.provider.Telephony;
29import android.provider.Telephony.Sms;
30import android.support.v4.app.NotificationCompat;
31import android.support.v4.app.NotificationCompat.Builder;
32import android.support.v4.app.NotificationCompat.Style;
33import android.support.v4.app.NotificationManagerCompat;
34
35import java.util.ArrayList;
36import java.util.regex.Pattern;
37import java.util.regex.PatternSyntaxException;
38
39import com.android.messaging.Factory;
40import com.android.messaging.R;
41import com.android.messaging.datamodel.BugleNotifications;
42import com.android.messaging.datamodel.MessageNotificationState;
43import com.android.messaging.datamodel.NoConfirmationSmsSendService;
44import com.android.messaging.datamodel.action.ReceiveSmsMessageAction;
45import com.android.messaging.sms.MmsUtils;
46import com.android.messaging.ui.UIIntents;
47import com.android.messaging.util.BugleGservices;
48import com.android.messaging.util.BugleGservicesKeys;
49import com.android.messaging.util.DebugUtils;
50import com.android.messaging.util.LogUtil;
51import com.android.messaging.util.OsUtil;
52import com.android.messaging.util.PendingIntentConstants;
53import com.android.messaging.util.PhoneUtils;
54
55/**
56 * Class that receives incoming SMS messages through android.provider.Telephony.SMS_RECEIVED
57 *
58 * This class serves two purposes:
59 * - Process phone verification SMS messages
60 * - Handle SMS messages when the user has enabled us to be the default SMS app (Pre-KLP)
61 */
62public final class SmsReceiver extends BroadcastReceiver {
63    private static final String TAG = LogUtil.BUGLE_TAG;
64
65    private static ArrayList<Pattern> sIgnoreSmsPatterns;
66
67    /**
68     * Enable or disable the SmsReceiver as appropriate. Pre-KLP we use this receiver for
69     * receiving incoming SMS messages. For KLP+ this receiver is not used when running as the
70     * primary user and the SmsDeliverReceiver is used for receiving incoming SMS messages.
71     * When running as a secondary user, this receiver is still used to trigger the incoming
72     * notification.
73     */
74    public static void updateSmsReceiveHandler(final Context context) {
75        boolean smsReceiverEnabled;
76        boolean mmsWapPushReceiverEnabled;
77        boolean respondViaMessageEnabled;
78        boolean broadcastAbortEnabled;
79
80        if (OsUtil.isAtLeastKLP()) {
81            // When we're running as the secondary user, we don't get the new SMS_DELIVER intent,
82            // only the primary user receives that. As secondary, we need to go old-school and
83            // listen for the SMS_RECEIVED intent. For the secondary user, use this SmsReceiver
84            // for both sms and mms notification. For the primary user on KLP (and above), we don't
85            // use the SmsReceiver.
86            smsReceiverEnabled = OsUtil.isSecondaryUser();
87            // On KLP use the new deliver event for mms
88            mmsWapPushReceiverEnabled = false;
89            // On KLP we need to always enable this handler to show in the list of sms apps
90            respondViaMessageEnabled = true;
91            // On KLP we don't need to abort the broadcast
92            broadcastAbortEnabled = false;
93        } else {
94            // On JB we use the sms receiver for both sms/mms delivery
95            final boolean carrierSmsEnabled = PhoneUtils.getDefault().isSmsEnabled();
96            smsReceiverEnabled = carrierSmsEnabled;
97
98            // On JB we use the mms receiver when sms/mms is enabled
99            mmsWapPushReceiverEnabled = carrierSmsEnabled;
100            // On JB this is dynamic to make sure we don't show in dialer if sms is disabled
101            respondViaMessageEnabled = carrierSmsEnabled;
102            // On JB we need to abort broadcasts if SMS is enabled
103            broadcastAbortEnabled = carrierSmsEnabled;
104        }
105
106        final PackageManager packageManager = context.getPackageManager();
107        final boolean logv = LogUtil.isLoggable(TAG, LogUtil.VERBOSE);
108        if (smsReceiverEnabled) {
109            if (logv) {
110                LogUtil.v(TAG, "Enabling SMS message receiving");
111            }
112            packageManager.setComponentEnabledSetting(
113                    new ComponentName(context, SmsReceiver.class),
114                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
115
116        } else {
117            if (logv) {
118                LogUtil.v(TAG, "Disabling SMS message receiving");
119            }
120            packageManager.setComponentEnabledSetting(
121                    new ComponentName(context, SmsReceiver.class),
122                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
123        }
124        if (mmsWapPushReceiverEnabled) {
125            if (logv) {
126                LogUtil.v(TAG, "Enabling MMS message receiving");
127            }
128            packageManager.setComponentEnabledSetting(
129                    new ComponentName(context, MmsWapPushReceiver.class),
130                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
131        } else {
132            if (logv) {
133                LogUtil.v(TAG, "Disabling MMS message receiving");
134            }
135            packageManager.setComponentEnabledSetting(
136                    new ComponentName(context, MmsWapPushReceiver.class),
137                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
138        }
139        if (broadcastAbortEnabled) {
140            if (logv) {
141                LogUtil.v(TAG, "Enabling SMS/MMS broadcast abort");
142            }
143            packageManager.setComponentEnabledSetting(
144                    new ComponentName(context, AbortSmsReceiver.class),
145                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
146            packageManager.setComponentEnabledSetting(
147                    new ComponentName(context, AbortMmsWapPushReceiver.class),
148                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
149        } else {
150            if (logv) {
151                LogUtil.v(TAG, "Disabling SMS/MMS broadcast abort");
152            }
153            packageManager.setComponentEnabledSetting(
154                    new ComponentName(context, AbortSmsReceiver.class),
155                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
156            packageManager.setComponentEnabledSetting(
157                    new ComponentName(context, AbortMmsWapPushReceiver.class),
158                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
159        }
160        if (respondViaMessageEnabled) {
161            if (logv) {
162                LogUtil.v(TAG, "Enabling respond via message intent");
163            }
164            packageManager.setComponentEnabledSetting(
165                    new ComponentName(context, NoConfirmationSmsSendService.class),
166                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
167        } else {
168            if (logv) {
169                LogUtil.v(TAG, "Disabling respond via message intent");
170            }
171            packageManager.setComponentEnabledSetting(
172                    new ComponentName(context, NoConfirmationSmsSendService.class),
173                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
174        }
175    }
176
177    private static final String EXTRA_ERROR_CODE = "errorCode";
178    private static final String EXTRA_SUB_ID = "subscription";
179
180    public static void deliverSmsIntent(final Context context, final Intent intent) {
181        final android.telephony.SmsMessage[] messages = getMessagesFromIntent(intent);
182
183        // Check messages for validity
184        if (messages == null || messages.length < 1) {
185            LogUtil.e(TAG, "processReceivedSms: null or zero or ignored message");
186            return;
187        }
188
189        final int errorCode = intent.getIntExtra(EXTRA_ERROR_CODE, 0);
190        // Always convert negative subIds into -1
191        int subId = PhoneUtils.getDefault().getEffectiveIncomingSubIdFromSystem(
192                intent, EXTRA_SUB_ID);
193        deliverSmsMessages(context, subId, errorCode, messages);
194        if (MmsUtils.isDumpSmsEnabled()) {
195            final String format = null;
196            DebugUtils.dumpSms(messages[0].getTimestampMillis(), messages, format);
197        }
198    }
199
200    public static void deliverSmsMessages(final Context context, final int subId,
201            final int errorCode, final android.telephony.SmsMessage[] messages) {
202        final ContentValues messageValues =
203                MmsUtils.parseReceivedSmsMessage(context, messages, errorCode);
204
205        LogUtil.v(TAG, "SmsReceiver.deliverSmsMessages");
206
207        final long nowInMillis =  System.currentTimeMillis();
208        final long receivedTimestampMs = MmsUtils.getMessageDate(messages[0], nowInMillis);
209
210        messageValues.put(Sms.Inbox.DATE, receivedTimestampMs);
211        // Default to unread and unseen for us but ReceiveSmsMessageAction will override
212        // seen for the telephony db.
213        messageValues.put(Sms.Inbox.READ, 0);
214        messageValues.put(Sms.Inbox.SEEN, 0);
215        if (OsUtil.isAtLeastL_MR1()) {
216            messageValues.put(Sms.SUBSCRIPTION_ID, subId);
217        }
218
219        if (messages[0].getMessageClass() == android.telephony.SmsMessage.MessageClass.CLASS_0 ||
220                DebugUtils.debugClassZeroSmsEnabled()) {
221            Factory.get().getUIIntents().launchClassZeroActivity(context, messageValues);
222        } else {
223            final ReceiveSmsMessageAction action = new ReceiveSmsMessageAction(messageValues);
224            action.start();
225        }
226    }
227
228    @Override
229    public void onReceive(final Context context, final Intent intent) {
230        LogUtil.v(TAG, "SmsReceiver.onReceive " + intent);
231        // On KLP+ we only take delivery of SMS messages in SmsDeliverReceiver.
232        if (PhoneUtils.getDefault().isSmsEnabled()) {
233            final String action = intent.getAction();
234            if (OsUtil.isSecondaryUser() &&
235                    (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action) ||
236                            // TODO: update this with the actual constant from Telephony
237                            "android.provider.Telephony.MMS_DOWNLOADED".equals(action))) {
238                postNewMessageSecondaryUserNotification();
239            } else if (!OsUtil.isAtLeastKLP()) {
240                deliverSmsIntent(context, intent);
241            }
242        }
243    }
244
245    private static class SecondaryUserNotificationState extends MessageNotificationState {
246        SecondaryUserNotificationState() {
247            super(null);
248        }
249
250        @Override
251        protected Style build(Builder builder) {
252            return null;
253        }
254
255        @Override
256        public boolean getNotificationVibrate() {
257            return true;
258        }
259    }
260
261    public static void postNewMessageSecondaryUserNotification() {
262        final Context context = Factory.get().getApplicationContext();
263        final Resources resources = context.getResources();
264        final PendingIntent pendingIntent = UIIntents.get()
265                .getPendingIntentForSecondaryUserNewMessageNotification(context);
266
267        final NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
268        builder.setContentTitle(resources.getString(R.string.secondary_user_new_message_title))
269                .setTicker(resources.getString(R.string.secondary_user_new_message_ticker))
270                .setSmallIcon(R.drawable.ic_sms_light)
271        // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker
272        // isn't displayed.
273                .setPriority(Notification.PRIORITY_HIGH)
274                .setContentIntent(pendingIntent);
275
276        final NotificationCompat.BigTextStyle bigTextStyle =
277                new NotificationCompat.BigTextStyle(builder);
278        bigTextStyle.bigText(resources.getString(R.string.secondary_user_new_message_title));
279        final Notification notification = bigTextStyle.build();
280
281        final NotificationManagerCompat notificationManager =
282                NotificationManagerCompat.from(Factory.get().getApplicationContext());
283
284        int defaults = Notification.DEFAULT_LIGHTS;
285        if (BugleNotifications.shouldVibrate(new SecondaryUserNotificationState())) {
286            defaults |= Notification.DEFAULT_VIBRATE;
287        }
288        notification.defaults = defaults;
289
290        notificationManager.notify(getNotificationTag(),
291                PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, notification);
292    }
293
294    /**
295     * Cancel the notification
296     */
297    public static void cancelSecondaryUserNotification() {
298        final NotificationManagerCompat notificationManager =
299                NotificationManagerCompat.from(Factory.get().getApplicationContext());
300        notificationManager.cancel(getNotificationTag(),
301                PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID);
302    }
303
304    private static String getNotificationTag() {
305        return Factory.get().getApplicationContext().getPackageName() + ":secondaryuser";
306    }
307
308    /**
309     * Compile all of the patterns we check for to ignore system SMS messages.
310     */
311    private static void compileIgnoreSmsPatterns() {
312        // Get the pattern set from GServices
313        final String smsIgnoreRegex = BugleGservices.get().getString(
314                BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX,
315                BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT);
316        if (smsIgnoreRegex != null) {
317            final String[] ignoreSmsExpressions = smsIgnoreRegex.split("\n");
318            if (ignoreSmsExpressions.length != 0) {
319                sIgnoreSmsPatterns = new ArrayList<Pattern>();
320                for (int i = 0; i < ignoreSmsExpressions.length; i++) {
321                    try {
322                        sIgnoreSmsPatterns.add(Pattern.compile(ignoreSmsExpressions[i]));
323                    } catch (PatternSyntaxException e) {
324                        LogUtil.e(TAG, "compileIgnoreSmsPatterns: Skipping bad expression: " +
325                                ignoreSmsExpressions[i]);
326                    }
327                }
328            }
329        }
330    }
331
332    /**
333     * Get the SMS messages from the specified SMS intent.
334     * @return the messages. If there is an error or the message should be ignored, return null.
335     */
336    public static android.telephony.SmsMessage[] getMessagesFromIntent(Intent intent) {
337        final android.telephony.SmsMessage[] messages = Sms.Intents.getMessagesFromIntent(intent);
338
339        // Check messages for validity
340        if (messages == null || messages.length < 1) {
341            return null;
342        }
343        // Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access
344        // the methods on it although the SmsMessage itself is not null. So do this check
345        // before we do anything on the parsed SmsMessages.
346        try {
347            final String messageBody = messages[0].getDisplayMessageBody();
348            if (messageBody != null) {
349                // Compile patterns if necessary
350                if (sIgnoreSmsPatterns == null) {
351                    compileIgnoreSmsPatterns();
352                }
353                // Check against filters
354                for (final Pattern pattern : sIgnoreSmsPatterns) {
355                    if (pattern.matcher(messageBody).matches()) {
356                        return null;
357                    }
358                }
359            }
360        } catch (final NullPointerException e) {
361            LogUtil.e(TAG, "shouldIgnoreMessage: NPE inside SmsMessage");
362            return null;
363        }
364        return messages;
365    }
366
367
368    /**
369     * Check the specified SMS intent to see if the message should be ignored
370     * @return true if the message should be ignored
371     */
372    public static boolean shouldIgnoreMessage(Intent intent) {
373        return getMessagesFromIntent(intent) == null;
374    }
375}
376