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