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