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.PhoneNumberUtils;
25import android.telephony.SmsMessage;
26import android.telephony.SubscriptionManager;
27import android.telephony.TelephonyManager;
28import android.telephony.VisualVoicemailSms;
29import android.telephony.VisualVoicemailSmsFilterSettings;
30import android.util.ArrayMap;
31import android.util.Log;
32
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
35
36import java.nio.ByteBuffer;
37import java.nio.charset.CharacterCodingException;
38import java.nio.charset.CharsetDecoder;
39import java.nio.charset.StandardCharsets;
40import java.util.ArrayList;
41import java.util.List;
42import java.util.Map;
43import 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 */
50public 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         */
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     */
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
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
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
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
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
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
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