Allowing multiline addresses to support microsoft vCard.

Multiline addresses are not allowed in vCard 2.1 but it has been encountered
in microsoft mobile vCard. This adds support for this by checking the next
line.  If the next line contains a colon, we assume it's a new property.
Otherwise, we assume it's a continuation.

Bug: 5188556
Change-Id: I4607eda901a1d535c759b5026f6702eb47290cb8
diff --git a/java/com/android/vcard/VCardParserImpl_V21.java b/java/com/android/vcard/VCardParserImpl_V21.java
index 410cf4f..9d802a5 100644
--- a/java/com/android/vcard/VCardParserImpl_V21.java
+++ b/java/com/android/vcard/VCardParserImpl_V21.java
@@ -584,41 +584,7 @@
         if (propertyNameUpper.equals(VCardConstants.PROPERTY_ADR)
                 || propertyNameUpper.equals(VCardConstants.PROPERTY_ORG)
                 || propertyNameUpper.equals(VCardConstants.PROPERTY_N)) {
-            List<String> encodedValueList = new ArrayList<String>();
-
-            // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some softwares/devices emit
-            // such data.
-            if (mCurrentEncoding.equalsIgnoreCase(VCardConstants.PARAM_ENCODING_QP)) {
-                // First we retrieve Quoted-Printable String from vCard entry, which may include
-                // multiple lines.
-                final String quotedPrintablePart = getQuotedPrintablePart(propertyRawValue);
-
-                // "Raw value" from the view of users should contain all part of QP string.
-                // TODO: add test for this handling
-                property.setRawValue(quotedPrintablePart);
-
-                // We split Quoted-Printable String using semi-colon before decoding it, as
-                // the Quoted-Printable may have semi-colon, which confuses splitter.
-                final List<String> quotedPrintableValueList =
-                        VCardUtils.constructListFromValue(quotedPrintablePart, getVersion());
-                for (String quotedPrintableValue : quotedPrintableValueList) {
-                    String encoded = VCardUtils.parseQuotedPrintable(quotedPrintableValue,
-                            false, sourceCharset, targetCharset);
-                    encodedValueList.add(encoded);
-                }
-            } else {
-                final List<String> rawValueList =
-                    VCardUtils.constructListFromValue(propertyRawValue, getVersion());
-                for (String rawValue : rawValueList) {
-                    encodedValueList.add(VCardUtils.convertStringCharset(
-                            rawValue, sourceCharset, targetCharset));
-                }
-            }
-
-            property.setValues(encodedValueList);
-            for (VCardInterpreter interpreter : mInterpreterList) {
-                interpreter.onPropertyCreated(property);
-            }
+            handleAdrOrgN(property, propertyRawValue, sourceCharset, targetCharset);
             return;
         }
 
@@ -723,6 +689,46 @@
         }
     }
 
+    private void handleAdrOrgN(VCardProperty property, String propertyRawValue,
+            String sourceCharset, String targetCharset) throws VCardException, IOException {
+        List<String> encodedValueList = new ArrayList<String>();
+
+        // vCard 2.1 does not allow QUOTED-PRINTABLE here, but some softwares/devices emit
+        // such data.
+        if (mCurrentEncoding.equalsIgnoreCase(VCardConstants.PARAM_ENCODING_QP)) {
+            // First we retrieve Quoted-Printable String from vCard entry, which may include
+            // multiple lines.
+            final String quotedPrintablePart = getQuotedPrintablePart(propertyRawValue);
+
+            // "Raw value" from the view of users should contain all part of QP string.
+            // TODO: add test for this handling
+            property.setRawValue(quotedPrintablePart);
+
+            // We split Quoted-Printable String using semi-colon before decoding it, as
+            // the Quoted-Printable may have semi-colon, which confuses splitter.
+            final List<String> quotedPrintableValueList =
+                    VCardUtils.constructListFromValue(quotedPrintablePart, getVersion());
+            for (String quotedPrintableValue : quotedPrintableValueList) {
+                String encoded = VCardUtils.parseQuotedPrintable(quotedPrintableValue,
+                        false, sourceCharset, targetCharset);
+                encodedValueList.add(encoded);
+            }
+        } else {
+            final String propertyValue = getPotentialMultiline(propertyRawValue);
+            final List<String> rawValueList =
+                    VCardUtils.constructListFromValue(propertyValue, getVersion());
+            for (String rawValue : rawValueList) {
+                encodedValueList.add(VCardUtils.convertStringCharset(
+                        rawValue, sourceCharset, targetCharset));
+            }
+        }
+
+        property.setValues(encodedValueList);
+        for (VCardInterpreter interpreter : mInterpreterList) {
+            interpreter.onPropertyCreated(property);
+        }
+    }
+
     /**
      * <p>
      * Parses and returns Quoted-Printable.
@@ -783,6 +789,40 @@
         }
     }
 
+    /**
+     * Given the first line of a property, checks consecutive lines after it and builds a new
+     * multi-line value if it exists.
+     *
+     * @param firstString The first line of the property.
+     * @return A new property, potentially built from multiple lines.
+     * @throws IOException
+     */
+    private String getPotentialMultiline(String firstString) throws IOException {
+        final StringBuilder builder = new StringBuilder();
+        builder.append(firstString);
+
+        while (true) {
+            final String line = peekLine();
+            if (line == null || line.length() == 0) {
+                break;
+            }
+
+            final String propertyName = getPropertyNameUpperCase(line);
+            if (propertyName != null) {
+                break;
+            }
+
+            // vCard 2.1 does not allow multi-line of adr but microsoft vcards may have it.
+            // We will consider the next line to be a part of a multi-line value if it does not
+            // contain a property name (i.e. a colon or semi-colon).
+            // Consume the line.
+            getLine();
+            builder.append(" ").append(line);
+        }
+
+        return builder.toString();
+    }
+
     protected String getBase64(String firstString) throws IOException, VCardException {
         final StringBuilder builder = new StringBuilder();
         builder.append(firstString);
@@ -800,26 +840,13 @@
             //      TEL;TYPE=WORK:+5555555
             // or
             //      END:VCARD
-            int colonIndex = line.indexOf(":");
-            int semiColonIndex = line.indexOf(";");
-            if (colonIndex > -1 || semiColonIndex > -1) {
-                // Find the minimum index that is greater than -1.
-                final int minIndex;
-                if (colonIndex == -1) {
-                    minIndex = semiColonIndex;
-                } else if (semiColonIndex == -1) {
-                    minIndex = colonIndex;
-                } else {
-                    minIndex = Math.min(colonIndex, semiColonIndex);
-                }
-
-                if (getKnownPropertyNameSet().contains(line.substring(0, minIndex).toUpperCase())) {
-                    Log.w(LOG_TAG, "Found a next property during parsing a BASE64 string, " +
-                            "which must not contain semi-colon or colon. Treat the line as next "
-                            + "property.");
-                    Log.w(LOG_TAG, "Problematic line: " + line.trim());
-                    break;
-                }
+            String propertyName = getPropertyNameUpperCase(line);
+            if (getKnownPropertyNameSet().contains(propertyName)) {
+                Log.w(LOG_TAG, "Found a next property during parsing a BASE64 string, " +
+                        "which must not contain semi-colon or colon. Treat the line as next "
+                        + "property.");
+                Log.w(LOG_TAG, "Problematic line: " + line.trim());
+                break;
             }
 
             // Consume the line.
@@ -834,6 +861,38 @@
         return builder.toString();
     }
 
+    /**
+     * Extracts the property name portion of a given vCard line.
+     * <p>
+     * Properties must contain a colon.
+     * <p>
+     * E.g.
+     *      TEL;TYPE=WORK:+5555555  // returns "TEL"
+     *      END:VCARD // returns "END"
+     *      TEL; // returns null
+     *
+     * @param line The vCard line.
+     * @return The property name portion. {@literal null} if no property name found.
+     */
+    private String getPropertyNameUpperCase(String line) {
+        final int colonIndex = line.indexOf(":");
+        if (colonIndex > -1) {
+            final int semiColonIndex = line.indexOf(";");
+
+            // Find the minimum index that is greater than -1.
+            final int minIndex;
+            if (colonIndex == -1) {
+                minIndex = semiColonIndex;
+            } else if (semiColonIndex == -1) {
+                minIndex = colonIndex;
+            } else {
+                minIndex = Math.min(colonIndex, semiColonIndex);
+            }
+            return line.substring(0, minIndex).toUpperCase();
+        }
+        return null;
+    }
+
     /*
      * vCard 2.1 specifies AGENT allows one vcard entry. Currently we emit an
      * error toward the AGENT property.
diff --git a/tests/res/raw/v21_adr_multiple_line.vcf b/tests/res/raw/v21_adr_multiple_line.vcf
new file mode 100644
index 0000000..4b24321
--- /dev/null
+++ b/tests/res/raw/v21_adr_multiple_line.vcf
@@ -0,0 +1,10 @@
+BEGIN:VCARD

+VERSION:2.1

+N:bogus

+URL:http://bogus.com/

+ADR;HOME:;;Grindelberg 999;Hamburg;;99999;Deutschland

+ADR;HOME:;;Hermann v. Brevern\

+9999999\

+Packstation 999;Hamburg;;99999;Deutschland

+BDAY;VALUE=DATE:20081203

+END:VCARD

diff --git a/tests/src/com/android/vcard/tests/VCardImporterTests.java b/tests/src/com/android/vcard/tests/VCardImporterTests.java
index 3279430..bcf2a8a 100644
--- a/tests/src/com/android/vcard/tests/VCardImporterTests.java
+++ b/tests/src/com/android/vcard/tests/VCardImporterTests.java
@@ -694,6 +694,27 @@
                 .put(Email.ADDRESS, "\"Omega\" <omega@example.com>");
     }
 
+    public void testAdrMultipleLineV21() {
+        ContentValues contentValuesForValue = new ContentValues();
+        contentValuesForValue.put("VALUE", "DATE");
+
+        mVerifier.initForImportTest(V21, R.raw.v21_adr_multiple_line);
+        mVerifier.addPropertyNodesVerifierElem()
+                .addExpectedNodeWithOrder("N", "bogus")
+                .addExpectedNodeWithOrder("URL", "http://bogus.com/")
+                .addExpectedNodeWithOrder("ADR",
+                        ";;Grindelberg 999;Hamburg;;99999;Deutschland",
+                        Arrays.asList("", "", "Grindelberg 999", "Hamburg", "", "99999",
+                                "Deutschland"),
+                        new TypeSet("HOME"))
+                .addExpectedNodeWithOrder("ADR", ";;Hermann v. Brevern\\ 9999999\\ " +
+                        "Packstation 999;Hamburg;;99999;Deutschland",
+                        Arrays.asList("", "", "Hermann v. Brevern\\ 9999999\\ Packstation 999",
+                                "Hamburg", "", "99999", "Deutschland"),
+                        new TypeSet("HOME"))
+                .addExpectedNodeWithOrder("BDAY", "20081203", contentValuesForValue);
+    }
+
     public void testV30Simple_Parsing() {
         mVerifier.initForImportTest(V30, R.raw.v30_simple);
         mVerifier.addPropertyNodesVerifierElem()
@@ -1220,8 +1241,8 @@
     public void testBase64Without2CrLfForBlackBerry_Parse() {
         mVerifier.initForImportTest(V21, R.raw.v21_blackberry_photo);
         mVerifier.addPropertyNodesVerifierElem()
-                .addExpectedNodeWithOrder("FN", "fullname")
-                .addExpectedNodeWithOrder("N", "name")
+                .addExpectedNodeWithOrder("FN", "boogie")
+                .addExpectedNodeWithOrder("N", "boogie")
                 .addExpectedNodeWithOrder("PHOTO", null,
                         null, sPhotoByteArrayForComplicatedCase, mContentValuesForBase64V21,
                         null, null)