| /* |
| * 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) { |
| 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<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 + " 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<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; |
| } |
| } |