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.os.Bundle;
     20 
     21 public class VisualVoicemailSmsParser {
     22 
     23     private static final String[] ALLOWED_ALTERNATIVE_FORMAT_EVENT = new String[] {
     24             "MBOXUPDATE", "UNRECOGNIZED"
     25     };
     26 
     27     /**
     28      * Class wrapping the raw OMTP message data, internally represented as as map of all key-value
     29      * pairs found in the SMS body. <p> All the methods return null if either the field was not
     30      * present or it could not be parsed.
     31      */
     32     public static class WrappedMessageData {
     33 
     34         public final String prefix;
     35         public final Bundle fields;
     36 
     37         @Override
     38         public String toString() {
     39             return "WrappedMessageData [type=" + prefix + " fields=" + fields + "]";
     40         }
     41 
     42         WrappedMessageData(String prefix, Bundle keyValues) {
     43             this.prefix = prefix;
     44             fields = keyValues;
     45         }
     46     }
     47 
     48     /**
     49      * Parses the supplied SMS body and returns back a structured OMTP message. Returns null if
     50      * unable to parse the SMS body.
     51      */
     52     @Nullable
     53     public static WrappedMessageData parse(String clientPrefix, String smsBody) {
     54         try {
     55             if (!smsBody.startsWith(clientPrefix)) {
     56                 return null;
     57             }
     58             int prefixEnd = clientPrefix.length();
     59             if (!(smsBody.charAt(prefixEnd) == ':')) {
     60                 return null;
     61             }
     62             int eventTypeEnd = smsBody.indexOf(":", prefixEnd + 1);
     63             if (eventTypeEnd == -1) {
     64                 return null;
     65             }
     66             String eventType = smsBody.substring(prefixEnd + 1, eventTypeEnd);
     67             Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
     68             if (fields == null) {
     69                 return null;
     70             }
     71             return new WrappedMessageData(eventType, fields);
     72         } catch (IndexOutOfBoundsException e) {
     73             return null;
     74         }
     75     }
     76 
     77     /**
     78      * Converts a String of key/value pairs into a Map object. The WrappedMessageData object
     79      * contains helper functions to retrieve the values.
     80      *
     81      * e.g. "//VVM:STATUS:st=R;rc=0;srv=1;dn=1;ipt=1;spt=0;u=eg (at) example.com;pw=1" =>
     82      * "WrappedMessageData [fields={st=R, ipt=1, srv=1, dn=1, u=eg (at) example.com, pw=1, rc=0}]"
     83      *
     84      * @param message The sms string with the prefix removed.
     85      * @return A WrappedMessageData object containing the map.
     86      */
     87     @Nullable
     88     private static Bundle parseSmsBody(String message) {
     89         // TODO: ensure fail if format does not match
     90         Bundle keyValues = new Bundle();
     91         String[] entries = message.split(";");
     92         for (String entry : entries) {
     93             if (entry.length() == 0) {
     94                 continue;
     95             }
     96             // The format for a field is <key>=<value>.
     97             // As the OMTP spec both key and value are required, but in some cases carriers will
     98             // send an SMS with missing value, so only the presence of the key is enforced.
     99             // For example, an SMS for a voicemail from restricted number might have "s=" for the
    100             // sender field, instead of omitting the field.
    101             int separatorIndex = entry.indexOf("=");
    102             if (separatorIndex == -1 || separatorIndex == 0) {
    103                 // No separator or no key.
    104                 // For example "foo" or "=value".
    105                 // A VVM SMS should have all of its' field valid.
    106                 return null;
    107             }
    108             String key = entry.substring(0, separatorIndex);
    109             String value = entry.substring(separatorIndex + 1);
    110             keyValues.putString(key, value);
    111         }
    112 
    113         return keyValues;
    114     }
    115 
    116     /**
    117      * The alternative format is [Event]?([key]=[value])*, for example
    118      *
    119      * <p>"MBOXUPDATE?m=1;server=example.com;port=143;name=foo (at) example.com;pw=foo".
    120      *
    121      * <p>This format is not protected with a client prefix and should be handled with care. For
    122      * safety, the event type must be one of {@link #ALLOWED_ALTERNATIVE_FORMAT_EVENT}
    123      */
    124     @Nullable
    125     public static WrappedMessageData parseAlternativeFormat(String smsBody) {
    126         try {
    127             int eventTypeEnd = smsBody.indexOf("?");
    128             if (eventTypeEnd == -1) {
    129                 return null;
    130             }
    131             String eventType = smsBody.substring(0, eventTypeEnd);
    132             if (!isAllowedAlternativeFormatEvent(eventType)) {
    133                 return null;
    134             }
    135             Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
    136             if (fields == null) {
    137                 return null;
    138             }
    139             return new WrappedMessageData(eventType, fields);
    140         } catch (IndexOutOfBoundsException e) {
    141             return null;
    142         }
    143     }
    144 
    145     private static boolean isAllowedAlternativeFormatEvent(String eventType) {
    146         for (String event : ALLOWED_ALTERNATIVE_FORMAT_EVENT) {
    147             if (event.equals(eventType)) {
    148                 return true;
    149             }
    150         }
    151         return false;
    152     }
    153 }
    154