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
toString()38         public String toString() {
39             return "WrappedMessageData [type=" + prefix + " fields=" + fields + "]";
40         }
41 
WrappedMessageData(String prefix, Bundle keyValues)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
parse(String clientPrefix, String smsBody)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@example.com;pw=1" =>
82      * "WrappedMessageData [fields={st=R, ipt=1, srv=1, dn=1, u=eg@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
parseSmsBody(String message)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@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
parseAlternativeFormat(String smsBody)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 
isAllowedAlternativeFormatEvent(String eventType)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