/* * Copyright (C) 2016 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.internal.telephony; import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.provider.VoicemailContract; import android.telecom.PhoneAccountHandle; import android.telephony.PhoneNumberUtils; import android.telephony.SmsMessage; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.telephony.VisualVoicemailSms; import android.telephony.VisualVoicemailSmsFilterSettings; import android.util.ArrayMap; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData; import java.nio.ByteBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; /** * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual * dispatching. */ public class VisualVoicemailSmsFilter { /** * Interface to convert subIds so the logic can be replaced in tests. */ @VisibleForTesting public interface PhoneAccountHandleConverter { /** * Convert the subId to a {@link PhoneAccountHandle} */ PhoneAccountHandle fromSubId(int subId); } private static final String TAG = "VvmSmsFilter"; private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone"; private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT = new ComponentName("com.android.phone", "com.android.services.telephony.TelephonyConnectionService"); private static Map> sPatterns; private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER = new PhoneAccountHandleConverter() { @Override public PhoneAccountHandle fromSubId(int subId) { if (!SubscriptionManager.isValidSubscriptionId(subId)) { return null; } int phoneId = SubscriptionManager.getPhoneId(subId); if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) { return null; } return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT, PhoneFactory.getPhone(phoneId).getFullIccSerialNumber()); } }; private static PhoneAccountHandleConverter sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; /** * Wrapper to combine multiple PDU into an SMS message */ private static class FullMessage { public SmsMessage firstMessage; public String fullMessageBody; } /** * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony * service, and the SMS will be dropped. * *

The accepted format for a visual voicemail SMS is a generalization of the OMTP format: * *

[clientPrefix]:[prefix]:([key]=[value];)* * * Additionally, if the SMS does not match the format, but matches the regex specified by the * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent. * * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped */ public static boolean filter(Context context, byte[][] pdus, String format, int destPort, int subId) { TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); VisualVoicemailSmsFilterSettings settings; settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId); if (settings == null) { return false; } PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId); if (phoneAccountHandle == null) { Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle"); return false; } FullMessage fullMessage = getFullMessage(pdus, format); if (fullMessage == null) { // Carrier WAP push SMS is not recognized by android, which has a ascii PDU. // Attempt to parse it. Log.i(TAG, "Unparsable SMS received"); String asciiMessage = parseAsciiPduMessage(pdus); WrappedMessageData messageData = VisualVoicemailSmsParser .parseAlternativeFormat(asciiMessage); if (messageData != null) { sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); } // Confidence for what the message actually is is low. Don't remove the message and let // system decide. Usually because it is not parsable it will be dropped. return false; } String messageBody = fullMessage.fullMessageBody; String clientPrefix = settings.clientPrefix; WrappedMessageData messageData = VisualVoicemailSmsParser .parse(clientPrefix, messageBody); if (messageData != null) { if (settings.destinationPort == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) { if (destPort == -1) { // Non-data SMS is directed to the port "-1". Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS"); return false; } } else if (settings.destinationPort != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) { if (settings.destinationPort != destPort) { Log.i(TAG, "SMS matching VVM format received but is not directed to port " + settings.destinationPort); return false; } } if (!settings.originatingNumbers.isEmpty() && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) { Log.i(TAG, "SMS matching VVM format received but is not from originating numbers"); return false; } sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); return true; } buildPatternsMap(context); String mccMnc = telephonyManager.getSimOperator(subId); List patterns = sPatterns.get(mccMnc); if (patterns == null || patterns.isEmpty()) { return false; } for (Pattern pattern : patterns) { if (pattern.matcher(messageBody).matches()) { Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, " + "still dropping as VVM SMS"); sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody); return true; } } return false; } /** * override how subId is converted to PhoneAccountHandle for tests */ @VisibleForTesting public static void setPhoneAccountHandleConverterForTest( PhoneAccountHandleConverter converter) { if (converter == null) { sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; } else { sPhoneAccountHandleConverter = converter; } } private static void buildPatternsMap(Context context) { if (sPatterns != null) { return; } sPatterns = new ArrayMap<>(); // TODO(twyen): build from CarrierConfig once public API can be updated. for (String entry : context.getResources() .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) { String[] mccMncList = entry.split(";")[0].split(","); Pattern pattern = Pattern.compile(entry.split(";")[1]); for (String mccMnc : mccMncList) { if (!sPatterns.containsKey(mccMnc)) { sPatterns.put(mccMnc, new ArrayList<>()); } sPatterns.get(mccMnc).add(pattern); } } } private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle, @Nullable WrappedMessageData messageData, @Nullable String messageBody) { Log.i(TAG, "VVM SMS received"); Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED); VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder(); if (messageData != null) { builder.setPrefix(messageData.prefix); builder.setFields(messageData.fields); } if (messageBody != null) { builder.setMessageBody(messageBody); } builder.setPhoneAccountHandle(phoneAccountHandle); intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build()); intent.setPackage(TELEPHONY_SERVICE_PACKAGE); context.sendBroadcast(intent); } /** * @return the message body of the SMS, or {@code null} if it can not be parsed. */ @Nullable private static FullMessage getFullMessage(byte[][] pdus, String format) { FullMessage result = new FullMessage(); StringBuilder builder = new StringBuilder(); CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); for (byte pdu[] : pdus) { SmsMessage message = SmsMessage.createFromPdu(pdu, format); if (message == null) { // The PDU is not recognized by android return null; } if (result.firstMessage == null) { result.firstMessage = message; } String body = message.getMessageBody(); if (body == null && message.getUserData() != null) { // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS // Tests. The OMTP visual voicemail specification does not specify the SMS type and // encoding. ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData()); try { body = decoder.decode(byteBuffer).toString(); } catch (CharacterCodingException e) { // User data is not decode-able as UTF-8. Ignoring. return null; } } if (body != null) { builder.append(body); } } result.fullMessageBody = builder.toString(); return result; } private static String parseAsciiPduMessage(byte[][] pdus) { StringBuilder builder = new StringBuilder(); for (byte pdu[] : pdus) { builder.append(new String(pdu, StandardCharsets.US_ASCII)); } return builder.toString(); } private static boolean isSmsFromNumbers(SmsMessage message, List numbers) { if (message == null) { Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number"); return false; } for (String number : numbers) { if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) { return true; } } return false; } }