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