Handle Verizon iPhone WAP push for VVM

Verizon iPhone WAP push is a pure ASCII message, which is used to be
dropped by the system.

this CL will attempt to parse such SMS as an "alternative" VVM format.
The filter will not drop the SMS.

Change-Id: I7ea68bfa7a0bdc190fdc86c85c0e532cbf301e74
Fixes: 30123702
diff --git a/src/java/com/android/internal/telephony/VisualVoicemailSmsFilter.java b/src/java/com/android/internal/telephony/VisualVoicemailSmsFilter.java
index 055d574..ce991db 100644
--- a/src/java/com/android/internal/telephony/VisualVoicemailSmsFilter.java
+++ b/src/java/com/android/internal/telephony/VisualVoicemailSmsFilter.java
@@ -15,19 +15,18 @@
  */
 package com.android.internal.telephony;
 
-import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
-import android.os.Bundle;
 import android.provider.VoicemailContract;
+import android.telephony.SmsMessage;
 import android.telephony.TelephonyManager;
 import android.telephony.VisualVoicemailSmsFilterSettings;
 import android.util.Log;
 
-import android.telephony.SmsMessage;
-
 import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
 
+import java.nio.charset.StandardCharsets;
+
 public class VisualVoicemailSmsFilter {
 
     private static final String TAG = "VvmSmsFilter";
@@ -61,35 +60,50 @@
         // TODO: filter base on originating number and destination port.
 
         String messageBody = getFullMessage(pdus, format);
+
         if(messageBody == null){
+            // Verizon WAP push SMS is not recognized by android, which has a ascii PDU.
+            // Attempt to parse it.
+            Log.i(TAG, "Unparsable SMS received");
+            String asciiMessage = parseAsciiPduMessage(pdus);
+            WrappedMessageData messageData = VisualVoicemailSmsParser
+                .parseAlternativeFormat(asciiMessage);
+            if (messageData != null) {
+                sendVvmSmsBroadcast(context, vvmClientPackage, subId, messageData);
+            }
+            // Confidence for what the message actually is is low. Don't remove the message and let
+            // system decide. Usually because it is not parsable it will be dropped.
             return false;
+        } else {
+            String clientPrefix = settings.clientPrefix;
+            WrappedMessageData messageData = VisualVoicemailSmsParser
+                .parse(clientPrefix, messageBody);
+            if (messageData != null) {
+                sendVvmSmsBroadcast(context, vvmClientPackage, subId, messageData);
+                return true;
+            }
         }
-        String clientPrefix = settings.clientPrefix;
-
-        WrappedMessageData messageData = VisualVoicemailSmsParser.parse(clientPrefix, messageBody);
-        if (messageData != null) {
-            Log.i(TAG, "VVM SMS received");
-            Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
-            intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_PREFIX, messageData.prefix);
-            intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_FIELDS, messageData.fields);
-            intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_SUBID, subId);
-            intent.setPackage(vvmClientPackage);
-            context.sendBroadcast(intent);
-            return true;
-        }
-
         return false;
     }
 
+    private static void sendVvmSmsBroadcast(Context context, String vvmClientPackage, int subId,
+        WrappedMessageData messageData) {
+        Log.i(TAG, "VVM SMS received");
+        Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
+        intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_PREFIX, messageData.prefix);
+        intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_FIELDS, messageData.fields);
+        intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS_SUBID, subId);
+        intent.setPackage(vvmClientPackage);
+        context.sendBroadcast(intent);
+    }
+
     private static String getFullMessage(byte[][] pdus, String format) {
         StringBuilder builder = new StringBuilder();
         for (byte pdu[] : pdus) {
             SmsMessage message =SmsMessage.createFromPdu(pdu, format);
 
-            if(message == null || message.mWrappedSmsMessage == null) {
-                // b/29123941 Certain PDU will cause createFromPdu() to return a SmsMessage with
-                // null mWrappedSmsMessage, throwing NPE on any method called. In this case, just
-                // ignore the message.
+            if (message == null) {
+                // The PDU is not recognized by android
                 return null;
             }
             String body = message.getMessageBody();
@@ -99,4 +113,12 @@
         }
         return builder.toString();
     }
+
+    private static String parseAsciiPduMessage(byte[][] pdus) {
+        StringBuilder builder = new StringBuilder();
+        for (byte pdu[] : pdus) {
+            builder.append(new String(pdu, StandardCharsets.US_ASCII));
+        }
+        return builder.toString();
+    }
 }
diff --git a/src/java/com/android/internal/telephony/VisualVoicemailSmsParser.java b/src/java/com/android/internal/telephony/VisualVoicemailSmsParser.java
index 0de37d5..b6b3202 100644
--- a/src/java/com/android/internal/telephony/VisualVoicemailSmsParser.java
+++ b/src/java/com/android/internal/telephony/VisualVoicemailSmsParser.java
@@ -20,6 +20,10 @@
 
 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
@@ -80,6 +84,7 @@
      * @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();
@@ -107,4 +112,42 @@
 
         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;
+    }
 }
diff --git a/tests/telephonytests/src/com/android/internal/telephony/VisualVoicemailSmsParserTest.java b/tests/telephonytests/src/com/android/internal/telephony/VisualVoicemailSmsParserTest.java
index 6caea7d..12a4655 100644
--- a/tests/telephonytests/src/com/android/internal/telephony/VisualVoicemailSmsParserTest.java
+++ b/tests/telephonytests/src/com/android/internal/telephony/VisualVoicemailSmsParserTest.java
@@ -16,9 +16,7 @@
 package com.android.internal.telephony;
 
 import android.test.suitebuilder.annotation.SmallTest;
-
 import com.android.internal.telephony.VisualVoicemailSmsParser.WrappedMessageData;
-
 import junit.framework.TestCase;
 
 public class VisualVoicemailSmsParserTest extends TestCase {
@@ -147,4 +145,42 @@
         assertEquals("STATUS", result.prefix);
         assertEquals("", result.fields.getString("key"));
     }
+
+    @SmallTest
+    public void testAlternativeParsing_Mboxupdate() {
+        WrappedMessageData result = VisualVoicemailSmsParser.parseAlternativeFormat(
+            "MBOXUPDATE?m=1;server=example.com;port=143;name=foo@example.com;pw=bar");
+
+        assertEquals("MBOXUPDATE", result.prefix);
+        assertEquals("1", result.fields.getString("m"));
+        assertEquals("example.com", result.fields.getString("server"));
+        assertEquals("143", result.fields.getString("port"));
+        assertEquals("foo@example.com", result.fields.getString("name"));
+        assertEquals("bar", result.fields.getString("pw"));
+    }
+
+    @SmallTest
+    public void testAlternativeParsing_Unrecognized() {
+        WrappedMessageData result = VisualVoicemailSmsParser.parseAlternativeFormat(
+            "UNRECOGNIZED?cmd=STATUS");
+
+        assertEquals("UNRECOGNIZED", result.prefix);
+        assertEquals("STATUS", result.fields.getString("cmd"));
+    }
+
+    @SmallTest
+    public void testAlternativeParsingFail_MissingSeparator() {
+        WrappedMessageData result = VisualVoicemailSmsParser.parseAlternativeFormat(
+            "I send SMS in weird formats");
+
+        assertNull(result);
+    }
+
+    @SmallTest
+    public void testAlternativeParsingFail_NotWhitelistedEvent() {
+        WrappedMessageData result = VisualVoicemailSmsParser.parseAlternativeFormat(
+            "AreYouStillThere?");
+
+        assertNull(result);
+    }
 }