VisualVoicemailSmsFilter.java revision a6db15590a22000a92d72102ba98b92f608458f3
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 */
16package com.android.internal.telephony;
17
18import android.annotation.Nullable;
19import android.content.ComponentName;
20import android.content.Context;
21import android.content.Intent;
22import android.provider.VoicemailContract;
23import android.telecom.PhoneAccountHandle;
24import android.telephony.SmsMessage;
25import android.telephony.SubscriptionManager;
26import android.telephony.TelephonyManager;
27import android.telephony.VisualVoicemailSms;
28import android.telephony.VisualVoicemailSmsFilterSettings;
29import android.util.ArrayMap;
30import android.util.Log;
31
32import com.android.internal.annotations.VisibleForTesting;
33import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
34
35import java.nio.charset.StandardCharsets;
36import java.util.ArrayList;
37import java.util.List;
38import java.util.Map;
39import java.util.regex.Pattern;
40
41/**
42 * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link
43 * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual
44 * dispatching.
45 */
46public class VisualVoicemailSmsFilter {
47
48    /**
49     * Interface to convert subIds so the logic can be replaced in tests.
50     */
51    @VisibleForTesting
52    public interface PhoneAccountHandleConverter {
53
54        /**
55         * Convert the subId to a {@link PhoneAccountHandle}
56         */
57        PhoneAccountHandle fromSubId(int subId);
58    }
59
60    private static final String TAG = "VvmSmsFilter";
61
62    private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone";
63
64    private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT =
65            new ComponentName("com.android.phone",
66                    "com.android.services.telephony.TelephonyConnectionService");
67
68    private static Map<String, List<Pattern>> sPatterns;
69
70    private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER =
71            new PhoneAccountHandleConverter() {
72
73                @Override
74                public PhoneAccountHandle fromSubId(int subId) {
75                    if (!SubscriptionManager.isValidSubscriptionId(subId)) {
76                        return null;
77                    }
78                    int phoneId = SubscriptionManager.getPhoneId(subId);
79                    if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
80                        return null;
81                    }
82                    return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
83                            PhoneFactory.getPhone(phoneId).getFullIccSerialNumber());
84                }
85            };
86
87    private static PhoneAccountHandleConverter sPhoneAccountHandleConverter =
88            DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
89
90    /**
91     * Wrapper to combine multiple PDU into an SMS message
92     */
93    private static class FullMessage {
94        public SmsMessage firstMessage;
95        public String fullMessageBody;
96    }
97
98    /**
99     * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A
100     * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony
101     * service, and the SMS will be dropped.
102     *
103     * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format:
104     *
105     * <p>[clientPrefix]:[prefix]:([key]=[value];)*
106     *
107     * Additionally, if the SMS does not match the format, but matches the regex specified by the
108     * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will
109     * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent.
110     *
111     * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped
112     */
113    public static boolean filter(Context context, byte[][] pdus, String format, int destPort,
114            int subId) {
115        TelephonyManager telephonyManager =
116                (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
117
118        VisualVoicemailSmsFilterSettings settings;
119        settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId);
120
121        if (settings == null) {
122            return false;
123        }
124
125        PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId);
126
127        if (phoneAccountHandle == null) {
128            Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle");
129            return false;
130        }
131
132        FullMessage fullMessage = getFullMessage(pdus, format);
133
134        if (fullMessage == null) {
135            // Carrier WAP push SMS is not recognized by android, which has a ascii PDU.
136            // Attempt to parse it.
137            Log.i(TAG, "Unparsable SMS received");
138            String asciiMessage = parseAsciiPduMessage(pdus);
139            WrappedMessageData messageData = VisualVoicemailSmsParser
140                    .parseAlternativeFormat(asciiMessage);
141            if (messageData != null) {
142                sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
143            }
144            // Confidence for what the message actually is is low. Don't remove the message and let
145            // system decide. Usually because it is not parsable it will be dropped.
146            return false;
147        }
148
149        String messageBody = fullMessage.fullMessageBody;
150        String clientPrefix = settings.clientPrefix;
151        WrappedMessageData messageData = VisualVoicemailSmsParser
152                .parse(clientPrefix, messageBody);
153        if (messageData != null) {
154            if (settings.destinationPort
155                    == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) {
156                if (destPort == -1) {
157                    // Non-data SMS is directed to the port "-1".
158                    Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS");
159                    return false;
160                }
161            } else if (settings.destinationPort
162                    != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) {
163                if (settings.destinationPort != destPort) {
164                    Log.i(TAG, "SMS matching VVM format received but is not directed to port "
165                            + settings.destinationPort);
166                    return false;
167                }
168            }
169
170            if (!settings.originatingNumbers.isEmpty()
171                    && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) {
172                Log.i(TAG, "SMS matching VVM format received but is not from originating numbers");
173                return false;
174            }
175
176            sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
177            return true;
178        }
179
180        buildPatternsMap(context);
181        String mccMnc = telephonyManager.getSimOperator(subId);
182
183        List<Pattern> patterns = sPatterns.get(mccMnc);
184        if (patterns == null || patterns.isEmpty()) {
185            return false;
186        }
187
188        for (Pattern pattern : patterns) {
189            if (pattern.matcher(messageBody).matches()) {
190                Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, "
191                        + "still dropping as VVM SMS");
192                sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody);
193                return true;
194            }
195        }
196        return false;
197    }
198
199    /**
200     * override how subId is converted to PhoneAccountHandle for tests
201     */
202    @VisibleForTesting
203    public static void setPhoneAccountHandleConverterForTest(
204            PhoneAccountHandleConverter converter) {
205        if (converter == null) {
206            sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
207        } else {
208            sPhoneAccountHandleConverter = converter;
209        }
210    }
211
212    private static void buildPatternsMap(Context context) {
213        if (sPatterns != null) {
214            return;
215        }
216        sPatterns = new ArrayMap<>();
217        // TODO(twyen): build from CarrierConfig once public API can be updated.
218        for (String entry : context.getResources()
219                .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) {
220            String[] mccMncList = entry.split(";")[0].split(",");
221            Pattern pattern = Pattern.compile(entry.split(";")[1]);
222
223            for (String mccMnc : mccMncList) {
224                if (!sPatterns.containsKey(mccMnc)) {
225                    sPatterns.put(mccMnc, new ArrayList<>());
226                }
227                sPatterns.get(mccMnc).add(pattern);
228            }
229        }
230    }
231
232    private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle,
233            @Nullable WrappedMessageData messageData, @Nullable String messageBody) {
234        Log.i(TAG, "VVM SMS received");
235        Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
236        VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder();
237        if (messageData != null) {
238            builder.setPrefix(messageData.prefix);
239            builder.setFields(messageData.fields);
240        }
241        if (messageBody != null) {
242            builder.setMessageBody(messageBody);
243        }
244        builder.setPhoneAccountHandle(phoneAccountHandle);
245        intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build());
246        intent.setPackage(TELEPHONY_SERVICE_PACKAGE);
247        context.sendBroadcast(intent);
248    }
249
250    /**
251     * @return the message body of the SMS, or {@code null} if it can not be parsed.
252     */
253    @Nullable
254    private static FullMessage getFullMessage(byte[][] pdus, String format) {
255        FullMessage result = new FullMessage();
256        StringBuilder builder = new StringBuilder();
257        for (byte pdu[] : pdus) {
258            SmsMessage message = SmsMessage.createFromPdu(pdu, format);
259            if (message == null) {
260                // The PDU is not recognized by android
261                return null;
262            }
263            if (result.firstMessage == null) {
264                result.firstMessage = message;
265            }
266            String body = message.getMessageBody();
267            if (body != null) {
268                builder.append(body);
269            }
270        }
271        result.fullMessageBody = builder.toString();
272        return result;
273    }
274
275    private static String parseAsciiPduMessage(byte[][] pdus) {
276        StringBuilder builder = new StringBuilder();
277        for (byte pdu[] : pdus) {
278            builder.append(new String(pdu, StandardCharsets.US_ASCII));
279        }
280        return builder.toString();
281    }
282
283    private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) {
284        if (message == null) {
285            Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number");
286            return false;
287        }
288
289        for (String number : numbers) {
290            if (number.equals(message.getOriginatingAddress())) {
291                return true;
292            }
293        }
294        return false;
295    }
296}
297