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