aboutsummaryrefslogtreecommitdiffstats
path: root/src/java/com/android/internal/telephony/VisualVoicemailSmsParser.java
blob: b6b32020cbefaf3782a9d6a31a437478de70197b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
/*
 * 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.os.Bundle;

public class VisualVoicemailSmsParser {

    private static final String[] ALLOWED_ALTERNATIVE_FORMAT_EVENT = new String[] {
            "MBOXUPDATE", "UNRECOGNIZED"
    };

    /**
     * Class wrapping the raw OMTP message data, internally represented as as map of all key-value
     * pairs found in the SMS body. <p> All the methods return null if either the field was not
     * present or it could not be parsed.
     */
    public static class WrappedMessageData {

        public final String prefix;
        public final Bundle fields;

        @Override
        public String toString() {
            return "WrappedMessageData [type=" + prefix + " fields=" + fields + "]";
        }

        WrappedMessageData(String prefix, Bundle keyValues) {
            this.prefix = prefix;
            fields = keyValues;
        }
    }

    /**
     * Parses the supplied SMS body and returns back a structured OMTP message. Returns null if
     * unable to parse the SMS body.
     */
    @Nullable
    public static WrappedMessageData parse(String clientPrefix, String smsBody) {
        try {
            if (!smsBody.startsWith(clientPrefix)) {
                return null;
            }
            int prefixEnd = clientPrefix.length();
            if (!(smsBody.charAt(prefixEnd) == ':')) {
                return null;
            }
            int eventTypeEnd = smsBody.indexOf(":", prefixEnd + 1);
            if (eventTypeEnd == -1) {
                return null;
            }
            String eventType = smsBody.substring(prefixEnd + 1, eventTypeEnd);
            Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
            if (fields == null) {
                return null;
            }
            return new WrappedMessageData(eventType, fields);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    /**
     * Converts a String of key/value pairs into a Map object. The WrappedMessageData object
     * contains helper functions to retrieve the values.
     *
     * e.g. "//VVM:STATUS:st=R;rc=0;srv=1;dn=1;ipt=1;spt=0;u=eg@example.com;pw=1" =>
     * "WrappedMessageData [fields={st=R, ipt=1, srv=1, dn=1, u=eg@example.com, pw=1, rc=0}]"
     *
     * @param message The sms string with the prefix removed.
     * @return A WrappedMessageData object containing the map.
     */
    @Nullable
    private static Bundle parseSmsBody(String message) {
        // TODO: ensure fail if format does not match
        Bundle keyValues = new Bundle();
        String[] entries = message.split(";");
        for (String entry : entries) {
            if (entry.length() == 0) {
                continue;
            }
            // The format for a field is <key>=<value>.
            // As the OMTP spec both key and value are required, but in some cases carriers will
            // send an SMS with missing value, so only the presence of the key is enforced.
            // For example, an SMS for a voicemail from restricted number might have "s=" for the
            // sender field, instead of omitting the field.
            int separatorIndex = entry.indexOf("=");
            if (separatorIndex == -1 || separatorIndex == 0) {
                // No separator or no key.
                // For example "foo" or "=value".
                // A VVM SMS should have all of its' field valid.
                return null;
            }
            String key = entry.substring(0, separatorIndex);
            String value = entry.substring(separatorIndex + 1);
            keyValues.putString(key, value);
        }

        return keyValues;
    }

    /**
     * The alternative format is [Event]?([key]=[value])*, for example
     *
     * <p>"MBOXUPDATE?m=1;server=example.com;port=143;name=foo@example.com;pw=foo".
     *
     * <p>This format is not protected with a client prefix and should be handled with care. For
     * safety, the event type must be one of {@link #ALLOWED_ALTERNATIVE_FORMAT_EVENT}
     */
    @Nullable
    public static WrappedMessageData parseAlternativeFormat(String smsBody) {
        try {
            int eventTypeEnd = smsBody.indexOf("?");
            if (eventTypeEnd == -1) {
                return null;
            }
            String eventType = smsBody.substring(0, eventTypeEnd);
            if (!isAllowedAlternativeFormatEvent(eventType)) {
                return null;
            }
            Bundle fields = parseSmsBody(smsBody.substring(eventTypeEnd + 1));
            if (fields == null) {
                return null;
            }
            return new WrappedMessageData(eventType, fields);
        } catch (IndexOutOfBoundsException e) {
            return null;
        }
    }

    private static boolean isAllowedAlternativeFormatEvent(String eventType) {
        for (String event : ALLOWED_ALTERNATIVE_FORMAT_EVENT) {
            if (event.equals(eventType)) {
                return true;
            }
        }
        return false;
    }
}