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