blob: 3ea5fc25866b8ba866a41b2481e6d7719c702aa5 [file] [log] [blame]
/*
* 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<String, List<Pattern>> 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.
*
* <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format:
*
* <p>[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) {
FullMessage fullMessage = getFullMessage(pdus, format);
if (fullMessage != null) {
// This is special case that voice mail SMS received before the filter has been
// set. To drop the SMS unconditionally.
if (messageBodyMatchesVvmPattern(context, subId, fullMessage.fullMessageBody)) {
Log.e(TAG, "SMS matching VVM format received but the filter not been set yet");
return true;
}
}
return false;
}
PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId);
if (phoneAccountHandle == null) {
Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle");
return false;
}
String clientPrefix = settings.clientPrefix;
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) {
Log.i(TAG, "Attempt to parse ascii PDU");
messageData = VisualVoicemailSmsParser.parse(clientPrefix, asciiMessage);
}
if (messageData != null) {
sendVvmSmsBroadcast(context, settings, 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;
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, settings, phoneAccountHandle, messageData, null);
return true;
}
if (messageBodyMatchesVvmPattern(context, subId, messageBody)) {
Log.w(TAG,
"SMS matches pattern but has illegal format, still dropping as VVM SMS");
sendVvmSmsBroadcast(context, settings, phoneAccountHandle, null, messageBody);
return true;
}
return false;
}
private static boolean messageBodyMatchesVvmPattern(Context context, int subId,
String messageBody) {
buildPatternsMap(context);
String mccMnc = context.getSystemService(TelephonyManager.class).getSimOperator(subId);
List<Pattern> 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);
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,
VisualVoicemailSmsFilterSettings filterSettings, 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.putExtra(VoicemailContract.EXTRA_TARGET_PACKAGE, filterSettings.packageName);
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<String> 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;
}
}