| /* |
| * Copyright (C) 2015 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.messaging.receiver; |
| |
| import android.app.Notification; |
| import android.app.PendingIntent; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.provider.Telephony; |
| import android.provider.Telephony.Sms; |
| import androidx.core.app.NotificationCompat; |
| import androidx.core.app.NotificationCompat.Builder; |
| import androidx.core.app.NotificationCompat.Style; |
| import androidx.core.app.NotificationManagerCompat; |
| |
| import java.util.ArrayList; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| import com.android.messaging.Factory; |
| import com.android.messaging.R; |
| import com.android.messaging.datamodel.BugleNotifications; |
| import com.android.messaging.datamodel.MessageNotificationState; |
| import com.android.messaging.datamodel.NoConfirmationSmsSendService; |
| import com.android.messaging.datamodel.action.ReceiveSmsMessageAction; |
| import com.android.messaging.sms.MmsUtils; |
| import com.android.messaging.ui.UIIntents; |
| import com.android.messaging.util.BugleGservices; |
| import com.android.messaging.util.BugleGservicesKeys; |
| import com.android.messaging.util.DebugUtils; |
| import com.android.messaging.util.LogUtil; |
| import com.android.messaging.util.NotificationsUtil; |
| import com.android.messaging.util.OsUtil; |
| import com.android.messaging.util.PendingIntentConstants; |
| import com.android.messaging.util.PhoneUtils; |
| |
| /** |
| * Class that receives incoming SMS messages through android.provider.Telephony.SMS_RECEIVED |
| * |
| * This class serves two purposes: |
| * - Process phone verification SMS messages |
| * - Handle SMS messages when the user has enabled us to be the default SMS app (Pre-KLP) |
| */ |
| public final class SmsReceiver extends BroadcastReceiver { |
| private static final String TAG = LogUtil.BUGLE_TAG; |
| |
| private static ArrayList<Pattern> sIgnoreSmsPatterns; |
| |
| /** |
| * Enable or disable the SmsReceiver as appropriate. Pre-KLP we use this receiver for |
| * receiving incoming SMS messages. For KLP+ this receiver is not used when running as the |
| * primary user and the SmsDeliverReceiver is used for receiving incoming SMS messages. |
| * When running as a secondary user, this receiver is still used to trigger the incoming |
| * notification. |
| */ |
| public static void updateSmsReceiveHandler(final Context context) { |
| boolean smsReceiverEnabled; |
| boolean mmsWapPushReceiverEnabled; |
| boolean respondViaMessageEnabled; |
| boolean broadcastAbortEnabled; |
| |
| if (OsUtil.isAtLeastKLP()) { |
| // When we're running as the secondary user, we don't get the new SMS_DELIVER intent, |
| // only the primary user receives that. As secondary, we need to go old-school and |
| // listen for the SMS_RECEIVED intent. For the secondary user, use this SmsReceiver |
| // for both sms and mms notification. For the primary user on KLP (and above), we don't |
| // use the SmsReceiver. |
| smsReceiverEnabled = OsUtil.isSecondaryUser(); |
| // On KLP use the new deliver event for mms |
| mmsWapPushReceiverEnabled = false; |
| // On KLP we need to always enable this handler to show in the list of sms apps |
| respondViaMessageEnabled = true; |
| // On KLP we don't need to abort the broadcast |
| broadcastAbortEnabled = false; |
| } else { |
| // On JB we use the sms receiver for both sms/mms delivery |
| final boolean carrierSmsEnabled = PhoneUtils.getDefault().isSmsEnabled(); |
| smsReceiverEnabled = carrierSmsEnabled; |
| |
| // On JB we use the mms receiver when sms/mms is enabled |
| mmsWapPushReceiverEnabled = carrierSmsEnabled; |
| // On JB this is dynamic to make sure we don't show in dialer if sms is disabled |
| respondViaMessageEnabled = carrierSmsEnabled; |
| // On JB we need to abort broadcasts if SMS is enabled |
| broadcastAbortEnabled = carrierSmsEnabled; |
| } |
| |
| final PackageManager packageManager = context.getPackageManager(); |
| final boolean logv = LogUtil.isLoggable(TAG, LogUtil.VERBOSE); |
| if (smsReceiverEnabled) { |
| if (logv) { |
| LogUtil.v(TAG, "Enabling SMS message receiving"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, SmsReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); |
| |
| } else { |
| if (logv) { |
| LogUtil.v(TAG, "Disabling SMS message receiving"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, SmsReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); |
| } |
| if (mmsWapPushReceiverEnabled) { |
| if (logv) { |
| LogUtil.v(TAG, "Enabling MMS message receiving"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, MmsWapPushReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); |
| } else { |
| if (logv) { |
| LogUtil.v(TAG, "Disabling MMS message receiving"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, MmsWapPushReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); |
| } |
| if (broadcastAbortEnabled) { |
| if (logv) { |
| LogUtil.v(TAG, "Enabling SMS/MMS broadcast abort"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, AbortSmsReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, AbortMmsWapPushReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); |
| } else { |
| if (logv) { |
| LogUtil.v(TAG, "Disabling SMS/MMS broadcast abort"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, AbortSmsReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, AbortMmsWapPushReceiver.class), |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); |
| } |
| if (respondViaMessageEnabled) { |
| if (logv) { |
| LogUtil.v(TAG, "Enabling respond via message intent"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, NoConfirmationSmsSendService.class), |
| PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP); |
| } else { |
| if (logv) { |
| LogUtil.v(TAG, "Disabling respond via message intent"); |
| } |
| packageManager.setComponentEnabledSetting( |
| new ComponentName(context, NoConfirmationSmsSendService.class), |
| PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); |
| } |
| } |
| |
| private static final String EXTRA_ERROR_CODE = "errorCode"; |
| private static final String EXTRA_SUB_ID = "subscription"; |
| |
| public static void deliverSmsIntent(final Context context, final Intent intent) { |
| final android.telephony.SmsMessage[] messages = getMessagesFromIntent(intent); |
| |
| // Check messages for validity |
| if (messages == null || messages.length < 1) { |
| LogUtil.e(TAG, "processReceivedSms: null or zero or ignored message"); |
| return; |
| } |
| |
| final int errorCode = |
| intent.getIntExtra(EXTRA_ERROR_CODE, SendStatusReceiver.NO_ERROR_CODE); |
| // Always convert negative subIds into -1 |
| int subId = PhoneUtils.getDefault().getEffectiveIncomingSubIdFromSystem( |
| intent, EXTRA_SUB_ID); |
| deliverSmsMessages(context, subId, errorCode, messages); |
| if (MmsUtils.isDumpSmsEnabled()) { |
| final String format = intent.getStringExtra("format"); |
| DebugUtils.dumpSms(messages[0].getTimestampMillis(), messages, format); |
| } |
| } |
| |
| public static void deliverSmsMessages(final Context context, final int subId, |
| final int errorCode, final android.telephony.SmsMessage[] messages) { |
| final ContentValues messageValues = |
| MmsUtils.parseReceivedSmsMessage(context, messages, errorCode); |
| |
| LogUtil.v(TAG, "SmsReceiver.deliverSmsMessages"); |
| |
| final long nowInMillis = System.currentTimeMillis(); |
| final long receivedTimestampMs = MmsUtils.getMessageDate(messages[0], nowInMillis); |
| |
| messageValues.put(Sms.Inbox.DATE, receivedTimestampMs); |
| // Default to unread and unseen for us but ReceiveSmsMessageAction will override |
| // seen for the telephony db. |
| messageValues.put(Sms.Inbox.READ, 0); |
| messageValues.put(Sms.Inbox.SEEN, 0); |
| if (OsUtil.isAtLeastL_MR1()) { |
| messageValues.put(Sms.SUBSCRIPTION_ID, subId); |
| } |
| |
| if (messages[0].getMessageClass() == android.telephony.SmsMessage.MessageClass.CLASS_0 || |
| DebugUtils.debugClassZeroSmsEnabled()) { |
| Factory.get().getUIIntents().launchClassZeroActivity(context, messageValues); |
| } else { |
| final ReceiveSmsMessageAction action = new ReceiveSmsMessageAction(messageValues); |
| action.start(); |
| } |
| } |
| |
| @Override |
| public void onReceive(final Context context, final Intent intent) { |
| LogUtil.v(TAG, "SmsReceiver.onReceive " + intent); |
| // On KLP+ we only take delivery of SMS messages in SmsDeliverReceiver. |
| if (PhoneUtils.getDefault().isSmsEnabled()) { |
| final String action = intent.getAction(); |
| if (OsUtil.isSecondaryUser() && |
| (Telephony.Sms.Intents.SMS_RECEIVED_ACTION.equals(action) || |
| // TODO: update this with the actual constant from Telephony |
| "android.provider.Telephony.MMS_DOWNLOADED".equals(action))) { |
| postNewMessageSecondaryUserNotification(); |
| } else if (!OsUtil.isAtLeastKLP()) { |
| deliverSmsIntent(context, intent); |
| } |
| } |
| } |
| |
| private static class SecondaryUserNotificationState extends MessageNotificationState { |
| SecondaryUserNotificationState() { |
| super(null); |
| } |
| |
| @Override |
| protected Style build(Builder builder) { |
| return null; |
| } |
| } |
| |
| public static void postNewMessageSecondaryUserNotification() { |
| final Context context = Factory.get().getApplicationContext(); |
| final Resources resources = context.getResources(); |
| final PendingIntent pendingIntent = UIIntents.get() |
| .getPendingIntentForSecondaryUserNewMessageNotification(context); |
| |
| final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, |
| NotificationsUtil.DEFAULT_CHANNEL_ID); |
| builder.setContentTitle(resources.getString(R.string.secondary_user_new_message_title)) |
| .setTicker(resources.getString(R.string.secondary_user_new_message_ticker)) |
| .setSmallIcon(R.drawable.ic_sms_light) |
| // Returning PRIORITY_HIGH causes L to put up a HUD notification. Without it, the ticker |
| // isn't displayed. |
| .setPriority(Notification.PRIORITY_HIGH) |
| .setContentIntent(pendingIntent); |
| |
| final NotificationCompat.BigTextStyle bigTextStyle = |
| new NotificationCompat.BigTextStyle(builder); |
| bigTextStyle.bigText(resources.getString(R.string.secondary_user_new_message_title)); |
| final Notification notification = bigTextStyle.build(); |
| |
| final NotificationManagerCompat notificationManager = |
| NotificationManagerCompat.from(Factory.get().getApplicationContext()); |
| |
| int defaults = Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE; |
| notification.defaults = defaults; |
| |
| notificationManager.notify(getNotificationTag(), |
| PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID, notification); |
| } |
| |
| /** |
| * Cancel the notification |
| */ |
| public static void cancelSecondaryUserNotification() { |
| final NotificationManagerCompat notificationManager = |
| NotificationManagerCompat.from(Factory.get().getApplicationContext()); |
| notificationManager.cancel(getNotificationTag(), |
| PendingIntentConstants.SMS_SECONDARY_USER_NOTIFICATION_ID); |
| } |
| |
| private static String getNotificationTag() { |
| return Factory.get().getApplicationContext().getPackageName() + ":secondaryuser"; |
| } |
| |
| /** |
| * Compile all of the patterns we check for to ignore system SMS messages. |
| */ |
| private static void compileIgnoreSmsPatterns() { |
| // Get the pattern set from GServices |
| final String smsIgnoreRegex = BugleGservices.get().getString( |
| BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX, |
| BugleGservicesKeys.SMS_IGNORE_MESSAGE_REGEX_DEFAULT); |
| if (smsIgnoreRegex != null) { |
| final String[] ignoreSmsExpressions = smsIgnoreRegex.split("\n"); |
| if (ignoreSmsExpressions.length != 0) { |
| sIgnoreSmsPatterns = new ArrayList<Pattern>(); |
| for (int i = 0; i < ignoreSmsExpressions.length; i++) { |
| try { |
| sIgnoreSmsPatterns.add(Pattern.compile(ignoreSmsExpressions[i])); |
| } catch (PatternSyntaxException e) { |
| LogUtil.e(TAG, "compileIgnoreSmsPatterns: Skipping bad expression: " + |
| ignoreSmsExpressions[i]); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Get the SMS messages from the specified SMS intent. |
| * @return the messages. If there is an error or the message should be ignored, return null. |
| */ |
| public static android.telephony.SmsMessage[] getMessagesFromIntent(Intent intent) { |
| final android.telephony.SmsMessage[] messages = Sms.Intents.getMessagesFromIntent(intent); |
| |
| // Check messages for validity |
| if (messages == null || messages.length < 1) { |
| return null; |
| } |
| // Sometimes, SmsMessage.mWrappedSmsMessage is null causing NPE when we access |
| // the methods on it although the SmsMessage itself is not null. So do this check |
| // before we do anything on the parsed SmsMessages. |
| try { |
| final String messageBody = messages[0].getDisplayMessageBody(); |
| if (messageBody != null) { |
| // Compile patterns if necessary |
| if (sIgnoreSmsPatterns == null) { |
| compileIgnoreSmsPatterns(); |
| } |
| // Check against filters |
| for (final Pattern pattern : sIgnoreSmsPatterns) { |
| if (pattern.matcher(messageBody).matches()) { |
| return null; |
| } |
| } |
| } |
| } catch (final NullPointerException e) { |
| LogUtil.e(TAG, "shouldIgnoreMessage: NPE inside SmsMessage"); |
| return null; |
| } |
| return messages; |
| } |
| |
| |
| /** |
| * Check the specified SMS intent to see if the message should be ignored |
| * @return true if the message should be ignored |
| */ |
| public static boolean shouldIgnoreMessage(Intent intent) { |
| return getMessagesFromIntent(intent) == null; |
| } |
| } |