content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License
   */
  package com.android.internal.telephony;
  
  import android.annotation.Nullable;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.provider.VoicemailContract;
  import android.telecom.PhoneAccountHandle;
  import android.telephony.PhoneNumberUtils;
  import android.telephony.SmsMessage;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.telephony.VisualVoicemailSms;
  import android.telephony.VisualVoicemailSmsFilterSettings;
  import android.util.ArrayMap;
  import android.util.Log;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
  
  import java.nio.ByteBuffer;
  import java.nio.charset.CharacterCodingException;
  import java.nio.charset.CharsetDecoder;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Map;
  import java.util.regex.Pattern;
  
  /**
   * Filters SMS to {@link android.telephony.VisualVoicemailService}, based on the config from {@link
   * VisualVoicemailSmsFilterSettings}. The SMS is sent to telephony service which will do the actual
   * dispatching.
   */
  public class VisualVoicemailSmsFilter {
  
      /**
       * Interface to convert subIds so the logic can be replaced in tests.
       */
      @VisibleForTesting
      public interface PhoneAccountHandleConverter {
  
          /**
           * Convert the subId to a {@link PhoneAccountHandle}
           */
          PhoneAccountHandle fromSubId(int subId);
      }
  
      private static final String TAG = "VvmSmsFilter";
  
      private static final String TELEPHONY_SERVICE_PACKAGE = "com.android.phone";
  
      private static final ComponentName PSTN_CONNECTION_SERVICE_COMPONENT =
              new ComponentName("com.android.phone",
                      "com.android.services.telephony.TelephonyConnectionService");
  
      private static Map<String, List<Pattern>> sPatterns;
  
      private static final PhoneAccountHandleConverter DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER =
              new PhoneAccountHandleConverter() {
  
                  @Override
                  public PhoneAccountHandle fromSubId(int subId) {
                      if (!SubscriptionManager.isValidSubscriptionId(subId)) {
                          return null;
                      }
                      int phoneId = SubscriptionManager.getPhoneId(subId);
                      if (phoneId == SubscriptionManager.INVALID_PHONE_INDEX) {
                          return null;
                      }
                      return new PhoneAccountHandle(PSTN_CONNECTION_SERVICE_COMPONENT,
                              PhoneFactory.getPhone(phoneId).getFullIccSerialNumber());
                  }
              };
  
      private static PhoneAccountHandleConverter sPhoneAccountHandleConverter =
              DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
  
      /**
       * Wrapper to combine multiple PDU into an SMS message
       */
      private static class FullMessage {
  
          public SmsMessage firstMessage;
          public String fullMessageBody;
      }
  
      /**
       * Attempt to parse the incoming SMS as a visual voicemail SMS. If the parsing succeeded, A
       * {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} intent will be sent to telephony
       * service, and the SMS will be dropped.
       *
       * <p>The accepted format for a visual voicemail SMS is a generalization of the OMTP format:
       *
       * <p>[clientPrefix]:[prefix]:([key]=[value];)*
       *
       * Additionally, if the SMS does not match the format, but matches the regex specified by the
       * carrier in {@link com.android.internal.R.array#config_vvmSmsFilterRegexes}, the SMS will
       * still be dropped and a {@link VoicemailContract#ACTION_VOICEMAIL_SMS_RECEIVED} will be sent.
       *
       * @return true if the SMS has been parsed to be a visual voicemail SMS and should be dropped
       */
      public static boolean filter(Context context, byte[][] pdus, String format, int destPort,
              int subId) {
          TelephonyManager telephonyManager =
                  (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
  
          VisualVoicemailSmsFilterSettings settings;
          settings = telephonyManager.getActiveVisualVoicemailSmsFilterSettings(subId);
  
          if (settings == null) {
              FullMessage fullMessage = getFullMessage(pdus, format);
              if (fullMessage != null) {
                  // This is special case that voice mail SMS received before the filter has been
                  // set. To drop the SMS unconditionally.
                  if (messageBodyMatchesVvmPattern(context, subId, fullMessage.fullMessageBody)) {
                      Log.e(TAG, "SMS matching VVM format received but the filter not been set yet");
                      return true;
                  }
              }
              return false;
          }
  
          PhoneAccountHandle phoneAccountHandle = sPhoneAccountHandleConverter.fromSubId(subId);
  
          if (phoneAccountHandle == null) {
              Log.e(TAG, "Unable to convert subId " + subId + " to PhoneAccountHandle");
              return false;
          }
  
          String clientPrefix = settings.clientPrefix;
          FullMessage fullMessage = getFullMessage(pdus, format);
  
          if (fullMessage == null) {
              // Carrier WAP push SMS is not recognized by android, which has a ascii PDU.
              // Attempt to parse it.
              Log.i(TAG, "Unparsable SMS received");
              String asciiMessage = parseAsciiPduMessage(pdus);
              WrappedMessageData messageData = VisualVoicemailSmsParser
                      .parseAlternativeFormat(asciiMessage);
              if (messageData == null) {
                  Log.i(TAG, "Attempt to parse ascii PDU");
                  messageData = VisualVoicemailSmsParser.parse(clientPrefix, asciiMessage);
              }
              if (messageData != null) {
                  sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
              }
              // Confidence for what the message actually is is low. Don't remove the message and let
              // system decide. Usually because it is not parsable it will be dropped.
              return false;
          }
  
          String messageBody = fullMessage.fullMessageBody;
          WrappedMessageData messageData = VisualVoicemailSmsParser
                  .parse(clientPrefix, messageBody);
          if (messageData != null) {
              if (settings.destinationPort
                      == VisualVoicemailSmsFilterSettings.DESTINATION_PORT_DATA_SMS) {
                  if (destPort == -1) {
                      // Non-data SMS is directed to the port "-1".
                      Log.i(TAG, "SMS matching VVM format received but is not a DATA SMS");
                      return false;
                  }
              } else if (settings.destinationPort
                      != VisualVoicemailSmsFilterSettings.DESTINATION_PORT_ANY) {
                  if (settings.destinationPort != destPort) {
                      Log.i(TAG, "SMS matching VVM format received but is not directed to port "
                              + settings.destinationPort);
                      return false;
                  }
              }
  
              if (!settings.originatingNumbers.isEmpty()
                      && !isSmsFromNumbers(fullMessage.firstMessage, settings.originatingNumbers)) {
                  Log.i(TAG, "SMS matching VVM format received but is not from originating numbers");
                  return false;
              }
  
              sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
              return true;
          }
  
          if (messageBodyMatchesVvmPattern(context, subId, messageBody)) {
              Log.w(TAG,
                      "SMS matches pattern but has illegal format, still dropping as VVM SMS");
              sendVvmSmsBroadcast(context, settings, phoneAccountHandle, null, messageBody);
              return true;
          }
          return false;
      }
  
      private static boolean messageBodyMatchesVvmPattern(Context context, int subId,
              String messageBody) {
          buildPatternsMap(context);
          String mccMnc = context.getSystemService(TelephonyManager.class).getSimOperator(subId);
  
          List<Pattern> patterns = sPatterns.get(mccMnc);
          if (patterns == null || patterns.isEmpty()) {
              return false;
          }
  
          for (Pattern pattern : patterns) {
              if (pattern.matcher(messageBody).matches()) {
                  Log.w(TAG, "Incoming SMS matches pattern " + pattern);
                  return true;
              }
          }
          return false;
      }
  
      /**
       * override how subId is converted to PhoneAccountHandle for tests
       */
      @VisibleForTesting
      public static void setPhoneAccountHandleConverterForTest(
              PhoneAccountHandleConverter converter) {
          if (converter == null) {
              sPhoneAccountHandleConverter = DEFAULT_PHONE_ACCOUNT_HANDLE_CONVERTER;
          } else {
              sPhoneAccountHandleConverter = converter;
          }
      }
  
      private static void buildPatternsMap(Context context) {
          if (sPatterns != null) {
              return;
          }
          sPatterns = new ArrayMap<>();
          // TODO(twyen): build from CarrierConfig once public API can be updated.
          for (String entry : context.getResources()
                  .getStringArray(com.android.internal.R.array.config_vvmSmsFilterRegexes)) {
              String[] mccMncList = entry.split(";")[0].split(",");
              Pattern pattern = Pattern.compile(entry.split(";")[1]);
  
              for (String mccMnc : mccMncList) {
                  if (!sPatterns.containsKey(mccMnc)) {
                      sPatterns.put(mccMnc, new ArrayList<>());
                  }
                  sPatterns.get(mccMnc).add(pattern);
              }
          }
      }
  
      private static void sendVvmSmsBroadcast(Context context,
              VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle,
              @Nullable WrappedMessageData messageData, @Nullable String messageBody) {
          Log.i(TAG, "VVM SMS received");
          Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
          VisualVoicemailSms.Builder builder = new VisualVoicemailSms.Builder();
          if (messageData != null) {
              builder.setPrefix(messageData.prefix);
              builder.setFields(messageData.fields);
          }
          if (messageBody != null) {
              builder.setMessageBody(messageBody);
          }
          builder.setPhoneAccountHandle(phoneAccountHandle);
          intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build());
          intent.putExtra(VoicemailContract.EXTRA_TARGET_PACKAGE, filterSettings.packageName);
          intent.setPackage(TELEPHONY_SERVICE_PACKAGE);
          context.sendBroadcast(intent);
      }
  
      /**
       * @return the message body of the SMS, or {@code null} if it can not be parsed.
       */
      @Nullable
      private static FullMessage getFullMessage(byte[][] pdus, String format) {
          FullMessage result = new FullMessage();
          StringBuilder builder = new StringBuilder();
          CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
          for (byte pdu[] : pdus) {
              SmsMessage message = SmsMessage.createFromPdu(pdu, format);
              if (message == null) {
                  // The PDU is not recognized by android
                  return null;
              }
              if (result.firstMessage == null) {
                  result.firstMessage = message;
              }
              String body = message.getMessageBody();
              if (body == null && message.getUserData() != null) {
                  // Attempt to interpret the user data as UTF-8. UTF-8 string over data SMS using
                  // 8BIT data coding scheme is our recommended way to send VVM SMS and is used in CTS
                  // Tests. The OMTP visual voicemail specification does not specify the SMS type and
                  // encoding.
                  ByteBuffer byteBuffer = ByteBuffer.wrap(message.getUserData());
                  try {
                      body = decoder.decode(byteBuffer).toString();
                  } catch (CharacterCodingException e) {
                      // User data is not decode-able as UTF-8. Ignoring.
                      return null;
                  }
              }
              if (body != null) {
                  builder.append(body);
              }
          }
          result.fullMessageBody = builder.toString();
          return result;
      }
  
      private static String parseAsciiPduMessage(byte[][] pdus) {
          StringBuilder builder = new StringBuilder();
          for (byte pdu[] : pdus) {
              builder.append(new String(pdu, StandardCharsets.US_ASCII));
          }
          return builder.toString();
      }
  
      private static boolean isSmsFromNumbers(SmsMessage message, List<String> numbers) {
          if (message == null) {
              Log.e(TAG, "Unable to create SmsMessage from PDU, cannot determine originating number");
              return false;
          }
  
          for (String number : numbers) {
              if (PhoneNumberUtils.compare(number, message.getOriginatingAddress())) {
                  return true;
              }
          }
          return false;
      }
  }
  