1 /* 2 * Copyright (C) 2016 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 package com.android.internal.telephony; 17 18 import android.annotation.Nullable; 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.provider.VoicemailContract; 23 import android.telecom.PhoneAccountHandle; 24 import android.telephony.PhoneNumberUtils; 25 import android.telephony.SmsMessage; 26 import android.telephony.SubscriptionManager; 27 import android.telephony.TelephonyManager; 28 import android.telephony.VisualVoicemailSms; 29 import android.telephony.VisualVoicemailSmsFilterSettings; 30 import android.util.ArrayMap; 31 import android.util.Log; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData; 35 36 import java.nio.ByteBuffer; 37 import java.nio.charset.CharacterCodingException; 38 import java.nio.charset.CharsetDecoder; 39 import java.nio.charset.StandardCharsets; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.regex.Pattern; 44 45 /** 46 * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link 47 * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual 48 * dispatching. 49 */ 50 public class VisualVoicemailSmsFilter { 51 52 /** 53 * Interface to convert subIds so the logic can be replaced in tests. 54 */ 55 @VisibleForTesting 56 public interface PhoneAccountHandleConverter { 57 58 /** 59 * Convert the subId to a {@link PhoneAccountHandle} 60 */ fromSubId(int subId)61 PhoneAccountHandle fromSubId(int subId); 62 } 63 64 private static final String TAG = "VvmSmsFilter"; 65 66 private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone"; 67 68 private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT = 69 new ComponentName("com.android.phone", 70 "com.android.services.telephony.TelephonyConnectionService"); 71 72 private static Map<String, List<Pattern>> sPatterns; 73 74 private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER = 75 new PhoneAccountHandleConverter() { 76 77 @Override 78 public PhoneAccountHandle fromSubId(int subId) { 79 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 80 return null; 81 } 82 int phoneId = SubscriptionManager.getPhoneId(subId); 83 if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) { 84 return null; 85 } 86 return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT, 87 PhoneFactory.getPhone(phoneId).getFullIccSerialNumber()); 88 } 89 }; 90 91 private static PhoneAccountHandleConverter sPhoneAccountHandleConverter = 92 DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; 93 94 /** 95 * Wrapper to combine multiple PDU into an SMS message 96 */ 97 private static class FullMessage { 98 public SmsMessage firstMessage; 99 public String fullMessageBody; 100 } 101 102 /** 103 * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A 104 * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony 105 * service, and the SMS will be dropped. 106 * 107 * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format: 108 * 109 * <p>[clientPrefix]:[prefix]:([key]=[value];)* 110 * 111 * Additionally, if the SMS does not match the format, but matches the regex specified by the 112 * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will 113 * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent. 114 * 115 * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped 116 */ filter(Context context, byte[][] pdus, String format, int destPort, int subId)117 public static boolean filter(Context context, byte[][] pdus, String format, int destPort, 118 int subId) { 119 TelephonyManager telephonyManager = 120 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 121 122 VisualVoicemailSmsFilterSettings settings; 123 settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId); 124 125 if (settings == null) { 126 return false; 127 } 128 129 PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId); 130 131 if (phoneAccountHandle == null) { 132 Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle"); 133 return false; 134 } 135 136 FullMessage fullMessage = getFullMessage(pdus, format); 137 138 if (fullMessage == null) { 139 // Carrier WAP push SMS is not recognized by android, which has a ascii PDU. 140 // Attempt to parse it. 141 Log.i(TAG, "Unparsable SMS received"); 142 String asciiMessage = parseAsciiPduMessage(pdus); 143 WrappedMessageData messageData = VisualVoicemailSmsParser 144 .parseAlternativeFormat(asciiMessage); 145 if (messageData != null) { 146 sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); 147 } 148 // Confidence for what the message actually is is low. Don't remove the message and let 149 // system decide. Usually because it is not parsable it will be dropped. 150 return false; 151 } 152 153 String messageBody = fullMessage.fullMessageBody; 154 String clientPrefix = settings.clientPrefix; 155 WrappedMessageData messageData = VisualVoicemailSmsParser 156 .parse(clientPrefix, messageBody); 157 if (messageData != null) { 158 if (settings.destinationPort 159 == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) { 160 if (destPort == -1) { 161 // Non-data SMS is directed to the port "-1". 162 Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS"); 163 return false; 164 } 165 } else if (settings.destinationPort 166 != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) { 167 if (settings.destinationPort != destPort) { 168 Log.i(TAG, "SMS matching VVM format received but is not directed to port " 169 + settings.destinationPort); 170 return false; 171 } 172 } 173 174 if (!settings.originatingNumbers.isEmpty() 175 && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) { 176 Log.i(TAG, "SMS matching VVM format received but is not from originating numbers"); 177 return false; 178 } 179 180 sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); 181 return true; 182 } 183 184 buildPatternsMap(context); 185 String mccMnc = telephonyManager.getSimOperator(subId); 186 187 List<Pattern> patterns = sPatterns.get(mccMnc); 188 if (patterns == null || patterns.isEmpty()) { 189 return false; 190 } 191 192 for (Pattern pattern : patterns) { 193 if (pattern.matcher(messageBody).matches()) { 194 Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, " 195 + "still dropping as VVM SMS"); 196 sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody); 197 return true; 198 } 199 } 200 return false; 201 } 202 203 /** 204 * override how subId is converted to PhoneAccountHandle for tests 205 */ 206 @VisibleForTesting setPhoneAccountHandleConverterForTest( PhoneAccountHandleConverter converter)207 public static void setPhoneAccountHandleConverterForTest( 208 PhoneAccountHandleConverter converter) { 209 if (converter == null) { 210 sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; 211 } else { 212 sPhoneAccountHandleConverter = converter; 213 } 214 } 215 buildPatternsMap(Context context)216 private static void buildPatternsMap(Context context) { 217 if (sPatterns != null) { 218 return; 219 } 220 sPatterns = new ArrayMap<>(); 221 // TODO(twyen): build from CarrierConfig once public API can be updated. 222 for (String entry : context.getResources() 223 .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) { 224 String[] mccMncList = entry.split(";")[0].split(","); 225 Pattern pattern = Pattern.compile(entry.split(";")[1]); 226 227 for (String mccMnc : mccMncList) { 228 if (!sPatterns.containsKey(mccMnc)) { 229 sPatterns.put(mccMnc, new ArrayList<>()); 230 } 231 sPatterns.get(mccMnc).add(pattern); 232 } 233 } 234 } 235 sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle, @Nullable WrappedMessageData messageData, @Nullable String messageBody)236 private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle, 237 @Nullable WrappedMessageData messageData, @Nullable String messageBody) { 238 Log.i(TAG, "VVM SMS received"); 239 Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED); 240 VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder(); 241 if (messageData != null) { 242 builder.setPrefix(messageData.prefix); 243 builder.setFields(messageData.fields); 244 } 245 if (messageBody != null) { 246 builder.setMessageBody(messageBody); 247 } 248 builder.setPhoneAccountHandle(phoneAccountHandle); 249 intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build()); 250 intent.setPackage(TELEPHONY_SERVICE_PACKAGE); 251 context.sendBroadcast(intent); 252 } 253 254 /** 255 * @return the message body of the SMS, or {@code null} if it can not be parsed. 256 */ 257 @Nullable getFullMessage(byte[][] pdus, String format)258 private static FullMessage getFullMessage(byte[][] pdus, String format) { 259 FullMessage result = new FullMessage(); 260 StringBuilder builder = new StringBuilder(); 261 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); 262 for (byte pdu[] : pdus) { 263 SmsMessage message = SmsMessage.createFromPdu(pdu, format); 264 if (message == null) { 265 // The PDU is not recognized by android 266 return null; 267 } 268 if (result.firstMessage == null) { 269 result.firstMessage = message; 270 } 271 String body = message.getMessageBody(); 272 if (body == null && message.getUserData() != null) { 273 // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using 274 // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS 275 // Tests. The OMTP visual voicemail specification does not specify the SMS type and 276 // encoding. 277 ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData()); 278 try { 279 body = decoder.decode(byteBuffer).toString(); 280 } catch (CharacterCodingException e) { 281 // User data is not decode-able as UTF-8. Ignoring. 282 return null; 283 } 284 } 285 if (body != null) { 286 builder.append(body); 287 } 288 } 289 result.fullMessageBody = builder.toString(); 290 return result; 291 } 292 parseAsciiPduMessage(byte[][] pdus)293 private static String parseAsciiPduMessage(byte[][] pdus) { 294 StringBuilder builder = new StringBuilder(); 295 for (byte pdu[] : pdus) { 296 builder.append(new String(pdu, StandardCharsets.US_ASCII)); 297 } 298 return builder.toString(); 299 } 300 isSmsFromNumbers(SmsMessage message, List<String> numbers)301 private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) { 302 if (message == null) { 303 Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number"); 304 return false; 305 } 306 307 for (String number : numbers) { 308 if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) { 309 return true; 310 } 311 } 312 return false; 313 } 314 } 315