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