1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.vcard;
17 
18 import android.provider.ContactsContract.CommonDataKinds.Im;
19 import android.provider.ContactsContract.CommonDataKinds.Phone;
20 import android.telephony.PhoneNumberUtils;
21 import android.text.SpannableStringBuilder;
22 import android.text.TextUtils;
23 import android.util.Log;
24 
25 import com.android.vcard.exception.VCardException;
26 
27 import java.io.ByteArrayOutputStream;
28 import java.io.UnsupportedEncodingException;
29 import java.nio.ByteBuffer;
30 import java.nio.charset.Charset;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.HashMap;
35 import java.util.HashSet;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Set;
39 
40 /**
41  * Utilities for VCard handling codes.
42  */
43 public class VCardUtils {
44     private static final String LOG_TAG = VCardConstants.LOG_TAG;
45 
46     /**
47      * See org.apache.commons.codec.DecoderException
48      */
49     private static class DecoderException extends Exception {
DecoderException(String pMessage)50         public DecoderException(String pMessage) {
51             super(pMessage);
52         }
53     }
54 
55     /**
56      * See org.apache.commons.codec.net.QuotedPrintableCodec
57      */
58     private static class QuotedPrintableCodecPort {
59         private static byte ESCAPE_CHAR = '=';
decodeQuotedPrintable(byte[] bytes)60         public static final byte[] decodeQuotedPrintable(byte[] bytes)
61                 throws DecoderException {
62             if (bytes == null) {
63                 return null;
64             }
65             ByteArrayOutputStream buffer = new ByteArrayOutputStream();
66             for (int i = 0; i < bytes.length; i++) {
67                 int b = bytes[i];
68                 if (b == ESCAPE_CHAR) {
69                     try {
70                         int u = Character.digit((char) bytes[++i], 16);
71                         int l = Character.digit((char) bytes[++i], 16);
72                         if (u == -1 || l == -1) {
73                             throw new DecoderException("Invalid quoted-printable encoding");
74                         }
75                         buffer.write((char) ((u << 4) + l));
76                     } catch (ArrayIndexOutOfBoundsException e) {
77                         throw new DecoderException("Invalid quoted-printable encoding");
78                     }
79                 } else {
80                     buffer.write(b);
81                 }
82             }
83             return buffer.toByteArray();
84         }
85     }
86 
87     /**
88      * Ported methods which are hidden in {@link PhoneNumberUtils}.
89      */
90     public static class PhoneNumberUtilsPort {
formatNumber(String source, int defaultFormattingType)91         public static String formatNumber(String source, int defaultFormattingType) {
92             final SpannableStringBuilder text = new SpannableStringBuilder(source);
93             PhoneNumberUtils.formatNumber(text, defaultFormattingType);
94             return text.toString();
95         }
96     }
97 
98     /**
99      * Ported methods which are hidden in {@link TextUtils}.
100      */
101     public static class TextUtilsPort {
isPrintableAscii(final char c)102         public static boolean isPrintableAscii(final char c) {
103             final int asciiFirst = 0x20;
104             final int asciiLast = 0x7E;  // included
105             return (asciiFirst <= c && c <= asciiLast) || c == '\r' || c == '\n';
106         }
107 
isPrintableAsciiOnly(final CharSequence str)108         public static boolean isPrintableAsciiOnly(final CharSequence str) {
109             final int len = str.length();
110             for (int i = 0; i < len; i++) {
111                 if (!isPrintableAscii(str.charAt(i))) {
112                     return false;
113                 }
114             }
115             return true;
116         }
117     }
118 
119     // Note that not all types are included in this map/set, since, for example, TYPE_HOME_FAX is
120     // converted to two parameter Strings. These only contain some minor fields valid in both
121     // vCard and current (as of 2009-08-07) Contacts structure.
122     private static final Map<Integer, String> sKnownPhoneTypesMap_ItoS;
123     private static final Set<String> sPhoneTypesUnknownToContactsSet;
124     private static final Map<String, Integer> sKnownPhoneTypeMap_StoI;
125     private static final Map<Integer, String> sKnownImPropNameMap_ItoS;
126     private static final Set<String> sMobilePhoneLabelSet;
127 
128     static {
129         sKnownPhoneTypesMap_ItoS = new HashMap<Integer, String>();
130         sKnownPhoneTypeMap_StoI = new HashMap<String, Integer>();
131 
sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR)132         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_CAR, VCardConstants.PARAM_TYPE_CAR);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR)133         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CAR, Phone.TYPE_CAR);
sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER)134         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_PAGER, VCardConstants.PARAM_TYPE_PAGER);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER)135         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_PAGER, Phone.TYPE_PAGER);
sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN)136         sKnownPhoneTypesMap_ItoS.put(Phone.TYPE_ISDN, VCardConstants.PARAM_TYPE_ISDN);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN)137         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_ISDN, Phone.TYPE_ISDN);
138 
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME)139         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_HOME, Phone.TYPE_HOME);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK)140         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_WORK, Phone.TYPE_WORK);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE)141         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_CELL, Phone.TYPE_MOBILE);
142 
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER)143         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_OTHER, Phone.TYPE_OTHER);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK, Phone.TYPE_CALLBACK)144         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_CALLBACK,
145                 Phone.TYPE_CALLBACK);
sKnownPhoneTypeMap_StoI.put( VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN)146         sKnownPhoneTypeMap_StoI.put(
147                 VCardConstants.PARAM_PHONE_EXTRA_TYPE_COMPANY_MAIN, Phone.TYPE_COMPANY_MAIN);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO)148         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_RADIO, Phone.TYPE_RADIO);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD, Phone.TYPE_TTY_TDD)149         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_TTY_TDD,
150                 Phone.TYPE_TTY_TDD);
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT, Phone.TYPE_ASSISTANT)151         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_PHONE_EXTRA_TYPE_ASSISTANT,
152                 Phone.TYPE_ASSISTANT);
153         // OTHER (default in Android) should correspond to VOICE (default in vCard).
sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_VOICE, Phone.TYPE_OTHER)154         sKnownPhoneTypeMap_StoI.put(VCardConstants.PARAM_TYPE_VOICE, Phone.TYPE_OTHER);
155 
156         sPhoneTypesUnknownToContactsSet = new HashSet<String>();
157         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MODEM);
158         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_MSG);
159         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_BBS);
160         sPhoneTypesUnknownToContactsSet.add(VCardConstants.PARAM_TYPE_VIDEO);
161 
162         sKnownImPropNameMap_ItoS = new HashMap<Integer, String>();
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM)163         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN)164         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO)165         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME)166         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK, VCardConstants.PROPERTY_X_GOOGLE_TALK)167         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_GOOGLE_TALK,
168                 VCardConstants.PROPERTY_X_GOOGLE_TALK);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ)169         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER)170         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ)171         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_QQ, VCardConstants.PROPERTY_X_QQ);
sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING)172         sKnownImPropNameMap_ItoS.put(Im.PROTOCOL_NETMEETING, VCardConstants.PROPERTY_X_NETMEETING);
173 
174         // \u643A\u5E2F\u96FB\u8A71 = Full-width Hiragana "Keitai-Denwa" (mobile phone)
175         // \u643A\u5E2F = Full-width Hiragana "Keitai" (mobile phone)
176         // \u30B1\u30A4\u30BF\u30A4 = Full-width Katakana "Keitai" (mobile phone)
177         // \uFF79\uFF72\uFF80\uFF72 = Half-width Katakana "Keitai" (mobile phone)
178         sMobilePhoneLabelSet = new HashSet<String>(Arrays.asList(
179                 "MOBILE", "\u643A\u5E2F\u96FB\u8A71", "\u643A\u5E2F", "\u30B1\u30A4\u30BF\u30A4",
180                 "\uFF79\uFF72\uFF80\uFF72"));
181     }
182 
getPhoneTypeString(Integer type)183     public static String getPhoneTypeString(Integer type) {
184         return sKnownPhoneTypesMap_ItoS.get(type);
185     }
186 
187     /**
188      * Returns Interger when the given types can be parsed as known type. Returns String object
189      * when not, which should be set to label.
190      */
getPhoneTypeFromStrings(Collection<String> types, String number)191     public static Object getPhoneTypeFromStrings(Collection<String> types,
192             String number) {
193         if (number == null) {
194             number = "";
195         }
196         int type = -1;
197         String label = null;
198         boolean isFax = false;
199         boolean hasPref = false;
200 
201         if (types != null) {
202             for (final String typeStringOrg : types) {
203                 if (typeStringOrg == null) {
204                     continue;
205                 }
206                 final String typeStringUpperCase = typeStringOrg.toUpperCase();
207                 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
208                     hasPref = true;
209                 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_FAX)) {
210                     isFax = true;
211                 } else {
212                     final String labelCandidate;
213                     if (typeStringUpperCase.startsWith("X-") && type < 0) {
214                         labelCandidate = typeStringOrg.substring(2);
215                     } else {
216                         labelCandidate = typeStringOrg;
217                     }
218                     if (labelCandidate.length() == 0) {
219                         continue;
220                     }
221                     // e.g. "home" -> TYPE_HOME
222                     final Integer tmp = sKnownPhoneTypeMap_StoI.get(labelCandidate.toUpperCase());
223                     if (tmp != null) {
224                         final int typeCandidate = tmp;
225                         // 1. If a type isn't specified yet, we'll choose the new type candidate.
226                         // 2. If the current type is default one (OTHER) or custom one, we'll
227                         // prefer more specific types specified in the vCard. Note that OTHER and
228                         // the other different types may appear simultaneously here, since vCard
229                         // allow to have VOICE and HOME/WORK in one line.
230                         // e.g. "TEL;WORK;VOICE:1" -> WORK + OTHER -> Type should be WORK
231                         // 3. TYPE_PAGER is prefered when the number contains @ surronded by
232                         // a pager number and a domain name.
233                         // e.g.
234                         // o 1111@domain.com
235                         // x @domain.com
236                         // x 1111@
237                         final int indexOfAt = number.indexOf("@");
238                         if ((typeCandidate == Phone.TYPE_PAGER
239                                 && 0 < indexOfAt && indexOfAt < number.length() - 1)
240                                 || type < 0
241                                 || type == Phone.TYPE_CUSTOM
242                                 || type == Phone.TYPE_OTHER) {
243                             type = tmp;
244                         }
245                     } else if (type < 0) {
246                         type = Phone.TYPE_CUSTOM;
247                         label = labelCandidate;
248                     }
249                 }
250             }
251         }
252         if (type < 0) {
253             if (hasPref) {
254                 type = Phone.TYPE_MAIN;
255             } else {
256                 // default to TYPE_HOME
257                 type = Phone.TYPE_HOME;
258             }
259         }
260         if (isFax) {
261             if (type == Phone.TYPE_HOME) {
262                 type = Phone.TYPE_FAX_HOME;
263             } else if (type == Phone.TYPE_WORK) {
264                 type = Phone.TYPE_FAX_WORK;
265             } else if (type == Phone.TYPE_OTHER) {
266                 type = Phone.TYPE_OTHER_FAX;
267             }
268         }
269         if (type == Phone.TYPE_CUSTOM) {
270             return label;
271         } else {
272             return type;
273         }
274     }
275 
276     @SuppressWarnings("deprecation")
isMobilePhoneLabel(final String label)277     public static boolean isMobilePhoneLabel(final String label) {
278         // For backward compatibility.
279         // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
280         //         To support mobile type at that time, this custom label had been used.
281         return ("_AUTO_CELL".equals(label) || sMobilePhoneLabelSet.contains(label));
282     }
283 
isValidInV21ButUnknownToContactsPhoteType(final String label)284     public static boolean isValidInV21ButUnknownToContactsPhoteType(final String label) {
285         return sPhoneTypesUnknownToContactsSet.contains(label);
286     }
287 
getPropertyNameForIm(final int protocol)288     public static String getPropertyNameForIm(final int protocol) {
289         return sKnownImPropNameMap_ItoS.get(protocol);
290     }
291 
sortNameElements(final int nameOrder, final String familyName, final String middleName, final String givenName)292     public static String[] sortNameElements(final int nameOrder,
293             final String familyName, final String middleName, final String givenName) {
294         final String[] list = new String[3];
295         final int nameOrderType = VCardConfig.getNameOrderType(nameOrder);
296         switch (nameOrderType) {
297             case VCardConfig.NAME_ORDER_JAPANESE: {
298                 if (containsOnlyPrintableAscii(familyName) &&
299                         containsOnlyPrintableAscii(givenName)) {
300                     list[0] = givenName;
301                     list[1] = middleName;
302                     list[2] = familyName;
303                 } else {
304                     list[0] = familyName;
305                     list[1] = middleName;
306                     list[2] = givenName;
307                 }
308                 break;
309             }
310             case VCardConfig.NAME_ORDER_EUROPE: {
311                 list[0] = middleName;
312                 list[1] = givenName;
313                 list[2] = familyName;
314                 break;
315             }
316             default: {
317                 list[0] = givenName;
318                 list[1] = middleName;
319                 list[2] = familyName;
320                 break;
321             }
322         }
323         return list;
324     }
325 
getPhoneNumberFormat(final int vcardType)326     public static int getPhoneNumberFormat(final int vcardType) {
327         if (VCardConfig.isJapaneseDevice(vcardType)) {
328             return PhoneNumberUtils.FORMAT_JAPAN;
329         } else {
330             return PhoneNumberUtils.FORMAT_NANP;
331         }
332     }
333 
constructNameFromElements(final int nameOrder, final String familyName, final String middleName, final String givenName)334     public static String constructNameFromElements(final int nameOrder,
335             final String familyName, final String middleName, final String givenName) {
336         return constructNameFromElements(nameOrder, familyName, middleName, givenName,
337                 null, null);
338     }
339 
constructNameFromElements(final int nameOrder, final String familyName, final String middleName, final String givenName, final String prefix, final String suffix)340     public static String constructNameFromElements(final int nameOrder,
341             final String familyName, final String middleName, final String givenName,
342             final String prefix, final String suffix) {
343         final StringBuilder builder = new StringBuilder();
344         final String[] nameList = sortNameElements(nameOrder, familyName, middleName, givenName);
345         boolean first = true;
346         if (!TextUtils.isEmpty(prefix)) {
347             first = false;
348             builder.append(prefix);
349         }
350         for (final String namePart : nameList) {
351             if (!TextUtils.isEmpty(namePart)) {
352                 if (first) {
353                     first = false;
354                 } else {
355                     builder.append(' ');
356                 }
357                 builder.append(namePart);
358             }
359         }
360         if (!TextUtils.isEmpty(suffix)) {
361             if (!first) {
362                 builder.append(' ');
363             }
364             builder.append(suffix);
365         }
366         return builder.toString();
367     }
368 
369     /**
370      * Splits the given value into pieces using the delimiter ';' inside it.
371      *
372      * Escaped characters in those values are automatically unescaped into original form.
373      */
constructListFromValue(final String value, final int vcardType)374     public static List<String> constructListFromValue(final String value,
375             final int vcardType) {
376         final List<String> list = new ArrayList<String>();
377         StringBuilder builder = new StringBuilder();
378         final int length = value.length();
379         for (int i = 0; i < length; i++) {
380             char ch = value.charAt(i);
381             if (ch == '\\' && i < length - 1) {
382                 char nextCh = value.charAt(i + 1);
383                 final String unescapedString;
384                 if (VCardConfig.isVersion40(vcardType)) {
385                     unescapedString = VCardParserImpl_V40.unescapeCharacter(nextCh);
386                 } else if (VCardConfig.isVersion30(vcardType)) {
387                     unescapedString = VCardParserImpl_V30.unescapeCharacter(nextCh);
388                 } else {
389                     if (!VCardConfig.isVersion21(vcardType)) {
390                         // Unknown vCard type
391                         Log.w(LOG_TAG, "Unknown vCard type");
392                     }
393                     unescapedString = VCardParserImpl_V21.unescapeCharacter(nextCh);
394                 }
395 
396                 if (unescapedString != null) {
397                     builder.append(unescapedString);
398                     i++;
399                 } else {
400                     builder.append(ch);
401                 }
402             } else if (ch == ';') {
403                 list.add(builder.toString());
404                 builder = new StringBuilder();
405             } else {
406                 builder.append(ch);
407             }
408         }
409         list.add(builder.toString());
410         return list;
411     }
412 
containsOnlyPrintableAscii(final String...values)413     public static boolean containsOnlyPrintableAscii(final String...values) {
414         if (values == null) {
415             return true;
416         }
417         return containsOnlyPrintableAscii(Arrays.asList(values));
418     }
419 
containsOnlyPrintableAscii(final Collection<String> values)420     public static boolean containsOnlyPrintableAscii(final Collection<String> values) {
421         if (values == null) {
422             return true;
423         }
424         for (final String value : values) {
425             if (TextUtils.isEmpty(value)) {
426                 continue;
427             }
428             if (!TextUtilsPort.isPrintableAsciiOnly(value)) {
429                 return false;
430             }
431         }
432         return true;
433     }
434 
435     /**
436      * <p>
437      * This is useful when checking the string should be encoded into quoted-printable
438      * or not, which is required by vCard 2.1.
439      * </p>
440      * <p>
441      * See the definition of "7bit" in vCard 2.1 spec for more information.
442      * </p>
443      */
containsOnlyNonCrLfPrintableAscii(final String...values)444     public static boolean containsOnlyNonCrLfPrintableAscii(final String...values) {
445         if (values == null) {
446             return true;
447         }
448         return containsOnlyNonCrLfPrintableAscii(Arrays.asList(values));
449     }
450 
containsOnlyNonCrLfPrintableAscii(final Collection<String> values)451     public static boolean containsOnlyNonCrLfPrintableAscii(final Collection<String> values) {
452         if (values == null) {
453             return true;
454         }
455         final int asciiFirst = 0x20;
456         final int asciiLast = 0x7E;  // included
457         for (final String value : values) {
458             if (TextUtils.isEmpty(value)) {
459                 continue;
460             }
461             final int length = value.length();
462             for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
463                 final int c = value.codePointAt(i);
464                 if (!(asciiFirst <= c && c <= asciiLast)) {
465                     return false;
466                 }
467             }
468         }
469         return true;
470     }
471 
472     private static final Set<Character> sUnAcceptableAsciiInV21WordSet =
473         new HashSet<Character>(Arrays.asList('[', ']', '=', ':', '.', ',', ' '));
474 
475     /**
476      * <p>
477      * This is useful since vCard 3.0 often requires the ("X-") properties and groups
478      * should contain only alphabets, digits, and hyphen.
479      * </p>
480      * <p>
481      * Note: It is already known some devices (wrongly) outputs properties with characters
482      *       which should not be in the field. One example is "X-GOOGLE TALK". We accept
483      *       such kind of input but must never output it unless the target is very specific
484      *       to the device which is able to parse the malformed input.
485      * </p>
486      */
containsOnlyAlphaDigitHyphen(final String...values)487     public static boolean containsOnlyAlphaDigitHyphen(final String...values) {
488         if (values == null) {
489             return true;
490         }
491         return containsOnlyAlphaDigitHyphen(Arrays.asList(values));
492     }
493 
containsOnlyAlphaDigitHyphen(final Collection<String> values)494     public static boolean containsOnlyAlphaDigitHyphen(final Collection<String> values) {
495         if (values == null) {
496             return true;
497         }
498         final int upperAlphabetFirst = 0x41;  // A
499         final int upperAlphabetAfterLast = 0x5b;  // [
500         final int lowerAlphabetFirst = 0x61;  // a
501         final int lowerAlphabetAfterLast = 0x7b;  // {
502         final int digitFirst = 0x30;  // 0
503         final int digitAfterLast = 0x3A;  // :
504         final int hyphen = '-';
505         for (final String str : values) {
506             if (TextUtils.isEmpty(str)) {
507                 continue;
508             }
509             final int length = str.length();
510             for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
511                 int codepoint = str.codePointAt(i);
512                 if (!((lowerAlphabetFirst <= codepoint && codepoint < lowerAlphabetAfterLast) ||
513                     (upperAlphabetFirst <= codepoint && codepoint < upperAlphabetAfterLast) ||
514                     (digitFirst <= codepoint && codepoint < digitAfterLast) ||
515                     (codepoint == hyphen))) {
516                     return false;
517                 }
518             }
519         }
520         return true;
521     }
522 
containsOnlyWhiteSpaces(final String...values)523     public static boolean containsOnlyWhiteSpaces(final String...values) {
524         if (values == null) {
525             return true;
526         }
527         return containsOnlyWhiteSpaces(Arrays.asList(values));
528     }
529 
containsOnlyWhiteSpaces(final Collection<String> values)530     public static boolean containsOnlyWhiteSpaces(final Collection<String> values) {
531         if (values == null) {
532             return true;
533         }
534         for (final String str : values) {
535             if (TextUtils.isEmpty(str)) {
536                 continue;
537             }
538             final int length = str.length();
539             for (int i = 0; i < length; i = str.offsetByCodePoints(i, 1)) {
540                 if (!Character.isWhitespace(str.codePointAt(i))) {
541                     return false;
542                 }
543             }
544         }
545         return true;
546     }
547 
548     /**
549      * <p>
550      * Returns true when the given String is categorized as "word" specified in vCard spec 2.1.
551      * </p>
552      * <p>
553      * vCard 2.1 specifies:<br />
554      * word = &lt;any printable 7bit us-ascii except []=:., &gt;
555      * </p>
556      */
isV21Word(final String value)557     public static boolean isV21Word(final String value) {
558         if (TextUtils.isEmpty(value)) {
559             return true;
560         }
561         final int asciiFirst = 0x20;
562         final int asciiLast = 0x7E;  // included
563         final int length = value.length();
564         for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
565             final int c = value.codePointAt(i);
566             if (!(asciiFirst <= c && c <= asciiLast) ||
567                     sUnAcceptableAsciiInV21WordSet.contains((char)c)) {
568                 return false;
569             }
570         }
571         return true;
572     }
573 
574     private static final int[] sEscapeIndicatorsV30 = new int[]{
575         ':', ';', ',', ' '
576     };
577 
578     private static final int[] sEscapeIndicatorsV40 = new int[]{
579         ';', ':'
580     };
581 
582     /**
583      * <P>
584      * Returns String available as parameter value in vCard 3.0.
585      * </P>
586      * <P>
587      * RFC 2426 requires vCard composer to quote parameter values when it contains
588      * semi-colon, for example (See RFC 2426 for more information).
589      * This method checks whether the given String can be used without quotes.
590      * </P>
591      * <P>
592      * Note: We remove DQUOTE inside the given value silently for now.
593      * </P>
594      */
toStringAsV30ParamValue(String value)595     public static String toStringAsV30ParamValue(String value) {
596         return toStringAsParamValue(value, sEscapeIndicatorsV30);
597     }
598 
toStringAsV40ParamValue(String value)599     public static String toStringAsV40ParamValue(String value) {
600         return toStringAsParamValue(value, sEscapeIndicatorsV40);
601     }
602 
toStringAsParamValue(String value, final int[] escapeIndicators)603     private static String toStringAsParamValue(String value, final int[] escapeIndicators) {
604         if (TextUtils.isEmpty(value)) {
605             value = "";
606         }
607         final int asciiFirst = 0x20;
608         final int asciiLast = 0x7E;  // included
609         final StringBuilder builder = new StringBuilder();
610         final int length = value.length();
611         boolean needQuote = false;
612         for (int i = 0; i < length; i = value.offsetByCodePoints(i, 1)) {
613             final int codePoint = value.codePointAt(i);
614             if (codePoint < asciiFirst || codePoint == '"') {
615                 // CTL characters and DQUOTE are never accepted. Remove them.
616                 continue;
617             }
618             builder.appendCodePoint(codePoint);
619             for (int indicator : escapeIndicators) {
620                 if (codePoint == indicator) {
621                     needQuote = true;
622                     break;
623                 }
624             }
625         }
626 
627         final String result = builder.toString();
628         return ((result.isEmpty() || VCardUtils.containsOnlyWhiteSpaces(result))
629                 ? ""
630                 : (needQuote ? ('"' + result + '"')
631                 : result));
632     }
633 
toHalfWidthString(final String orgString)634     public static String toHalfWidthString(final String orgString) {
635         if (TextUtils.isEmpty(orgString)) {
636             return null;
637         }
638         final StringBuilder builder = new StringBuilder();
639         final int length = orgString.length();
640         for (int i = 0; i < length; i = orgString.offsetByCodePoints(i, 1)) {
641             // All Japanese character is able to be expressed by char.
642             // Do not need to use String#codepPointAt().
643             final char ch = orgString.charAt(i);
644             final String halfWidthText = JapaneseUtils.tryGetHalfWidthText(ch);
645             if (halfWidthText != null) {
646                 builder.append(halfWidthText);
647             } else {
648                 builder.append(ch);
649             }
650         }
651         return builder.toString();
652     }
653 
654     /**
655      * Guesses the format of input image. Currently just the first few bytes are used.
656      * The type "GIF", "PNG", or "JPEG" is returned when possible. Returns null when
657      * the guess failed.
658      * @param input Image as byte array.
659      * @return The image type or null when the type cannot be determined.
660      */
guessImageType(final byte[] input)661     public static String guessImageType(final byte[] input) {
662         if (input == null) {
663             return null;
664         }
665         if (input.length >= 3 && input[0] == 'G' && input[1] == 'I' && input[2] == 'F') {
666             return "GIF";
667         } else if (input.length >= 4 && input[0] == (byte) 0x89
668                 && input[1] == 'P' && input[2] == 'N' && input[3] == 'G') {
669             // Note: vCard 2.1 officially does not support PNG, but we may have it and
670             //       using X- word like "X-PNG" may not let importers know it is PNG.
671             //       So we use the String "PNG" as is...
672             return "PNG";
673         } else if (input.length >= 2 && input[0] == (byte) 0xff
674                 && input[1] == (byte) 0xd8) {
675             return "JPEG";
676         } else {
677             return null;
678         }
679     }
680 
681     /**
682      * @return True when all the given values are null or empty Strings.
683      */
areAllEmpty(final String...values)684     public static boolean areAllEmpty(final String...values) {
685         if (values == null) {
686             return true;
687         }
688 
689         for (final String value : values) {
690             if (!TextUtils.isEmpty(value)) {
691                 return false;
692             }
693         }
694         return true;
695     }
696 
697     /**
698      * Checks to see if a string looks like it could be an android generated quoted printable.
699      *
700      * Identification of quoted printable is not 100% reliable since it's just ascii.  But given
701      * the high number and exact location of generated = signs, there is a high likely-hood that
702      * it would be.
703      *
704      * @return True if it appears like android quoted printable.  False otherwise.
705      */
appearsLikeAndroidVCardQuotedPrintable(String value)706     public static boolean appearsLikeAndroidVCardQuotedPrintable(String value) {
707 
708         // Quoted printable is always in multiple of 3s. With optional 1 '=' at end.
709         final int remainder = (value.length() % 3);
710         if (value.length() < 2 || (remainder != 1 && remainder != 0)) {
711             return false;
712         }
713         for (int i = 0; i < value.length(); i += 3) {
714             if (value.charAt(i) != '=') {
715                 return false;
716             }
717         }
718         return true;
719     }
720 
721     //// The methods bellow may be used by unit test.
722 
723     /**
724      * Unquotes given Quoted-Printable value. value must not be null.
725      */
parseQuotedPrintable( final String value, boolean strictLineBreaking, String sourceCharset, String targetCharset)726     public static String parseQuotedPrintable(
727             final String value, boolean strictLineBreaking,
728             String sourceCharset, String targetCharset) {
729         // "= " -> " ", "=\t" -> "\t".
730         // Previous code had done this replacement. Keep on the safe side.
731         final String quotedPrintable;
732         {
733             final StringBuilder builder = new StringBuilder();
734             final int length = value.length();
735             for (int i = 0; i < length; i++) {
736                 char ch = value.charAt(i);
737                 if (ch == '=' && i < length - 1) {
738                     char nextCh = value.charAt(i + 1);
739                     if (nextCh == ' ' || nextCh == '\t') {
740                         builder.append(nextCh);
741                         i++;
742                         continue;
743                     }
744                 }
745                 builder.append(ch);
746             }
747             quotedPrintable = builder.toString();
748         }
749 
750         String[] lines;
751         if (strictLineBreaking) {
752             lines = quotedPrintable.split("\r\n");
753         } else {
754             StringBuilder builder = new StringBuilder();
755             final int length = quotedPrintable.length();
756             ArrayList<String> list = new ArrayList<String>();
757             for (int i = 0; i < length; i++) {
758                 char ch = quotedPrintable.charAt(i);
759                 if (ch == '\n') {
760                     list.add(builder.toString());
761                     builder = new StringBuilder();
762                 } else if (ch == '\r') {
763                     list.add(builder.toString());
764                     builder = new StringBuilder();
765                     if (i < length - 1) {
766                         char nextCh = quotedPrintable.charAt(i + 1);
767                         if (nextCh == '\n') {
768                             i++;
769                         }
770                     }
771                 } else {
772                     builder.append(ch);
773                 }
774             }
775             final String lastLine = builder.toString();
776             if (lastLine.length() > 0) {
777                 list.add(lastLine);
778             }
779             lines = list.toArray(new String[0]);
780         }
781 
782         final StringBuilder builder = new StringBuilder();
783         for (String line : lines) {
784             if (line.endsWith("=")) {
785                 line = line.substring(0, line.length() - 1);
786             }
787             builder.append(line);
788         }
789 
790         final String rawString = builder.toString();
791         if (TextUtils.isEmpty(rawString)) {
792             Log.w(LOG_TAG, "Given raw string is empty.");
793         }
794 
795         byte[] rawBytes = null;
796         try {
797             rawBytes = rawString.getBytes(sourceCharset);
798         } catch (UnsupportedEncodingException e) {
799             Log.w(LOG_TAG, "Failed to decode: " + sourceCharset);
800             rawBytes = rawString.getBytes();
801         }
802 
803         byte[] decodedBytes = null;
804         try {
805             decodedBytes = QuotedPrintableCodecPort.decodeQuotedPrintable(rawBytes);
806         } catch (DecoderException e) {
807             Log.e(LOG_TAG, "DecoderException is thrown.");
808             decodedBytes = rawBytes;
809         }
810 
811         try {
812             return new String(decodedBytes, targetCharset);
813         } catch (UnsupportedEncodingException e) {
814             Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
815             return new String(decodedBytes);
816         }
817     }
818 
getAppropriateParser(int vcardType)819     public static final VCardParser getAppropriateParser(int vcardType)
820             throws VCardException {
821         if (VCardConfig.isVersion21(vcardType)) {
822             return new VCardParser_V21();
823         } else if (VCardConfig.isVersion30(vcardType)) {
824             return new VCardParser_V30();
825         } else if (VCardConfig.isVersion40(vcardType)) {
826             return new VCardParser_V40();
827         } else {
828             throw new VCardException("Version is not specified");
829         }
830     }
831 
convertStringCharset( String originalString, String sourceCharset, String targetCharset)832     public static final String convertStringCharset(
833             String originalString, String sourceCharset, String targetCharset) {
834         if (sourceCharset.equalsIgnoreCase(targetCharset)) {
835             return originalString;
836         }
837         final Charset charset = Charset.forName(sourceCharset);
838         final ByteBuffer byteBuffer = charset.encode(originalString);
839         // byteBuffer.array() "may" return byte array which is larger than
840         // byteBuffer.remaining(). Here, we keep on the safe side.
841         final byte[] bytes = new byte[byteBuffer.remaining()];
842         byteBuffer.get(bytes);
843         try {
844             return new String(bytes, targetCharset);
845         } catch (UnsupportedEncodingException e) {
846             Log.e(LOG_TAG, "Failed to encode: charset=" + targetCharset);
847             return null;
848         }
849     }
850 
851     // TODO: utilities for vCard 4.0: datetime, timestamp, integer, float, and boolean
852 
VCardUtils()853     private VCardUtils() {
854     }
855 }
856