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 99 public SmsMessage firstMessage; 100 public String fullMessageBody; 101 } 102 103 /** 104 * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A 105 * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony 106 * service, and the SMS will be dropped. 107 * 108 * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format: 109 * 110 * <p>[clientPrefix]:[prefix]:([key]=[value];)* 111 * 112 * Additionally, if the SMS does not match the format, but matches the regex specified by the 113 * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will 114 * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent. 115 * 116 * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped 117 */ filter(Context context, byte[][] pdus, String format, int destPort, int subId)118 public static boolean filter(Context context, byte[][] pdus, String format, int destPort, 119 int subId) { 120 TelephonyManager telephonyManager = 121 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 122 123 VisualVoicemailSmsFilterSettings settings; 124 settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId); 125 126 if (settings == null) { 127 FullMessage fullMessage = getFullMessage(pdus, format); 128 if (fullMessage != null) { 129 // This is special case that voice mail SMS received before the filter has been 130 // set. To drop the SMS unconditionally. 131 if (messageBodyMatchesVvmPattern(context, subId, fullMessage.fullMessageBody)) { 132 Log.e(TAG, "SMS matching VVM format received but the filter not been set yet"); 133 return true; 134 } 135 } 136 return false; 137 } 138 139 PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId); 140 141 if (phoneAccountHandle == null) { 142 Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle"); 143 return false; 144 } 145 146 String clientPrefix = settings.clientPrefix; 147 FullMessage fullMessage = getFullMessage(pdus, format); 148 149 if (fullMessage == null) { 150 // Carrier WAP push SMS is not recognized by android, which has a ascii PDU. 151 // Attempt to parse it. 152 Log.i(TAG, "Unparsable SMS received"); 153 String asciiMessage = parseAsciiPduMessage(pdus); 154 WrappedMessageData messageData = VisualVoicemailSmsParser 155 .parseAlternativeFormat(asciiMessage); 156 if (messageData == null) { 157 Log.i(TAG, "Attempt to parse ascii PDU"); 158 messageData = VisualVoicemailSmsParser.parse(clientPrefix, asciiMessage); 159 } 160 if (messageData != null) { 161 sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null); 162 } 163 // Confidence for what the message actually is is low. Don't remove the message and let 164 // system decide. Usually because it is not parsable it will be dropped. 165 return false; 166 } 167 168 String messageBody = fullMessage.fullMessageBody; 169 WrappedMessageData messageData = VisualVoicemailSmsParser 170 .parse(clientPrefix, messageBody); 171 if (messageData != null) { 172 if (settings.destinationPort 173 == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) { 174 if (destPort == -1) { 175 // Non-data SMS is directed to the port "-1". 176 Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS"); 177 return false; 178 } 179 } else if (settings.destinationPort 180 != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) { 181 if (settings.destinationPort != destPort) { 182 Log.i(TAG, "SMS matching VVM format received but is not directed to port " 183 + settings.destinationPort); 184 return false; 185 } 186 } 187 188 if (!settings.originatingNumbers.isEmpty() 189 && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) { 190 Log.i(TAG, "SMS matching VVM format received but is not from originating numbers"); 191 return false; 192 } 193 194 sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null); 195 return true; 196 } 197 198 if (messageBodyMatchesVvmPattern(context, subId, messageBody)) { 199 Log.w(TAG, 200 "SMS matches pattern but has illegal format, still dropping as VVM SMS"); 201 sendVvmSmsBroadcast(context, settings, phoneAccountHandle, null, messageBody); 202 return true; 203 } 204 return false; 205 } 206 messageBodyMatchesVvmPattern(Context context, int subId, String messageBody)207 private static boolean messageBodyMatchesVvmPattern(Context context, int subId, 208 String messageBody) { 209 buildPatternsMap(context); 210 String mccMnc = context.getSystemService(TelephonyManager.class).getSimOperator(subId); 211 212 List<Pattern> patterns = sPatterns.get(mccMnc); 213 if (patterns == null || patterns.isEmpty()) { 214 return false; 215 } 216 217 for (Pattern pattern : patterns) { 218 if (pattern.matcher(messageBody).matches()) { 219 Log.w(TAG, "Incoming SMS matches pattern " + pattern); 220 return true; 221 } 222 } 223 return false; 224 } 225 226 /** 227 * override how subId is converted to PhoneAccountHandle for tests 228 */ 229 @VisibleForTesting setPhoneAccountHandleConverterForTest( PhoneAccountHandleConverter converter)230 public static void setPhoneAccountHandleConverterForTest( 231 PhoneAccountHandleConverter converter) { 232 if (converter == null) { 233 sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; 234 } else { 235 sPhoneAccountHandleConverter = converter; 236 } 237 } 238 buildPatternsMap(Context context)239 private static void buildPatternsMap(Context context) { 240 if (sPatterns != null) { 241 return; 242 } 243 sPatterns = new ArrayMap<>(); 244 // TODO(twyen): build from CarrierConfig once public API can be updated. 245 for (String entry : context.getResources() 246 .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) { 247 String[] mccMncList = entry.split(";")[0].split(","); 248 Pattern pattern = Pattern.compile(entry.split(";")[1]); 249 250 for (String mccMnc : mccMncList) { 251 if (!sPatterns.containsKey(mccMnc)) { 252 sPatterns.put(mccMnc, new ArrayList<>()); 253 } 254 sPatterns.get(mccMnc).add(pattern); 255 } 256 } 257 } 258 sendVvmSmsBroadcast(Context context, VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle, @Nullable WrappedMessageData messageData, @Nullable String messageBody)259 private static void sendVvmSmsBroadcast(Context context, 260 VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle, 261 @Nullable WrappedMessageData messageData, @Nullable String messageBody) { 262 Log.i(TAG, "VVM SMS received"); 263 Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED); 264 VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder(); 265 if (messageData != null) { 266 builder.setPrefix(messageData.prefix); 267 builder.setFields(messageData.fields); 268 } 269 if (messageBody != null) { 270 builder.setMessageBody(messageBody); 271 } 272 builder.setPhoneAccountHandle(phoneAccountHandle); 273 intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build()); 274 intent.putExtra(VoicemailContract.EXTRA_TARGET_PACKAGE, filterSettings.packageName); 275 intent.setPackage(TELEPHONY_SERVICE_PACKAGE); 276 context.sendBroadcast(intent); 277 } 278 279 /** 280 * @return the message body of the SMS, or {@code null} if it can not be parsed. 281 */ 282 @Nullable getFullMessage(byte[][] pdus, String format)283 private static FullMessage getFullMessage(byte[][] pdus, String format) { 284 FullMessage result = new FullMessage(); 285 StringBuilder builder = new StringBuilder(); 286 CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder(); 287 for (byte pdu[] : pdus) { 288 SmsMessage message = SmsMessage.createFromPdu(pdu, format); 289 if (message == null) { 290 // The PDU is not recognized by android 291 return null; 292 } 293 if (result.firstMessage == null) { 294 result.firstMessage = message; 295 } 296 String body = message.getMessageBody(); 297 if (body == null && message.getUserData() != null) { 298 // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using 299 // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS 300 // Tests. The OMTP visual voicemail specification does not specify the SMS type and 301 // encoding. 302 ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData()); 303 try { 304 body = decoder.decode(byteBuffer).toString(); 305 } catch (CharacterCodingException e) { 306 // User data is not decode-able as UTF-8. Ignoring. 307 return null; 308 } 309 } 310 if (body != null) { 311 builder.append(body); 312 } 313 } 314 result.fullMessageBody = builder.toString(); 315 return result; 316 } 317 parseAsciiPduMessage(byte[][] pdus)318 private static String parseAsciiPduMessage(byte[][] pdus) { 319 StringBuilder builder = new StringBuilder(); 320 for (byte pdu[] : pdus) { 321 builder.append(new String(pdu, StandardCharsets.US_ASCII)); 322 } 323 return builder.toString(); 324 } 325 isSmsFromNumbers(SmsMessage message, List<String> numbers)326 private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) { 327 if (message == null) { 328 Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number"); 329 return false; 330 } 331 332 for (String number : numbers) { 333 if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) { 334 return true; 335 } 336 } 337 return false; 338 } 339 } 340