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