Make vCard importer/exporter aware of multi-byte parameters.
Bug: 2922186
Change-Id: Ic877940242d87ef918bf8d4dac601d37b296259b
diff --git a/core/java/android/pim/vcard/VCardBuilder.java b/core/java/android/pim/vcard/VCardBuilder.java
index 1da6d7a..d634672 100644
--- a/core/java/android/pim/vcard/VCardBuilder.java
+++ b/core/java/android/pim/vcard/VCardBuilder.java
@@ -1463,6 +1463,9 @@
parameterList.add(VCardConstants.PARAM_TYPE_VOICE);
} else if (VCardUtils.isMobilePhoneLabel(label)) {
parameterList.add(VCardConstants.PARAM_TYPE_CELL);
+ } else if (mIsV30) {
+ // This label is appropriately encoded in appendTypeParameters.
+ parameterList.add(label);
} else {
final String upperLabel = label.toUpperCase();
if (VCardUtils.isValidInV21ButUnknownToContactsPhoteType(upperLabel)) {
@@ -1741,21 +1744,30 @@
// which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
boolean first = true;
for (final String typeValue : types) {
- // Note: vCard 3.0 specifies the different type of acceptable type Strings, but
- // we don't emit that kind of vCard 3.0 specific type since there should be
- // high probabilyty in which external importers cannot understand them.
- //
- // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they
- // are quoted.)
- if (!VCardUtils.isV21Word(typeValue)) {
- continue;
+ if (VCardConfig.isV30(mVCardType)) {
+ // Note: vCard 3.0 specifies the different type of acceptable type Strings, but
+ // we don't emit that kind of vCard 3.0 specific type since there should be
+ // high probabilyty in which external importers cannot understand them.
+ //
+ // e.g. TYPE="\u578B\u306B\u3087" (vCard 3.0 allows non-Ascii characters if they
+ // are quoted.)
+ if (first) {
+ first = false;
+ } else {
+ mBuilder.append(VCARD_PARAM_SEPARATOR);
+ }
+ appendTypeParameter(VCardUtils.toStringAvailableAsV30ParameValue(typeValue));
+ } else { // vCard 2.1
+ if (!VCardUtils.isV21Word(typeValue)) {
+ continue;
+ }
+ if (first) {
+ first = false;
+ } else {
+ mBuilder.append(VCARD_PARAM_SEPARATOR);
+ }
+ appendTypeParameter(typeValue);
}
- if (first) {
- first = false;
- } else {
- mBuilder.append(VCARD_PARAM_SEPARATOR);
- }
- appendTypeParameter(typeValue);
}
}
diff --git a/core/java/android/pim/vcard/VCardEntryConstructor.java b/core/java/android/pim/vcard/VCardEntryConstructor.java
index 290ca2b..ae4ec29 100644
--- a/core/java/android/pim/vcard/VCardEntryConstructor.java
+++ b/core/java/android/pim/vcard/VCardEntryConstructor.java
@@ -157,11 +157,15 @@
mParamType = type;
}
+ @Override
public void propertyParamValue(String value) {
if (mParamType == null) {
// From vCard 2.1 specification. vCard 3.0 formally does not allow this case.
mParamType = "TYPE";
}
+ if (!VCardUtils.containsOnlyAlphaDigitHyphen(value)) {
+ value = encodeString(value, mCharsetForDecodedBytes);
+ }
mCurrentProperty.addParameter(mParamType, value);
mParamType = null;
}
diff --git a/core/java/android/pim/vcard/VCardUtils.java b/core/java/android/pim/vcard/VCardUtils.java
index 11b112b..f972799 100644
--- a/core/java/android/pim/vcard/VCardUtils.java
+++ b/core/java/android/pim/vcard/VCardUtils.java
@@ -16,10 +16,10 @@
package android.pim.vcard;
import android.content.ContentProviderOperation;
-import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Data;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
@@ -477,6 +477,43 @@
return true;
}
+ /**
+ * <P>
+ * Returns String available as parameter value in vCard 3.0.
+ * </P>
+ * <P>
+ * RFC 2426 requires vCard composer to quote parameter values when it contains
+ * semi-colon, for example (See RFC 2426 for more information).
+ * This method checks whether the given String can be used without quotes.
+ * </P>
+ * <P>
+ * Note: We remove DQUOTE silently for now.
+ * </P>
+ */
+ public static String toStringAvailableAsV30ParameValue(String value) {
+ if (TextUtils.isEmpty(value)) {
+ value = "";
+ }
+ final int asciiFirst = 0x20;
+ final int asciiLast = 0x7E; // included
+ final StringBuilder builder = new StringBuilder();
+ final int length = value.length();
+ boolean needQuote = false;
+ for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
+ final int codePoint = value.codePointAt(i);
+ if (codePoint < asciiFirst || codePoint == '"') {
+ // CTL characters and DQUOTE are never accepted. Remove them.
+ continue;
+ }
+ builder.appendCodePoint(codePoint);
+ if (codePoint == ':' || codePoint == ',' || codePoint == ' ') {
+ needQuote = true;
+ }
+ }
+ final String result = builder.toString();
+ return ((needQuote || result.isEmpty()) ? ('"' + result + '"') : result);
+ }
+
public static String toHalfWidthString(final String orgString) {
if (TextUtils.isEmpty(orgString)) {
return null;
diff --git a/core/tests/coretests/res/raw/v30_multibyte_param.vcf b/core/tests/coretests/res/raw/v30_multibyte_param.vcf
new file mode 100644
index 0000000..cd200e5
--- /dev/null
+++ b/core/tests/coretests/res/raw/v30_multibyte_param.vcf
@@ -0,0 +1,5 @@
+BEGIN:VCARD
+VERSION:3.0
+N:F;G;M;;
+TEL;TYPE="่ดน":1
+END:VCARD
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java b/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
index 21f2254..e0e1f87 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardImporterTests.java
@@ -1008,4 +1008,26 @@
.put(Phone.TYPE, Phone.TYPE_PAGER)
.put(Phone.NUMBER, "6101231234@pagersample.com");
}
+
+ public void testMultiBytePropV30_Parse() {
+ mVerifier.initForImportTest(V30, R.raw.v30_multibyte_param);
+ mVerifier.addPropertyNodesVerifierElem()
+ .addExpectedNodeWithOrder("VERSION", "3.0")
+ .addExpectedNodeWithOrder("N", Arrays.asList("F", "G", "M", "", ""))
+ .addExpectedNodeWithOrder("TEL", "1", new TypeSet("\u8D39"));
+ }
+
+ public void testMultiBytePropV30() {
+ mVerifier.initForImportTest(V30, R.raw.v30_multibyte_param);
+ final ContentValuesVerifierElem elem = mVerifier.addContentValuesVerifierElem();
+ elem.addExpected(StructuredName.CONTENT_ITEM_TYPE)
+ .put(StructuredName.FAMILY_NAME, "F")
+ .put(StructuredName.MIDDLE_NAME, "M")
+ .put(StructuredName.GIVEN_NAME, "G")
+ .put(StructuredName.DISPLAY_NAME, "G M F");
+ elem.addExpected(Phone.CONTENT_ITEM_TYPE)
+ .put(Phone.TYPE, Phone.TYPE_CUSTOM)
+ .put(Phone.LABEL, "\u8D39")
+ .put(Phone.NUMBER, "1");
+ }
}
diff --git a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java b/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
index 59299f9..e805bee 100644
--- a/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
+++ b/core/tests/coretests/src/android/pim/vcard/VCardUtilsTests.java
@@ -82,4 +82,34 @@
assertFalse(VCardUtils.containsOnlyAlphaDigitHyphen(String.valueOf((char)i)));
}
}
+
+ public void testToStringAvailableAsV30ParamValue() {
+ // Smoke tests.
+ assertEquals("HOME", VCardUtils.toStringAvailableAsV30ParameValue("HOME"));
+ assertEquals("TEL", VCardUtils.toStringAvailableAsV30ParameValue("TEL"));
+ assertEquals("PAGER", VCardUtils.toStringAvailableAsV30ParameValue("PAGER"));
+
+ assertEquals("\"\"", VCardUtils.toStringAvailableAsV30ParameValue(""));
+
+ // non-Ascii must be allowed
+ assertEquals("\u4E8B\u52D9\u6240",
+ VCardUtils.toStringAvailableAsV30ParameValue("\u4E8B\u52D9\u6240"));
+ // Reported as bug report.
+ assertEquals("\u8D39", VCardUtils.toStringAvailableAsV30ParameValue("\u8D39"));
+ assertEquals("\"comma,separated\"",
+ VCardUtils.toStringAvailableAsV30ParameValue("comma,separated"));
+ assertEquals("\"colon:aware\"",
+ VCardUtils.toStringAvailableAsV30ParameValue("colon:aware"));
+ // CTL characters.
+ assertEquals("CTLExample",
+ VCardUtils.toStringAvailableAsV30ParameValue("CTL\u0001Example"));
+ // DQUOTE must be removed.
+ assertEquals("quoted",
+ VCardUtils.toStringAvailableAsV30ParameValue("\"quoted\""));
+ // DQUOTE must be removed basically, but we should detect a space, which
+ // require us to use DQUOTE again.
+ // Right-side has one more illegal dquote to test quote-handle code thoroughly.
+ assertEquals("\"Already quoted\"",
+ VCardUtils.toStringAvailableAsV30ParameValue("\"Already quoted\"\""));
+ }
}