VisualVoicemailSmsFilter.java revision a6db15590a22000a92d72102ba98b92f608458f3
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 */ 16package com.android.internal.telephony; 17 18import android.annotation.Nullable; 19import android.content.ComponentName; 20import android.content.Context; 21import android.content.Intent; 22import android.provider.VoicemailContract; 23import android.telecom.PhoneAccountHandle; 24import android.telephony.SmsMessage; 25import android.telephony.SubscriptionManager; 26import android.telephony.TelephonyManager; 27import android.telephony.VisualVoicemailSms; 28import android.telephony.VisualVoicemailSmsFilterSettings; 29import android.util.ArrayMap; 30import android.util.Log; 31 32import com.android.internal.annotations.VisibleForTesting; 33import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData; 34 35import java.nio.charset.StandardCharsets; 36import java.util.ArrayList; 37import java.util.List; 38import java.util.Map; 39import java.util.regex.Pattern; 40 41/** 42 * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link 43 * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual 44 * dispatching. 45 */ 46public class VisualVoicemailSmsFilter { 47 48 /** 49 * Interface to convert subIds so the logic can be replaced in tests. 50 */ 51 @VisibleForTesting 52 public interface PhoneAccountHandleConverter { 53 54 /** 55 * Convert the subId to a {@link PhoneAccountHandle} 56 */ 57 PhoneAccountHandle fromSubId(int subId); 58 } 59 60 private static final String TAG = "VvmSmsFilter"; 61 62 private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone"; 63 64 private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT = 65 new ComponentName("com.android.phone", 66 "com.android.services.telephony.TelephonyConnectionService"); 67 68 private static Map<String, List<Pattern>> sPatterns; 69 70 private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER = 71 new PhoneAccountHandleConverter() { 72 73 @Override 74 public PhoneAccountHandle fromSubId(int subId) { 75 if (!SubscriptionManager.isValidSubscriptionId(subId)) { 76 return null; 77 } 78 int phoneId = SubscriptionManager.getPhoneId(subId); 79 if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) { 80 return null; 81 } 82 return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT, 83 PhoneFactory.getPhone(phoneId).getFullIccSerialNumber()); 84 } 85 }; 86 87 private static PhoneAccountHandleConverter sPhoneAccountHandleConverter = 88 DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; 89 90 /** 91 * Wrapper to combine multiple PDU into an SMS message 92 */ 93 private static class FullMessage { 94 public SmsMessage firstMessage; 95 public String fullMessageBody; 96 } 97 98 /** 99 * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A 100 * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony 101 * service, and the SMS will be dropped. 102 * 103 * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format: 104 * 105 * <p>[clientPrefix]:[prefix]:([key]=[value];)* 106 * 107 * Additionally, if the SMS does not match the format, but matches the regex specified by the 108 * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will 109 * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent. 110 * 111 * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped 112 */ 113 public static boolean filter(Context context, byte[][] pdus, String format, int destPort, 114 int subId) { 115 TelephonyManager telephonyManager = 116 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 117 118 VisualVoicemailSmsFilterSettings settings; 119 settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId); 120 121 if (settings == null) { 122 return false; 123 } 124 125 PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId); 126 127 if (phoneAccountHandle == null) { 128 Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle"); 129 return false; 130 } 131 132 FullMessage fullMessage = getFullMessage(pdus, format); 133 134 if (fullMessage == null) { 135 // Carrier WAP push SMS is not recognized by android, which has a ascii PDU. 136 // Attempt to parse it. 137 Log.i(TAG, "Unparsable SMS received"); 138 String asciiMessage = parseAsciiPduMessage(pdus); 139 WrappedMessageData messageData = VisualVoicemailSmsParser 140 .parseAlternativeFormat(asciiMessage); 141 if (messageData != null) { 142 sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); 143 } 144 // Confidence for what the message actually is is low. Don't remove the message and let 145 // system decide. Usually because it is not parsable it will be dropped. 146 return false; 147 } 148 149 String messageBody = fullMessage.fullMessageBody; 150 String clientPrefix = settings.clientPrefix; 151 WrappedMessageData messageData = VisualVoicemailSmsParser 152 .parse(clientPrefix, messageBody); 153 if (messageData != null) { 154 if (settings.destinationPort 155 == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) { 156 if (destPort == -1) { 157 // Non-data SMS is directed to the port "-1". 158 Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS"); 159 return false; 160 } 161 } else if (settings.destinationPort 162 != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) { 163 if (settings.destinationPort != destPort) { 164 Log.i(TAG, "SMS matching VVM format received but is not directed to port " 165 + settings.destinationPort); 166 return false; 167 } 168 } 169 170 if (!settings.originatingNumbers.isEmpty() 171 && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) { 172 Log.i(TAG, "SMS matching VVM format received but is not from originating numbers"); 173 return false; 174 } 175 176 sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null); 177 return true; 178 } 179 180 buildPatternsMap(context); 181 String mccMnc = telephonyManager.getSimOperator(subId); 182 183 List<Pattern> patterns = sPatterns.get(mccMnc); 184 if (patterns == null || patterns.isEmpty()) { 185 return false; 186 } 187 188 for (Pattern pattern : patterns) { 189 if (pattern.matcher(messageBody).matches()) { 190 Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, " 191 + "still dropping as VVM SMS"); 192 sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody); 193 return true; 194 } 195 } 196 return false; 197 } 198 199 /** 200 * override how subId is converted to PhoneAccountHandle for tests 201 */ 202 @VisibleForTesting 203 public static void setPhoneAccountHandleConverterForTest( 204 PhoneAccountHandleConverter converter) { 205 if (converter == null) { 206 sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER; 207 } else { 208 sPhoneAccountHandleConverter = converter; 209 } 210 } 211 212 private static void buildPatternsMap(Context context) { 213 if (sPatterns != null) { 214 return; 215 } 216 sPatterns = new ArrayMap<>(); 217 // TODO(twyen): build from CarrierConfig once public API can be updated. 218 for (String entry : context.getResources() 219 .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) { 220 String[] mccMncList = entry.split(";")[0].split(","); 221 Pattern pattern = Pattern.compile(entry.split(";")[1]); 222 223 for (String mccMnc : mccMncList) { 224 if (!sPatterns.containsKey(mccMnc)) { 225 sPatterns.put(mccMnc, new ArrayList<>()); 226 } 227 sPatterns.get(mccMnc).add(pattern); 228 } 229 } 230 } 231 232 private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle, 233 @Nullable WrappedMessageData messageData, @Nullable String messageBody) { 234 Log.i(TAG, "VVM SMS received"); 235 Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED); 236 VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder(); 237 if (messageData != null) { 238 builder.setPrefix(messageData.prefix); 239 builder.setFields(messageData.fields); 240 } 241 if (messageBody != null) { 242 builder.setMessageBody(messageBody); 243 } 244 builder.setPhoneAccountHandle(phoneAccountHandle); 245 intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build()); 246 intent.setPackage(TELEPHONY_SERVICE_PACKAGE); 247 context.sendBroadcast(intent); 248 } 249 250 /** 251 * @return the message body of the SMS, or {@code null} if it can not be parsed. 252 */ 253 @Nullable 254 private static FullMessage getFullMessage(byte[][] pdus, String format) { 255 FullMessage result = new FullMessage(); 256 StringBuilder builder = new StringBuilder(); 257 for (byte pdu[] : pdus) { 258 SmsMessage message = SmsMessage.createFromPdu(pdu, format); 259 if (message == null) { 260 // The PDU is not recognized by android 261 return null; 262 } 263 if (result.firstMessage == null) { 264 result.firstMessage = message; 265 } 266 String body = message.getMessageBody(); 267 if (body != null) { 268 builder.append(body); 269 } 270 } 271 result.fullMessageBody = builder.toString(); 272 return result; 273 } 274 275 private static String parseAsciiPduMessage(byte[][] pdus) { 276 StringBuilder builder = new StringBuilder(); 277 for (byte pdu[] : pdus) { 278 builder.append(new String(pdu, StandardCharsets.US_ASCII)); 279 } 280 return builder.toString(); 281 } 282 283 private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) { 284 if (message == null) { 285 Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number"); 286 return false; 287 } 288 289 for (String number : numbers) { 290 if (number.equals(message.getOriginatingAddress())) { 291 return true; 292 } 293 } 294 return false; 295 } 296} 297