Receive SYNC and STATUS SMS text messages.

These classes are adopted from the voicemail-example-for-android
voicemail source implementation with these notable differences:
skipping the interface classes, omitting OmtpSmsHandler, returning "null"
instead of throwing an exception, excluding constants that are not being used.
* Message receiver: receives SMS messages and sends to the parser
* Message parser: extracts fields and inserts into a wrapper, then create
  StatusMessage and SyncMessage from the fields in the wrapper
* StatusMessage: an object containing the values of a status message
* SyncMessage: an object containing the values of a sync message

Bug:19236241

Change-Id: Ifb2dea832f5e5634fc301e9eb238650770b009e2
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index bc84bf5..14a283a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -583,5 +583,13 @@
                 <action android:name="android.content.action.SEARCH_INDEXABLES_PROVIDER" />
             </intent-filter>
         </provider>
+
+        <receiver android:name="com.android.phone.vvm.omtp.sms.OmtpMessageReceiver"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.DATA_SMS_RECEIVED" />
+                <data android:scheme="sms" />
+            </intent-filter>
+        </receiver>
     </application>
 </manifest>
diff --git a/src/com/android/phone/vvm/omtp/OmtpConstants.java b/src/com/android/phone/vvm/omtp/OmtpConstants.java
index e911e54..971d2d6 100644
--- a/src/com/android/phone/vvm/omtp/OmtpConstants.java
+++ b/src/com/android/phone/vvm/omtp/OmtpConstants.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2015 Google Inc. All Rights Reserved.
+ * Copyright (C) 2015 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.
@@ -15,6 +15,9 @@
  */
 package com.android.phone.vvm.omtp;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec.
  * <p>
@@ -26,21 +29,163 @@
     public static final String SMS_PREFIX_SEPARATOR = ":";
 
     public static final String CLIENT_PREFIX = "//VVM";
+    public static final String SYNC_SMS_PREFIX = CLIENT_PREFIX + ":SYNC:";
+    public static final String STATUS_SMS_PREFIX = CLIENT_PREFIX + ":STATUS:";
+
+    // This is the format designated by the OMTP spec.
+    public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z";
 
     /** OMTP protocol versions. */
-    public static String PROTOCOL_VERSION1_1 = "11";
-    public static String PROTOCOL_VERSION1_2 = "12";
-    public static String PROTOCOL_VERSION1_3 = "13";
+    public static final String PROTOCOL_VERSION1_1 = "11";
+    public static final String PROTOCOL_VERSION1_2 = "12";
+    public static final String PROTOCOL_VERSION1_3 = "13";
 
     ///////////////////////// Client/Mobile originated SMS //////////////////////
 
     /** Mobile Originated requests */
-    public static String ACTIVATE_REQUEST = "Activate";
-    public static String DEACTIVATE_REQUEST = "Deactivate";
-    public static String STATUS_REQUEST = "Status";
+    public static final String ACTIVATE_REQUEST = "Activate";
+    public static final String DEACTIVATE_REQUEST = "Deactivate";
+    public static final String STATUS_REQUEST = "Status";
 
     /** fields that can be present in a Mobile Originated OMTP SMS */
-    public static String CLIENT_TYPE = "ct";
-    public static String APPLICATION_PORT = "pt";
-    public static String PROTOCOL_VERSION = "pv";
+    public static final String CLIENT_TYPE = "ct";
+    public static final String APPLICATION_PORT = "pt";
+    public static final String PROTOCOL_VERSION = "pv";
+
+
+    //////////////////////////////// Sync SMS fields ////////////////////////////
+
+    /**
+     * Sync SMS fields.
+     * <p>
+     * Each string constant is the field's key in the SMS body which is used by the parser to
+     * identify the field's value, if present, in the SMS body.
+     */
+
+    /**
+     * The event that triggered this SYNC SMS.
+     * See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES}
+     */
+    public static final String SYNC_TRIGGER_EVENT = "ev";
+    public static final String MESSAGE_UID = "id";
+    public static final String MESSAGE_LENGTH = "l";
+    public static final String NUM_MESSAGE_COUNT = "c";
+    /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */
+    public static final String CONTENT_TYPE = "t";
+    public static final String SENDER = "s";
+    public static final String TIME = "dt";
+
+    /**
+     * SYNC message trigger events.
+     * <p>
+     * These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}.
+     */
+    public static final String NEW_MESSAGE = "NM";
+    public static final String MAILBOX_UPDATE = "MBU";
+    public static final String GREETINGS_UPDATE = "GU";
+
+    public static final String[] SYNC_TRIGGER_EVENT_VALUES = {
+        NEW_MESSAGE,
+        MAILBOX_UPDATE,
+        GREETINGS_UPDATE
+    };
+
+    /**
+     * Content types supported by OMTP VVM.
+     * <p>
+     * These are the possible values of {@link OmtpConstants#CONTENT_TYPE}.
+     */
+    public static final String VOICE = "v";
+    public static final String VIDEO = "o";
+    public static final String FAX = "f";
+    /** Voice message deposited by an external application */
+    public static final String INFOTAINMENT = "i";
+    /** Empty Call Capture - i.e. voicemail with no voice message. */
+    public static final String ECC = "e";
+
+    public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC};
+
+    ////////////////////////////// Status SMS fields ////////////////////////////
+
+    /**
+     * Status SMS fields.
+     * <p>
+     * Each string constant is the field's key in the SMS body which is used by the parser to
+     * identify the field's value, if present, in the SMS body.
+     */
+    /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */
+    public static final String PROVISIONING_STATUS = "st";
+    /** See {@link OmtpConstants#RETURN_CODE_VALUES} */
+    public static final String RETURN_CODE = "rc";
+    /** URL to send users to for activation VVM */
+    public static final String SUBSCRIPTION_URL = "rs";
+    /** IMAP4/SMTP server IP address or fully qualified domain name */
+    public static final String SERVER_ADDRESS = "srv";
+    /** Phone number to access voicemails through Telephony User Interface */
+    public static final String TUI_ACCESS_NUMBER = "tui";
+    /** Number to send client origination SMS */
+    public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn";
+    public static final String IMAP_PORT = "ipt";
+    public static final String IMAP_USER_NAME = "u";
+    public static final String IMAP_PASSWORD = "pw";
+    public static final String SMTP_PORT = "spt";
+    public static final String SMTP_USER_NAME = "smtp_u";
+    public static final String SMTP_PASSWORD = "smtp_pw";
+
+    /**
+     * User provisioning status values.
+     * <p>
+     * Referred by {@link OmtpConstants#PROVISIONING_STATUS}.
+     */
+    // TODO: As per the spec the code could be either be with or w/o quotes  = "N"/N). Currently
+    // this only handles the w/o quotes values.
+    public static final String SUBSCRIBER_NEW = "N";
+    public static final String SUBSCRIBER_READY = "R";
+    public static final String SUBSCRIBER_PROVISIONED = "P";
+    public static final String SUBSCRIBER_UNKNOWN = "U";
+    public static final String SUBSCRIBER_BLOCKED = "B";
+
+    public static final String[] PROVISIONING_STATUS_VALUES = {
+        SUBSCRIBER_NEW,
+        SUBSCRIBER_READY,
+        SUBSCRIBER_PROVISIONED,
+        SUBSCRIBER_UNKNOWN,
+        SUBSCRIBER_BLOCKED
+    };
+
+    /**
+     * The return code included in a status message.
+     * <p>
+     * These are the possible values of {@link OmtpConstants#RETURN_CODE}.
+     */
+    public static final String SUCCESS = "0";
+    public static final String SYSTEM_ERROR = "1";
+    public static final String SUBSCRIBER_ERROR = "2";
+    public static final String MAILBOX_UNKNOWN = "3";
+    public static final String VVM_NOT_ACTIVATED = "4";
+    public static final String VVM_NOT_PROVISIONED = "5";
+    public static final String VVM_CLIENT_UKNOWN = "6";
+    public static final String VVM_MAILBOX_NOT_INITIALIZED = "7";
+
+    public static final String[] RETURN_CODE_VALUES = {
+        SUCCESS,
+        SYSTEM_ERROR,
+        SUBSCRIBER_ERROR,
+        MAILBOX_UNKNOWN,
+        VVM_NOT_ACTIVATED,
+        VVM_NOT_PROVISIONED,
+        VVM_CLIENT_UKNOWN,
+        VVM_MAILBOX_NOT_INITIALIZED,
+    };
+
+    /**
+     * A map of all the field keys to the possible values they can have.
+     */
+    public static final Map<String, String[]> possibleValuesMap = new HashMap<String, String[]>() {{
+        put(SYNC_TRIGGER_EVENT, SYNC_TRIGGER_EVENT_VALUES);
+        put(CONTENT_TYPE, CONTENT_TYPE_VALUES);
+        put(PROVISIONING_STATUS, PROVISIONING_STATUS_VALUES);
+        put(RETURN_CODE, RETURN_CODE_VALUES);
+    }};
+
 }
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
new file mode 100644
index 0000000..bb9b28f
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpMessageReceiver.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2015 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.phone.vvm.omtp.sms;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Telephony;
+import android.telephony.SmsMessage;
+import android.util.Log;
+
+import com.android.phone.vvm.omtp.OmtpConstants;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Receive SMS messages and send for processing by the OMTP visual voicemail source.
+ */
+public class OmtpMessageReceiver extends BroadcastReceiver {
+    private static final String TAG = "OmtpMessageReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
+        StringBuilder userData = new StringBuilder();
+        StringBuilder messageBody = new StringBuilder();
+
+        for (int i = 0; i < messages.length; i++) {
+            messageBody.append(messages[i].getMessageBody());
+            userData.append(extractUserData(messages[i]));
+        }
+
+        WrappedMessageData messageData = OmtpSmsParser.parse(messageBody.toString());
+        if (messageData != null) {
+            if (messageData.getPrefix() == OmtpConstants.SYNC_SMS_PREFIX) {
+                SyncMessage message = new SyncMessage(messageData);
+                //TODO: handle message
+            } else if (messageData.getPrefix() == OmtpConstants.STATUS_SMS_PREFIX) {
+                StatusMessage message = new StatusMessage(messageData);
+                //TODO: handle message
+            } else {
+                Log.e(TAG, "This should never have happened");
+            }
+        }
+        // Let this fall through: this is not a message we're interested in.
+    }
+
+    private String extractUserData(SmsMessage sms) {
+        try {
+            // OMTP spec does not tell about the encoding. We assume ASCII.
+            // UTF-8 sounds safer as it can handle ascii as well as other charsets.
+            return new String(sms.getUserData(), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalStateException("This should have never happened", e);
+        }
+    }
+}
diff --git a/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java b/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java
new file mode 100644
index 0000000..54a2a02
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sms/OmtpSmsParser.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015 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.phone.vvm.omtp.sms;
+
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.phone.vvm.omtp.OmtpConstants;
+
+import java.util.Map;
+
+/**
+ * OMTP SMS parser interface, for parsing SYNC and STATUS SMS sent by OMTP visual voicemail server.
+ */
+public class OmtpSmsParser {
+    private static String TAG = "OmtpSmsParser";
+    /**
+     * Parses the supplied SMS body and returns back a structured OMTP message.
+     * Returns null if unable to parse the SMS body.
+     */
+    public static WrappedMessageData parse(String smsBody) {
+        if (smsBody == null) {
+            return null;
+        }
+
+        WrappedMessageData messageData = null;
+        if (smsBody.startsWith(OmtpConstants.SYNC_SMS_PREFIX)) {
+            messageData = new WrappedMessageData(OmtpConstants.SYNC_SMS_PREFIX,
+                    parseSmsBody(smsBody.substring(OmtpConstants.SYNC_SMS_PREFIX.length())));
+            // Check for a mandatory field.
+            String triggerEvent = messageData.extractString(OmtpConstants.SYNC_TRIGGER_EVENT);
+            if (triggerEvent == null) {
+                Log.e(TAG, "Missing mandatory field: " + OmtpConstants.SYNC_TRIGGER_EVENT);
+                return null;
+            }
+        } else if (smsBody.startsWith(OmtpConstants.STATUS_SMS_PREFIX)) {
+            messageData = new WrappedMessageData(OmtpConstants.STATUS_SMS_PREFIX,
+                    parseSmsBody(smsBody.substring(OmtpConstants.STATUS_SMS_PREFIX.length())));
+        }
+
+        return messageData;
+    }
+
+    /**
+     * 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 [mFields={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.
+     */
+    private static Map<String, String> parseSmsBody(String message) {
+        Map<String, String> keyValues = new ArrayMap<String, String>();
+        String[] entries = message.split(OmtpConstants.SMS_FIELD_SEPARATOR);
+        for (String entry : entries) {
+            String[] keyValue = entry.split(OmtpConstants.SMS_KEY_VALUE_SEPARATOR);
+            if (keyValue.length != 2) {
+                continue;
+            }
+            keyValues.put(keyValue[0].trim(), keyValue[1].trim());
+        }
+
+        return keyValues;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sms/StatusMessage.java b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
new file mode 100644
index 0000000..7e4faac
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sms/StatusMessage.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2015 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.phone.vvm.omtp.sms;
+
+import android.telecom.Log;
+
+import com.android.phone.vvm.omtp.OmtpConstants;
+
+/**
+ * Structured data representation of OMTP STATUS message.
+ *
+ * The getters will return null if the field was not set in the message body or it could not be
+ * parsed.
+ */
+public class StatusMessage {
+    // NOTE: Following Status SMS fields are not yet parsed, as they do not seem
+    // to be useful for initial omtp source implementation.
+    // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt
+
+    private final String mProvisioningStatus;
+    private final String mStatusReturnCode;
+    private final String mSubscriptionUrl;
+    private final String mServerAddress;
+    private final String mTuiAccessNumber;
+    private final String mClientSmsDestinationNumber;
+    private final String mImapPort;
+    private final String mImapUserName;
+    private final String mImapPassword;
+    private final String mSmtpPort;
+    private final String mSmtpUserName;
+    private final String mSmtpPassword;
+
+    @Override
+    public String toString() {
+        return "StatusMessage [mProvisioningStatus=" + mProvisioningStatus
+                + ", mStatusReturnCode=" + mStatusReturnCode
+                + ", mSubscriptionUrl=" + mSubscriptionUrl
+                + ", mServerAddress=" + mServerAddress
+                + ", mTuiAccessNumber=" + mTuiAccessNumber
+                + ", mClientSmsDestinationNumber=" + mClientSmsDestinationNumber
+                + ", mImapPort=" + mImapPort
+                + ", mImapUserName=" + mImapUserName
+                + ", mImapPassword=" + Log.pii(mImapPassword)
+                + ", mSmtpPort=" + mSmtpPort
+                + ", mSmtpUserName=" + mSmtpUserName
+                + ", mSmtpPassword=" + Log.pii(mSmtpPassword) + "]";
+    }
+
+    public StatusMessage(WrappedMessageData wrappedData) {
+        mProvisioningStatus = wrappedData.extractString(OmtpConstants.PROVISIONING_STATUS);
+        mStatusReturnCode = wrappedData.extractString(OmtpConstants.RETURN_CODE);
+        mSubscriptionUrl = wrappedData.extractString(OmtpConstants.SUBSCRIPTION_URL);
+        mServerAddress = wrappedData.extractString(OmtpConstants.SERVER_ADDRESS);
+        mTuiAccessNumber = wrappedData.extractString(OmtpConstants.TUI_ACCESS_NUMBER);
+        mClientSmsDestinationNumber = wrappedData.extractString(
+                OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER);
+        mImapPort = wrappedData.extractString(OmtpConstants.IMAP_PORT);
+        mImapUserName = wrappedData.extractString(OmtpConstants.IMAP_USER_NAME);
+        mImapPassword = wrappedData.extractString(OmtpConstants.IMAP_PASSWORD);
+        mSmtpPort = wrappedData.extractString(OmtpConstants.SMTP_PORT);
+        mSmtpUserName = wrappedData.extractString(OmtpConstants.SMTP_USER_NAME);
+        mSmtpPassword = wrappedData.extractString(OmtpConstants.SMTP_PASSWORD);
+    }
+
+    /**
+     * @return the subscriber's VVM provisioning status.
+     */
+    public String getProvisioningStatus() {
+        return mProvisioningStatus;
+    }
+
+    /**
+     * @return the return-code of the status SMS.
+     */
+    public String getReturnCode() {
+        return mStatusReturnCode;
+    }
+
+    /**
+     * @return the URL of the voicemail server. This is the URL to send the users to for subscribing
+     * to the visual voicemail service.
+     */
+    public String getSubscriptionUrl() {
+        return mSubscriptionUrl;
+    }
+
+    /**
+     * @return the voicemail server address. Either server IP address or fully qualified domain
+     * name.
+     */
+    public String getServerAddress() {
+        return mServerAddress;
+    }
+
+    /**
+     * @return the Telephony User Interface number to call to access voicemails directly from the
+     * IVR.
+     */
+    public String getTuiAccessNumber() {
+        return mTuiAccessNumber;
+    }
+
+    /**
+     * @return the number to which client originated SMSes should be sent to.
+     */
+    public String getClientSmsDestinationNumber() {
+        return mClientSmsDestinationNumber;
+    }
+
+    /**
+     * @return the IMAP server port to talk to.
+     */
+    public String getImapPort() {
+        return mImapPort;
+    }
+
+    /**
+     * @return the IMAP user name to be used for authentication.
+     */
+    public String getImapUserName() {
+        return mImapUserName;
+    }
+
+    /**
+     * @return the IMAP password to be used for authentication.
+     */
+    public String getImapPassword() {
+        return mImapPassword;
+    }
+
+    /**
+     * @return the SMTP server port to talk to.
+     */
+    public String getSmtpPort() {
+        return mSmtpPort;
+    }
+
+    /**
+     * @return the SMTP user name to be used for SMTP authentication.
+     */
+    public String getSmtpUserName() {
+        return mSmtpUserName;
+    }
+
+    /**
+     * @return the SMTP password to be used for SMTP authentication.
+     */
+    public String getSmtpPassword() {
+        return mSmtpPassword;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sms/SyncMessage.java b/src/com/android/phone/vvm/omtp/sms/SyncMessage.java
new file mode 100644
index 0000000..9a78a6d
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sms/SyncMessage.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 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.phone.vvm.omtp.sms;
+
+import com.android.phone.vvm.omtp.OmtpConstants;
+
+/**
+ * Structured data representation of an OMTP SYNC message.
+ *
+ * Getters will return null if the field was not set in the message body or it could not be parsed.
+ */
+public class SyncMessage {
+    // Sync event that triggered this message.
+    private final String mSyncTriggerEvent;
+    // Total number of new messages on the server.
+    private final Integer mNewMessageCount;
+    // UID of the new message.
+    private final String mMessageId;
+    // Length of the message.
+    private final Integer mMessageLength;
+    // Content type (voice, video, fax...) of the new message.
+    private final String mContentType;
+    // Sender of the new message.
+    private final String mSender;
+    // Timestamp (in millis) of the new message.
+    private final Long mMsgTimeMillis;
+
+    @Override
+    public String toString() {
+        return "SyncMessage [mSyncTriggerEvent=" + mSyncTriggerEvent
+                + ", mNewMessageCount=" + mNewMessageCount
+                + ", mMessageId=" + mMessageId
+                + ", mMessageLength=" + mMessageLength
+                + ", mContentType=" + mContentType
+                + ", mSender=" + mSender
+                + ", mMsgTimeMillis=" + mMsgTimeMillis + "]";
+    }
+
+    public SyncMessage(WrappedMessageData wrappedData) {
+        mSyncTriggerEvent = wrappedData.extractString(OmtpConstants.SYNC_TRIGGER_EVENT);
+        mMessageId = wrappedData.extractString(OmtpConstants.MESSAGE_UID);
+        mMessageLength = wrappedData.extractInteger(OmtpConstants.MESSAGE_LENGTH);
+        mContentType = wrappedData.extractString(OmtpConstants.CONTENT_TYPE);
+        mSender = wrappedData.extractString(OmtpConstants.SENDER);
+        mNewMessageCount = wrappedData.extractInteger(OmtpConstants.NUM_MESSAGE_COUNT);
+        mMsgTimeMillis = wrappedData.extractTime(OmtpConstants.TIME);
+    }
+
+    /**
+     * @return the event that triggered the sync message. This is a mandatory field and must always
+     * be set.
+     */
+    public String getSyncTriggerEvent() {
+        return mSyncTriggerEvent;
+    }
+
+    /**
+     * @return the number of new messages stored on the voicemail server.
+     */
+    public int getNewMessageCount() {
+        return mNewMessageCount;
+    }
+
+    /**
+     * @return the message ID of the new message.
+     * <p>
+     * Expected to be set only for
+     * {@link com.android.phone.vvm.omtp.OmtpConstants#NEW_MESSAGE}
+     */
+    public String getId() {
+        return mMessageId;
+    }
+
+    /**
+     * @return the content type of the new message.
+     * <p>
+     * Expected to be set only for
+     * {@link com.android.phone.vvm.omtp.OmtpConstants#NEW_MESSAGE}
+     */
+    public String getContentType() {
+        return mContentType;
+    }
+
+    /**
+     * @return the message length of the new message.
+     * <p>
+     * Expected to be set only for
+     * {@link com.android.phone.vvm.omtp.OmtpConstants#NEW_MESSAGE}
+     */
+    public int getLength() {
+        return mMessageLength;
+    }
+
+    /**
+     * @return the sender's phone number of the new message specified as MSISDN.
+     * <p>
+     * Expected to be set only for
+     * {@link com.android.phone.vvm.omtp.OmtpConstants#NEW_MESSAGE}
+     */
+    public String getSender() {
+        return mSender;
+    }
+
+    /**
+     * @return the timestamp as milliseconds for the new message.
+     * <p>
+     * Expected to be set only for
+     * {@link com.android.phone.vvm.omtp.OmtpConstants#NEW_MESSAGE}
+     */
+    public long getTimestampMillis() {
+        return mMsgTimeMillis;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java b/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
new file mode 100644
index 0000000..109dfb2
--- /dev/null
+++ b/src/com/android/phone/vvm/omtp/sms/WrappedMessageData.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 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.phone.vvm.omtp.sms;
+
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.phone.vvm.omtp.OmtpConstants;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Class wrapping the raw OMTP message data, internally represented as as map of all key-value pairs
+ * found in the SMS body.
+ * <p>
+ * Provides convenience methods to extract parse fields of different types.
+ * <p>
+ * All the methods return null if either the field was not present or it could not be parsed.
+ */
+public class WrappedMessageData {
+    private final String TAG = "WrappedMessageData";
+    private final String mPrefix;
+    private final Map<String, String> mFields;
+
+    @Override
+    public String toString() {
+        return "WrappedMessageData [mFields=" + mFields + "]";
+    }
+
+    WrappedMessageData(String prefix, Map<String, String> keyValues) {
+        mPrefix = prefix;
+        mFields = new ArrayMap<String, String>();
+        mFields.putAll(keyValues);
+    }
+
+    /**
+     * @return The String prefix of the message, designating whether this is the message data of a
+     * STATUS or SYNC sms.
+     */
+    String getPrefix() {
+        return mPrefix;
+    }
+
+    /**
+     * Extracts the requested field from underlying data and returns the String value as is.
+     *
+     * @param field The requested field.
+     * @return the parsed string value, or null if the field was not present or not valid.
+     */
+    String extractString(final String field) {
+        String value = mFields.get(field);
+        String[] possibleValues = OmtpConstants.possibleValuesMap.get(field);
+        if (possibleValues == null) {
+            return value;
+        }
+        for (int i = 0; i < possibleValues.length; i++) {
+            if (TextUtils.equals(value, possibleValues[i])) {
+                return value;
+            }
+        }
+        Log.e(TAG, "extractString - value \"" + value +
+                "\" of field \"" + field + "\" is not allowed.");
+        return null;
+    }
+
+    /**
+     * Extracts the requested field from underlying data and parses it as an {@link Integer}.
+     *
+     * @param field The requested field.
+     * @return the parsed integer value, or null if the field was not present.
+     */
+    Integer extractInteger(final String field) {
+        String value = mFields.get(field);
+        if (value == null) {
+            return null;
+        }
+
+        try {
+            return Integer.decode(value);
+        } catch (NumberFormatException e) {
+            Log.e(TAG, "extractInteger - could not parse integer: " + value);
+            return null;
+        }
+    }
+
+    /**
+     * Extracts the requested field from underlying data and parses it as a date/time represented in
+     * {@link OmtpConstants#DATE_TIME_FORMAT} format.
+     *
+     * @param field The requested field.
+     * @return the parsed string value, or null if the field was not present.
+     */
+    Long extractTime(final String field) {
+        String value = mFields.get(field);
+        if (value == null) {
+            return null;
+        }
+
+        try {
+            return new SimpleDateFormat(
+                    OmtpConstants.DATE_TIME_FORMAT, Locale.US).parse(value).getTime();
+        } catch (ParseException e) {
+            Log.e(TAG, "extractTime - could not parse time: " + value);
+            return null;
+        }
+    }
+}
\ No newline at end of file