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.os.Bundle;
20
21public 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@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
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
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