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