/* * 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; } }