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