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