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