• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.internal.telephony.SmsConstants.ENCODING_8BIT;
19 
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.provider.VoicemailContract;
25 import android.telecom.PhoneAccountHandle;
26 import android.telephony.PhoneNumberUtils;
27 import android.telephony.SmsMessage;
28 import android.telephony.SubscriptionManager;
29 import android.telephony.TelephonyManager;
30 import android.telephony.VisualVoicemailSms;
31 import android.telephony.VisualVoicemailSmsFilterSettings;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
37 
38 import java.nio.ByteBuffer;
39 import java.nio.charset.CharacterCodingException;
40 import java.nio.charset.CharsetDecoder;
41 import java.nio.charset.StandardCharsets;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.regex.Pattern;
46 
47 /**
48  * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link
49  * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual
50  * dispatching.
51  */
52 public class VisualVoicemailSmsFilter {
53 
54     /**
55      * Interface to convert subIds so the logic can be replaced in tests.
56      */
57     @VisibleForTesting
58     public interface PhoneAccountHandleConverter {
59 
60         /**
61          * Convert the subId to a {@link PhoneAccountHandle}
62          */
fromSubId(int subId)63         PhoneAccountHandle fromSubId(int subId);
64     }
65 
66     private static final String TAG = "VvmSmsFilter";
67 
68     private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone";
69 
70     private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT =
71             new ComponentName("com.android.phone",
72                     "com.android.services.telephony.TelephonyConnectionService");
73 
74     private static Map<String, List<Pattern>> sPatterns;
75 
76     private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER =
77             new PhoneAccountHandleConverter() {
78 
79                 @Override
80                 public PhoneAccountHandle fromSubId(int subId) {
81                     if (!SubscriptionManager.isValidSubscriptionId(subId)) {
82                         return null;
83                     }
84                     int phoneId = SubscriptionManager.getPhoneId(subId);
85                     if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
86                         return null;
87                     }
88                     return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
89                             Integer.toString(PhoneFactory.getPhone(phoneId).getSubId()));
90                 }
91             };
92 
93     private static PhoneAccountHandleConverter sPhoneAccountHandleConverter =
94             DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
95 
96     /**
97      * Wrapper to combine multiple PDU into an SMS message
98      */
99     private static class FullMessage {
100 
101         public SmsMessage firstMessage;
102         public String fullMessageBody;
103     }
104 
105     /**
106      * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A
107      * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony
108      * service, and the SMS will be dropped.
109      *
110      * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format:
111      *
112      * <p>[clientPrefix]:[prefix]:([key]=[value];)*
113      *
114      * Additionally, if the SMS does not match the format, but matches the regex specified by the
115      * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will
116      * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent.
117      *
118      * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped
119      */
filter(Context context, byte[][] pdus, String format, int destPort, int subId)120     public static boolean filter(Context context, byte[][] pdus, String format, int destPort,
121             int subId) {
122         TelephonyManager telephonyManager =
123                 (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
124 
125         VisualVoicemailSmsFilterSettings settings;
126         settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId);
127 
128         if (settings == null) {
129             FullMessage fullMessage = getFullMessage(pdus, format);
130             if (fullMessage != null) {
131                 // This is special case that voice mail SMS received before the filter has been
132                 // set. To drop the SMS unconditionally.
133                 if (messageBodyMatchesVvmPattern(context, subId, fullMessage.fullMessageBody)) {
134                     Log.e(TAG, "SMS matching VVM format received but the filter not been set yet");
135                     return true;
136                 }
137             }
138             return false;
139         }
140 
141         PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId);
142 
143         if (phoneAccountHandle == null) {
144             Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle");
145             return false;
146         }
147 
148         String clientPrefix = settings.clientPrefix;
149         FullMessage fullMessage = getFullMessage(pdus, format);
150 
151         if (fullMessage == null) {
152             // Carrier WAP push SMS is not recognized by android, which has a ascii PDU.
153             // Attempt to parse it.
154             Log.i(TAG, "Unparsable SMS received");
155             String asciiMessage = parseAsciiPduMessage(pdus);
156             WrappedMessageData messageData = VisualVoicemailSmsParser
157                     .parseAlternativeFormat(asciiMessage);
158             if (messageData == null) {
159                 Log.i(TAG, "Attempt to parse ascii PDU");
160                 messageData = VisualVoicemailSmsParser.parse(clientPrefix, asciiMessage);
161             }
162             if (messageData != null) {
163                 sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
164             }
165             // Confidence for what the message actually is is low. Don't remove the message and let
166             // system decide. Usually because it is not parsable it will be dropped.
167             return false;
168         }
169 
170         String messageBody = fullMessage.fullMessageBody;
171         WrappedMessageData messageData = VisualVoicemailSmsParser
172                 .parse(clientPrefix, messageBody);
173         if (messageData != null) {
174             if (settings.destinationPort
175                     == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) {
176                 if (destPort == -1) {
177                     // Non-data SMS is directed to the port "-1".
178                     Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS");
179                     return false;
180                 }
181             } else if (settings.destinationPort
182                     != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) {
183                 if (settings.destinationPort != destPort) {
184                     Log.i(TAG, "SMS matching VVM format received but is not directed to port "
185                             + settings.destinationPort);
186                     return false;
187                 }
188             }
189 
190             if (!settings.originatingNumbers.isEmpty()
191                     && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) {
192                 Log.i(TAG, "SMS matching VVM format received but is not from originating numbers");
193                 return false;
194             }
195 
196             sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
197             return true;
198         }
199 
200         if (messageBodyMatchesVvmPattern(context, subId, messageBody)) {
201             Log.w(TAG,
202                     "SMS matches pattern but has illegal format, still dropping as VVM SMS");
203             sendVvmSmsBroadcast(context, settings, phoneAccountHandle, null, messageBody);
204             return true;
205         }
206         return false;
207     }
208 
messageBodyMatchesVvmPattern(Context context, int subId, String messageBody)209     private static boolean messageBodyMatchesVvmPattern(Context context, int subId,
210             String messageBody) {
211         buildPatternsMap(context);
212         String mccMnc = context.getSystemService(TelephonyManager.class).getSimOperator(subId);
213 
214         List<Pattern> patterns = sPatterns.get(mccMnc);
215         if (patterns == null || patterns.isEmpty()) {
216             return false;
217         }
218 
219         for (Pattern pattern : patterns) {
220             if (pattern.matcher(messageBody).matches()) {
221                 Log.w(TAG, "Incoming SMS matches pattern " + pattern);
222                 return true;
223             }
224         }
225         return false;
226     }
227 
228     /**
229      * override how subId is converted to PhoneAccountHandle for tests
230      */
231     @VisibleForTesting
setPhoneAccountHandleConverterForTest( PhoneAccountHandleConverter converter)232     public static void setPhoneAccountHandleConverterForTest(
233             PhoneAccountHandleConverter converter) {
234         if (converter == null) {
235             sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
236         } else {
237             sPhoneAccountHandleConverter = converter;
238         }
239     }
240 
buildPatternsMap(Context context)241     private static void buildPatternsMap(Context context) {
242         if (sPatterns != null) {
243             return;
244         }
245         sPatterns = new ArrayMap<>();
246         // TODO(twyen): build from CarrierConfig once public API can be updated.
247         for (String entry : context.getResources()
248                 .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) {
249             String[] mccMncList = entry.split(";")[0].split(",");
250             Pattern pattern = Pattern.compile(entry.split(";")[1]);
251 
252             for (String mccMnc : mccMncList) {
253                 if (!sPatterns.containsKey(mccMnc)) {
254                     sPatterns.put(mccMnc, new ArrayList<>());
255                 }
256                 sPatterns.get(mccMnc).add(pattern);
257             }
258         }
259     }
260 
sendVvmSmsBroadcast(Context context, VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle, @Nullable WrappedMessageData messageData, @Nullable String messageBody)261     private static void sendVvmSmsBroadcast(Context context,
262             VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle,
263             @Nullable WrappedMessageData messageData, @Nullable String messageBody) {
264         Log.i(TAG, "VVM SMS received");
265         Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
266         VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder();
267         if (messageData != null) {
268             builder.setPrefix(messageData.prefix);
269             builder.setFields(messageData.fields);
270         }
271         if (messageBody != null) {
272             builder.setMessageBody(messageBody);
273         }
274         builder.setPhoneAccountHandle(phoneAccountHandle);
275         intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build());
276         intent.putExtra(VoicemailContract.EXTRA_TARGET_PACKAGE, filterSettings.packageName);
277         intent.setPackage(TELEPHONY_SERVICE_PACKAGE);
278         context.sendBroadcast(intent);
279     }
280 
281     /**
282      * @return the message body of the SMS, or {@code null} if it can not be parsed.
283      */
284     @Nullable
getFullMessage(byte[][] pdus, String format)285     private static FullMessage getFullMessage(byte[][] pdus, String format) {
286         FullMessage result = new FullMessage();
287         StringBuilder builder = new StringBuilder();
288         CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
289         for (byte pdu[] : pdus) {
290             SmsMessage message = SmsMessage.createFromPdu(pdu, format);
291             if (message == null) {
292                 // The PDU is not recognized by android
293                 return null;
294             }
295             if (result.firstMessage == null) {
296                 result.firstMessage = message;
297             }
298             String body = message.getMessageBody();
299 
300             /*
301              * For visual voice mail SMS message, UTF-8 is used by default
302              * {@link com.android.internal.telephony.SmsController#sendVisualVoicemailSmsForSubscriber}
303              *
304              * If config_sms_decode_gsm_8bit_data is enabled, GSM-8bit will be used to decode the
305              * received message. However, the message is most likely encoded with UTF-8. Therefore,
306              * we need to retry decoding the received message with UTF-8.
307              */
308             if ((body == null || (message.is3gpp()
309                     && message.getReceivedEncodingType() == ENCODING_8BIT))
310                     && message.getUserData() != null) {
311                 Log.d(TAG, "getFullMessage decode using UTF-8");
312                 // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using
313                 // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS
314                 // Tests. The OMTP visual voicemail specification does not specify the SMS type and
315                 // encoding.
316                 ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData());
317                 try {
318                     body = decoder.decode(byteBuffer).toString();
319                 } catch (CharacterCodingException e) {
320                     Log.e(TAG, "getFullMessage: got CharacterCodingException"
321                             + " when decoding with UTF-8, e = " + e);
322                     return null;
323                 }
324             }
325             if (body != null) {
326                 builder.append(body);
327             }
328         }
329         result.fullMessageBody = builder.toString();
330         return result;
331     }
332 
parseAsciiPduMessage(byte[][] pdus)333     private static String parseAsciiPduMessage(byte[][] pdus) {
334         StringBuilder builder = new StringBuilder();
335         for (byte pdu[] : pdus) {
336             builder.append(new String(pdu, StandardCharsets.US_ASCII));
337         }
338         return builder.toString();
339     }
340 
isSmsFromNumbers(SmsMessage message, List<String> numbers)341     private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) {
342         if (message == null) {
343             Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number");
344             return false;
345         }
346 
347         for (String number : numbers) {
348             if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) {
349                 return true;
350             }
351         }
352         return false;
353     }
354 }
355