• 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 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