• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.vcard;
18 
19 import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
20 
21 import android.accounts.Account;
22 import android.content.ContentProviderOperation;
23 import android.content.ContentResolver;
24 import android.net.Uri;
25 import android.provider.ContactsContract;
26 import android.provider.ContactsContract.CommonDataKinds.Email;
27 import android.provider.ContactsContract.CommonDataKinds.Event;
28 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
29 import android.provider.ContactsContract.CommonDataKinds.Im;
30 import android.provider.ContactsContract.CommonDataKinds.Nickname;
31 import android.provider.ContactsContract.CommonDataKinds.Note;
32 import android.provider.ContactsContract.CommonDataKinds.Organization;
33 import android.provider.ContactsContract.CommonDataKinds.Phone;
34 import android.provider.ContactsContract.CommonDataKinds.Photo;
35 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
36 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
37 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
38 import android.provider.ContactsContract.CommonDataKinds.Website;
39 import android.provider.ContactsContract.Contacts;
40 import android.provider.ContactsContract.Data;
41 import android.provider.ContactsContract.RawContacts;
42 import android.telephony.PhoneNumberUtils;
43 import android.text.TextUtils;
44 import android.util.Log;
45 import android.util.Pair;
46 
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.Collections;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Map;
54 
55 /**
56  * Represents one vCard entry, which should start with "BEGIN:VCARD" and end
57  * with "END:VCARD". This class is for bridging between real vCard data and
58  * Android's {@link ContactsContract}, which means some aspects of vCard are
59  * dropped before this object being constructed. Raw vCard data should be first
60  * supplied with {@link #addProperty(VCardProperty)}. After supplying all data,
61  * user should call {@link #consolidateFields()} to prepare some additional
62  * information which is constructable from supplied raw data. TODO: preserve raw
63  * data using {@link VCardProperty}. If it may just waste memory, this at least
64  * should contain them when it cannot convert vCard as a string to Android's
65  * Contacts representation. Those raw properties should _not_ be used for
66  * {@link #isIgnorable()}.
67  */
68 public class VCardEntry {
69     private static final String LOG_TAG = VCardConstants.LOG_TAG;
70 
71     private static final int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
72 
73     private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
74 
75     static {
sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM)76         sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN)77         sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO)78         sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ)79         sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER)80         sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE)81         sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK)82         sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, Im.PROTOCOL_GOOGLE_TALK)83         sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
84                 Im.PROTOCOL_GOOGLE_TALK);
85     }
86 
87     /**
88      * Whether to insert this VCardEntry as RawContacts.STARRED
89      */
90     private boolean mStarred = false;
91 
setStarred(boolean val)92     public void setStarred(boolean val) {
93         mStarred = val;
94     }
getStarred()95     public boolean getStarred() {
96         return mStarred;
97     }
98 
99     public enum EntryLabel {
100         NAME,
101         PHONE,
102         EMAIL,
103         POSTAL_ADDRESS,
104         ORGANIZATION,
105         IM,
106         PHOTO,
107         WEBSITE,
108         SIP,
109         NICKNAME,
110         NOTE,
111         BIRTHDAY,
112         ANNIVERSARY,
113         ANDROID_CUSTOM
114     }
115 
116     public static interface EntryElement {
117         // Also need to inherit toString(), equals().
getEntryLabel()118         public EntryLabel getEntryLabel();
119 
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)120         public void constructInsertOperation(List<ContentProviderOperation> operationList,
121                 int backReferenceIndex);
122 
isEmpty()123         public boolean isEmpty();
124     }
125 
126     // TODO: vCard 4.0 logically has multiple formatted names and we need to
127     // select the most preferable one using PREF parameter.
128     //
129     // e.g. (based on rev.13)
130     // FN;PREF=1:John M. Doe
131     // FN;PREF=2:John Doe
132     // FN;PREF=3;John
133     public static class NameData implements EntryElement {
134         private String mFamily;
135         private String mGiven;
136         private String mMiddle;
137         private String mPrefix;
138         private String mSuffix;
139 
140         // Used only when no family nor given name is found.
141         private String mFormatted;
142 
143         private String mPhoneticFamily;
144         private String mPhoneticGiven;
145         private String mPhoneticMiddle;
146 
147         // For "SORT-STRING" in vCard 3.0.
148         private String mSortString;
149 
150         /**
151          * Not in vCard but for {@link StructuredName#DISPLAY_NAME}. This field
152          * is constructed by VCardEntry on demand. Consider using
153          * {@link VCardEntry#getDisplayName()}.
154          */
155         // This field should reflect the other Elem fields like Email,
156         // PostalAddress, etc., while
157         // This is static class which cannot see other data. Thus we ask
158         // VCardEntry to populate it.
159         public String displayName;
160 
emptyStructuredName()161         public boolean emptyStructuredName() {
162             return TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mGiven)
163                     && TextUtils.isEmpty(mMiddle) && TextUtils.isEmpty(mPrefix)
164                     && TextUtils.isEmpty(mSuffix);
165         }
166 
emptyPhoneticStructuredName()167         public boolean emptyPhoneticStructuredName() {
168             return TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticGiven)
169                     && TextUtils.isEmpty(mPhoneticMiddle);
170         }
171 
172         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)173         public void constructInsertOperation(List<ContentProviderOperation> operationList,
174                 int backReferenceIndex) {
175             final ContentProviderOperation.Builder builder = ContentProviderOperation
176                     .newInsert(Data.CONTENT_URI);
177             builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, backReferenceIndex);
178             builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
179 
180             if (!TextUtils.isEmpty(mGiven)) {
181                 builder.withValue(StructuredName.GIVEN_NAME, mGiven);
182             }
183             if (!TextUtils.isEmpty(mFamily)) {
184                 builder.withValue(StructuredName.FAMILY_NAME, mFamily);
185             }
186             if (!TextUtils.isEmpty(mMiddle)) {
187                 builder.withValue(StructuredName.MIDDLE_NAME, mMiddle);
188             }
189             if (!TextUtils.isEmpty(mPrefix)) {
190                 builder.withValue(StructuredName.PREFIX, mPrefix);
191             }
192             if (!TextUtils.isEmpty(mSuffix)) {
193                 builder.withValue(StructuredName.SUFFIX, mSuffix);
194             }
195 
196             boolean phoneticNameSpecified = false;
197 
198             if (!TextUtils.isEmpty(mPhoneticGiven)) {
199                 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGiven);
200                 phoneticNameSpecified = true;
201             }
202             if (!TextUtils.isEmpty(mPhoneticFamily)) {
203                 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamily);
204                 phoneticNameSpecified = true;
205             }
206             if (!TextUtils.isEmpty(mPhoneticMiddle)) {
207                 builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddle);
208                 phoneticNameSpecified = true;
209             }
210 
211             // SORT-STRING is used only when phonetic names aren't specified in
212             // the original vCard.
213             if (!phoneticNameSpecified) {
214                 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mSortString);
215             }
216 
217             builder.withValue(StructuredName.DISPLAY_NAME, displayName);
218             operationList.add(builder.build());
219         }
220 
221         @Override
isEmpty()222         public boolean isEmpty() {
223             return (TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mMiddle)
224                     && TextUtils.isEmpty(mGiven) && TextUtils.isEmpty(mPrefix)
225                     && TextUtils.isEmpty(mSuffix) && TextUtils.isEmpty(mFormatted)
226                     && TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticMiddle)
227                     && TextUtils.isEmpty(mPhoneticGiven) && TextUtils.isEmpty(mSortString));
228         }
229 
230         @Override
equals(Object obj)231         public boolean equals(Object obj) {
232             if (this == obj) {
233                 return true;
234             }
235             if (!(obj instanceof NameData)) {
236                 return false;
237             }
238             NameData nameData = (NameData) obj;
239 
240             return (TextUtils.equals(mFamily, nameData.mFamily)
241                     && TextUtils.equals(mMiddle, nameData.mMiddle)
242                     && TextUtils.equals(mGiven, nameData.mGiven)
243                     && TextUtils.equals(mPrefix, nameData.mPrefix)
244                     && TextUtils.equals(mSuffix, nameData.mSuffix)
245                     && TextUtils.equals(mFormatted, nameData.mFormatted)
246                     && TextUtils.equals(mPhoneticFamily, nameData.mPhoneticFamily)
247                     && TextUtils.equals(mPhoneticMiddle, nameData.mPhoneticMiddle)
248                     && TextUtils.equals(mPhoneticGiven, nameData.mPhoneticGiven)
249                     && TextUtils.equals(mSortString, nameData.mSortString));
250         }
251 
252         @Override
hashCode()253         public int hashCode() {
254             final String[] hashTargets = new String[] {mFamily, mMiddle, mGiven, mPrefix, mSuffix,
255                     mFormatted, mPhoneticFamily, mPhoneticMiddle,
256                     mPhoneticGiven, mSortString};
257             int hash = 0;
258             for (String hashTarget : hashTargets) {
259                 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
260             }
261             return hash;
262         }
263 
264         @Override
toString()265         public String toString() {
266             return String.format("family: %s, given: %s, middle: %s, prefix: %s, suffix: %s",
267                     mFamily, mGiven, mMiddle, mPrefix, mSuffix);
268         }
269 
270         @Override
getEntryLabel()271         public final EntryLabel getEntryLabel() {
272             return EntryLabel.NAME;
273         }
274 
getFamily()275         public String getFamily() {
276             return mFamily;
277         }
278 
getMiddle()279         public String getMiddle() {
280             return mMiddle;
281         }
282 
getGiven()283         public String getGiven() {
284             return mGiven;
285         }
286 
getPrefix()287         public String getPrefix() {
288             return mPrefix;
289         }
290 
getSuffix()291         public String getSuffix() {
292             return mSuffix;
293         }
294 
getFormatted()295         public String getFormatted() {
296             return mFormatted;
297         }
298 
getSortString()299         public String getSortString() {
300             return mSortString;
301         }
302 
303         /** @hide Just for testing. */
setFamily(String family)304         public void setFamily(String family) { mFamily = family; }
305         /** @hide Just for testing. */
setMiddle(String middle)306         public void setMiddle(String middle) { mMiddle = middle; }
307         /** @hide Just for testing. */
setGiven(String given)308         public void setGiven(String given) { mGiven = given; }
309         /** @hide Just for testing. */
setPrefix(String prefix)310         public void setPrefix(String prefix) { mPrefix = prefix; }
311         /** @hide Just for testing. */
setSuffix(String suffix)312         public void setSuffix(String suffix) { mSuffix = suffix; }
313     }
314 
315     public static class PhoneData implements EntryElement {
316         private final String mNumber;
317         private final int mType;
318         private final String mLabel;
319 
320         // isPrimary is (not final but) changable, only when there's no
321         // appropriate one existing
322         // in the original VCard.
323         private boolean mIsPrimary;
324 
PhoneData(String data, int type, String label, boolean isPrimary)325         public PhoneData(String data, int type, String label, boolean isPrimary) {
326             mNumber = data;
327             mType = type;
328             mLabel = label;
329             mIsPrimary = isPrimary;
330         }
331 
332         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)333         public void constructInsertOperation(List<ContentProviderOperation> operationList,
334                 int backReferenceIndex) {
335             final ContentProviderOperation.Builder builder = ContentProviderOperation
336                     .newInsert(Data.CONTENT_URI);
337             builder.withValueBackReference(Phone.RAW_CONTACT_ID, backReferenceIndex);
338             builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
339 
340             builder.withValue(Phone.TYPE, mType);
341             if (mType == Phone.TYPE_CUSTOM) {
342                 builder.withValue(Phone.LABEL, mLabel);
343             }
344             builder.withValue(Phone.NUMBER, mNumber);
345             if (mIsPrimary) {
346                 builder.withValue(Phone.IS_PRIMARY, 1);
347             }
348             operationList.add(builder.build());
349         }
350 
351         @Override
isEmpty()352         public boolean isEmpty() {
353             return TextUtils.isEmpty(mNumber);
354         }
355 
356         @Override
equals(Object obj)357         public boolean equals(Object obj) {
358             if (this == obj) {
359                 return true;
360             }
361             if (!(obj instanceof PhoneData)) {
362                 return false;
363             }
364             PhoneData phoneData = (PhoneData) obj;
365             return (mType == phoneData.mType
366                     && TextUtils.equals(mNumber, phoneData.mNumber)
367                     && TextUtils.equals(mLabel, phoneData.mLabel)
368                     && (mIsPrimary == phoneData.mIsPrimary));
369         }
370 
371         @Override
hashCode()372         public int hashCode() {
373             int hash = mType;
374             hash = hash * 31 + (mNumber != null ? mNumber.hashCode() : 0);
375             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
376             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
377             return hash;
378         }
379 
380         @Override
toString()381         public String toString() {
382             return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mNumber,
383                     mLabel, mIsPrimary);
384         }
385 
386         @Override
getEntryLabel()387         public final EntryLabel getEntryLabel() {
388             return EntryLabel.PHONE;
389         }
390 
getNumber()391         public String getNumber() {
392             return mNumber;
393         }
394 
getType()395         public int getType() {
396             return mType;
397         }
398 
getLabel()399         public String getLabel() {
400             return mLabel;
401         }
402 
isPrimary()403         public boolean isPrimary() {
404             return mIsPrimary;
405         }
406     }
407 
408     public static class EmailData implements EntryElement {
409         private final String mAddress;
410         private final int mType;
411         // Used only when TYPE is TYPE_CUSTOM.
412         private final String mLabel;
413         private final boolean mIsPrimary;
414 
EmailData(String data, int type, String label, boolean isPrimary)415         public EmailData(String data, int type, String label, boolean isPrimary) {
416             mType = type;
417             mAddress = data;
418             mLabel = label;
419             mIsPrimary = isPrimary;
420         }
421 
422         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)423         public void constructInsertOperation(List<ContentProviderOperation> operationList,
424                 int backReferenceIndex) {
425             final ContentProviderOperation.Builder builder = ContentProviderOperation
426                     .newInsert(Data.CONTENT_URI);
427             builder.withValueBackReference(Email.RAW_CONTACT_ID, backReferenceIndex);
428             builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
429 
430             builder.withValue(Email.TYPE, mType);
431             if (mType == Email.TYPE_CUSTOM) {
432                 builder.withValue(Email.LABEL, mLabel);
433             }
434             builder.withValue(Email.DATA, mAddress);
435             if (mIsPrimary) {
436                 builder.withValue(Data.IS_PRIMARY, 1);
437             }
438             operationList.add(builder.build());
439         }
440 
441         @Override
isEmpty()442         public boolean isEmpty() {
443             return TextUtils.isEmpty(mAddress);
444         }
445 
446         @Override
equals(Object obj)447         public boolean equals(Object obj) {
448             if (this == obj) {
449                 return true;
450             }
451             if (!(obj instanceof EmailData)) {
452                 return false;
453             }
454             EmailData emailData = (EmailData) obj;
455             return (mType == emailData.mType
456                     && TextUtils.equals(mAddress, emailData.mAddress)
457                     && TextUtils.equals(mLabel, emailData.mLabel)
458                     && (mIsPrimary == emailData.mIsPrimary));
459         }
460 
461         @Override
hashCode()462         public int hashCode() {
463             int hash = mType;
464             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
465             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
466             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
467             return hash;
468         }
469 
470         @Override
toString()471         public String toString() {
472             return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mAddress,
473                     mLabel, mIsPrimary);
474         }
475 
476         @Override
getEntryLabel()477         public final EntryLabel getEntryLabel() {
478             return EntryLabel.EMAIL;
479         }
480 
getAddress()481         public String getAddress() {
482             return mAddress;
483         }
484 
getType()485         public int getType() {
486             return mType;
487         }
488 
getLabel()489         public String getLabel() {
490             return mLabel;
491         }
492 
isPrimary()493         public boolean isPrimary() {
494             return mIsPrimary;
495         }
496     }
497 
498     public static class PostalData implements EntryElement {
499         // Determined by vCard specification.
500         // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
501         private static final int ADDR_MAX_DATA_SIZE = 7;
502         private final String mPobox;
503         private final String mExtendedAddress;
504         private final String mStreet;
505         private final String mLocalty;
506         private final String mRegion;
507         private final String mPostalCode;
508         private final String mCountry;
509         private final int mType;
510         private final String mLabel;
511         private boolean mIsPrimary;
512 
513         /** We keep this for {@link StructuredPostal#FORMATTED_ADDRESS} */
514         // TODO: need better way to construct formatted address.
515         private int mVCardType;
516 
PostalData(String pobox, String extendedAddress, String street, String localty, String region, String postalCode, String country, int type, String label, boolean isPrimary, int vcardType)517         public PostalData(String pobox, String extendedAddress, String street, String localty,
518                 String region, String postalCode, String country, int type, String label,
519                 boolean isPrimary, int vcardType) {
520             mType = type;
521             mPobox = pobox;
522             mExtendedAddress = extendedAddress;
523             mStreet = street;
524             mLocalty = localty;
525             mRegion = region;
526             mPostalCode = postalCode;
527             mCountry = country;
528             mLabel = label;
529             mIsPrimary = isPrimary;
530             mVCardType = vcardType;
531         }
532 
533         /**
534          * Accepts raw propertyValueList in vCard and constructs PostalData.
535          */
constructPostalData(final List<String> propValueList, final int type, final String label, boolean isPrimary, int vcardType)536         public static PostalData constructPostalData(final List<String> propValueList,
537                 final int type, final String label, boolean isPrimary, int vcardType) {
538             final String[] dataArray = new String[ADDR_MAX_DATA_SIZE];
539 
540             int size = propValueList.size();
541             if (size > ADDR_MAX_DATA_SIZE) {
542                 size = ADDR_MAX_DATA_SIZE;
543             }
544 
545             // adr-value = 0*6(text-value ";") text-value
546             // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name
547             //
548             // Use Iterator assuming List may be LinkedList, though actually it is
549             // always ArrayList in the current implementation.
550             int i = 0;
551             for (String addressElement : propValueList) {
552                 dataArray[i] = addressElement;
553                 if (++i >= size) {
554                     break;
555                 }
556             }
557             while (i < ADDR_MAX_DATA_SIZE) {
558                 dataArray[i++] = null;
559             }
560 
561             return new PostalData(dataArray[0], dataArray[1], dataArray[2], dataArray[3],
562                     dataArray[4], dataArray[5], dataArray[6], type, label, isPrimary, vcardType);
563         }
564 
565         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)566         public void constructInsertOperation(List<ContentProviderOperation> operationList,
567                 int backReferenceIndex) {
568             final ContentProviderOperation.Builder builder = ContentProviderOperation
569                     .newInsert(Data.CONTENT_URI);
570             builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, backReferenceIndex);
571             builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
572 
573             builder.withValue(StructuredPostal.TYPE, mType);
574             if (mType == StructuredPostal.TYPE_CUSTOM) {
575                 builder.withValue(StructuredPostal.LABEL, mLabel);
576             }
577 
578             final String streetString;
579             if (TextUtils.isEmpty(mStreet)) {
580                 if (TextUtils.isEmpty(mExtendedAddress)) {
581                     streetString = null;
582                 } else {
583                     streetString = mExtendedAddress;
584                 }
585             } else {
586                 if (TextUtils.isEmpty(mExtendedAddress)) {
587                     streetString = mStreet;
588                 } else {
589                     streetString = mStreet + " " + mExtendedAddress;
590                 }
591             }
592             builder.withValue(StructuredPostal.POBOX, mPobox);
593             builder.withValue(StructuredPostal.STREET, streetString);
594             builder.withValue(StructuredPostal.CITY, mLocalty);
595             builder.withValue(StructuredPostal.REGION, mRegion);
596             builder.withValue(StructuredPostal.POSTCODE, mPostalCode);
597             builder.withValue(StructuredPostal.COUNTRY, mCountry);
598 
599             builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress(mVCardType));
600             if (mIsPrimary) {
601                 builder.withValue(Data.IS_PRIMARY, 1);
602             }
603             operationList.add(builder.build());
604         }
605 
getFormattedAddress(final int vcardType)606         public String getFormattedAddress(final int vcardType) {
607             StringBuilder builder = new StringBuilder();
608             boolean empty = true;
609             final String[] dataArray = new String[] {
610                     mPobox, mExtendedAddress, mStreet, mLocalty, mRegion, mPostalCode, mCountry
611             };
612             if (VCardConfig.isJapaneseDevice(vcardType)) {
613                 // In Japan, the order is reversed.
614                 for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
615                     String addressPart = dataArray[i];
616                     if (!TextUtils.isEmpty(addressPart)) {
617                         if (!empty) {
618                             builder.append(' ');
619                         } else {
620                             empty = false;
621                         }
622                         builder.append(addressPart);
623                     }
624                 }
625             } else {
626                 for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
627                     String addressPart = dataArray[i];
628                     if (!TextUtils.isEmpty(addressPart)) {
629                         if (!empty) {
630                             builder.append(' ');
631                         } else {
632                             empty = false;
633                         }
634                         builder.append(addressPart);
635                     }
636                 }
637             }
638 
639             return builder.toString().trim();
640         }
641 
642         @Override
isEmpty()643         public boolean isEmpty() {
644             return (TextUtils.isEmpty(mPobox)
645                     && TextUtils.isEmpty(mExtendedAddress)
646                     && TextUtils.isEmpty(mStreet)
647                     && TextUtils.isEmpty(mLocalty)
648                     && TextUtils.isEmpty(mRegion)
649                     && TextUtils.isEmpty(mPostalCode)
650                     && TextUtils.isEmpty(mCountry));
651         }
652 
653         @Override
equals(Object obj)654         public boolean equals(Object obj) {
655             if (this == obj) {
656                 return true;
657             }
658             if (!(obj instanceof PostalData)) {
659                 return false;
660             }
661             final PostalData postalData = (PostalData) obj;
662             return (mType == postalData.mType)
663                     && (mType == StructuredPostal.TYPE_CUSTOM ? TextUtils.equals(mLabel,
664                             postalData.mLabel) : true)
665                     && (mIsPrimary == postalData.mIsPrimary)
666                     && TextUtils.equals(mPobox, postalData.mPobox)
667                     && TextUtils.equals(mExtendedAddress, postalData.mExtendedAddress)
668                     && TextUtils.equals(mStreet, postalData.mStreet)
669                     && TextUtils.equals(mLocalty, postalData.mLocalty)
670                     && TextUtils.equals(mRegion, postalData.mRegion)
671                     && TextUtils.equals(mPostalCode, postalData.mPostalCode)
672                     && TextUtils.equals(mCountry, postalData.mCountry);
673         }
674 
675         @Override
hashCode()676         public int hashCode() {
677             int hash = mType;
678             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
679             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
680 
681             final String[] hashTargets = new String[] {mPobox, mExtendedAddress, mStreet,
682                     mLocalty, mRegion, mPostalCode, mCountry};
683             for (String hashTarget : hashTargets) {
684                 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
685             }
686             return hash;
687         }
688 
689         @Override
toString()690         public String toString() {
691             return String.format("type: %d, label: %s, isPrimary: %s, pobox: %s, "
692                     + "extendedAddress: %s, street: %s, localty: %s, region: %s, postalCode %s, "
693                     + "country: %s", mType, mLabel, mIsPrimary, mPobox, mExtendedAddress, mStreet,
694                     mLocalty, mRegion, mPostalCode, mCountry);
695         }
696 
697         @Override
getEntryLabel()698         public final EntryLabel getEntryLabel() {
699             return EntryLabel.POSTAL_ADDRESS;
700         }
701 
getPobox()702         public String getPobox() {
703             return mPobox;
704         }
705 
getExtendedAddress()706         public String getExtendedAddress() {
707             return mExtendedAddress;
708         }
709 
getStreet()710         public String getStreet() {
711             return mStreet;
712         }
713 
getLocalty()714         public String getLocalty() {
715             return mLocalty;
716         }
717 
getRegion()718         public String getRegion() {
719             return mRegion;
720         }
721 
getPostalCode()722         public String getPostalCode() {
723             return mPostalCode;
724         }
725 
getCountry()726         public String getCountry() {
727             return mCountry;
728         }
729 
getType()730         public int getType() {
731             return mType;
732         }
733 
getLabel()734         public String getLabel() {
735             return mLabel;
736         }
737 
isPrimary()738         public boolean isPrimary() {
739             return mIsPrimary;
740         }
741     }
742 
743     public static class OrganizationData implements EntryElement {
744         // non-final is Intentional: we may change the values since this info is separated into
745         // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in different
746         // timing.
747         private String mOrganizationName;
748         private String mDepartmentName;
749         private String mTitle;
750         private final String mPhoneticName; // We won't have this in "TITLE" property.
751         private final int mType;
752         private boolean mIsPrimary;
753 
OrganizationData(final String organizationName, final String departmentName, final String titleName, final String phoneticName, int type, final boolean isPrimary)754         public OrganizationData(final String organizationName, final String departmentName,
755                 final String titleName, final String phoneticName, int type,
756                 final boolean isPrimary) {
757             mType = type;
758             mOrganizationName = organizationName;
759             mDepartmentName = departmentName;
760             mTitle = titleName;
761             mPhoneticName = phoneticName;
762             mIsPrimary = isPrimary;
763         }
764 
getFormattedString()765         public String getFormattedString() {
766             final StringBuilder builder = new StringBuilder();
767             if (!TextUtils.isEmpty(mOrganizationName)) {
768                 builder.append(mOrganizationName);
769             }
770 
771             if (!TextUtils.isEmpty(mDepartmentName)) {
772                 if (builder.length() > 0) {
773                     builder.append(", ");
774                 }
775                 builder.append(mDepartmentName);
776             }
777 
778             if (!TextUtils.isEmpty(mTitle)) {
779                 if (builder.length() > 0) {
780                     builder.append(", ");
781                 }
782                 builder.append(mTitle);
783             }
784 
785             return builder.toString();
786         }
787 
788         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)789         public void constructInsertOperation(List<ContentProviderOperation> operationList,
790                 int backReferenceIndex) {
791             final ContentProviderOperation.Builder builder = ContentProviderOperation
792                     .newInsert(Data.CONTENT_URI);
793             builder.withValueBackReference(Organization.RAW_CONTACT_ID, backReferenceIndex);
794             builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
795             builder.withValue(Organization.TYPE, mType);
796             if (mOrganizationName != null) {
797                 builder.withValue(Organization.COMPANY, mOrganizationName);
798             }
799             if (mDepartmentName != null) {
800                 builder.withValue(Organization.DEPARTMENT, mDepartmentName);
801             }
802             if (mTitle != null) {
803                 builder.withValue(Organization.TITLE, mTitle);
804             }
805             if (mPhoneticName != null) {
806                 builder.withValue(Organization.PHONETIC_NAME, mPhoneticName);
807             }
808             if (mIsPrimary) {
809                 builder.withValue(Organization.IS_PRIMARY, 1);
810             }
811             operationList.add(builder.build());
812         }
813 
814         @Override
isEmpty()815         public boolean isEmpty() {
816             return TextUtils.isEmpty(mOrganizationName) && TextUtils.isEmpty(mDepartmentName)
817                     && TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mPhoneticName);
818         }
819 
820         @Override
equals(Object obj)821         public boolean equals(Object obj) {
822             if (this == obj) {
823                 return true;
824             }
825             if (!(obj instanceof OrganizationData)) {
826                 return false;
827             }
828             OrganizationData organization = (OrganizationData) obj;
829             return (mType == organization.mType
830                     && TextUtils.equals(mOrganizationName, organization.mOrganizationName)
831                     && TextUtils.equals(mDepartmentName, organization.mDepartmentName)
832                     && TextUtils.equals(mTitle, organization.mTitle)
833                     && (mIsPrimary == organization.mIsPrimary));
834         }
835 
836         @Override
hashCode()837         public int hashCode() {
838             int hash = mType;
839             hash = hash * 31 + (mOrganizationName != null ? mOrganizationName.hashCode() : 0);
840             hash = hash * 31 + (mDepartmentName != null ? mDepartmentName.hashCode() : 0);
841             hash = hash * 31 + (mTitle != null ? mTitle.hashCode() : 0);
842             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
843             return hash;
844         }
845 
846         @Override
toString()847         public String toString() {
848             return String.format(
849                     "type: %d, organization: %s, department: %s, title: %s, isPrimary: %s", mType,
850                     mOrganizationName, mDepartmentName, mTitle, mIsPrimary);
851         }
852 
853         @Override
getEntryLabel()854         public final EntryLabel getEntryLabel() {
855             return EntryLabel.ORGANIZATION;
856         }
857 
getOrganizationName()858         public String getOrganizationName() {
859             return mOrganizationName;
860         }
861 
getDepartmentName()862         public String getDepartmentName() {
863             return mDepartmentName;
864         }
865 
getTitle()866         public String getTitle() {
867             return mTitle;
868         }
869 
getPhoneticName()870         public String getPhoneticName() {
871             return mPhoneticName;
872         }
873 
getType()874         public int getType() {
875             return mType;
876         }
877 
isPrimary()878         public boolean isPrimary() {
879             return mIsPrimary;
880         }
881     }
882 
883     public static class ImData implements EntryElement {
884         private final String mAddress;
885         private final int mProtocol;
886         private final String mCustomProtocol;
887         private final int mType;
888         private final boolean mIsPrimary;
889 
ImData(final int protocol, final String customProtocol, final String address, final int type, final boolean isPrimary)890         public ImData(final int protocol, final String customProtocol, final String address,
891                 final int type, final boolean isPrimary) {
892             mProtocol = protocol;
893             mCustomProtocol = customProtocol;
894             mType = type;
895             mAddress = address;
896             mIsPrimary = isPrimary;
897         }
898 
899         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)900         public void constructInsertOperation(List<ContentProviderOperation> operationList,
901                 int backReferenceIndex) {
902             final ContentProviderOperation.Builder builder = ContentProviderOperation
903                     .newInsert(Data.CONTENT_URI);
904             builder.withValueBackReference(Im.RAW_CONTACT_ID, backReferenceIndex);
905             builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
906             builder.withValue(Im.TYPE, mType);
907             builder.withValue(Im.PROTOCOL, mProtocol);
908             builder.withValue(Im.DATA, mAddress);
909             if (mProtocol == Im.PROTOCOL_CUSTOM) {
910                 builder.withValue(Im.CUSTOM_PROTOCOL, mCustomProtocol);
911             }
912             if (mIsPrimary) {
913                 builder.withValue(Data.IS_PRIMARY, 1);
914             }
915             operationList.add(builder.build());
916         }
917 
918         @Override
isEmpty()919         public boolean isEmpty() {
920             return TextUtils.isEmpty(mAddress);
921         }
922 
923         @Override
equals(Object obj)924         public boolean equals(Object obj) {
925             if (this == obj) {
926                 return true;
927             }
928             if (!(obj instanceof ImData)) {
929                 return false;
930             }
931             ImData imData = (ImData) obj;
932             return (mType == imData.mType
933                     && mProtocol == imData.mProtocol
934                     && TextUtils.equals(mCustomProtocol, imData.mCustomProtocol)
935                     && TextUtils.equals(mAddress, imData.mAddress)
936                     && (mIsPrimary == imData.mIsPrimary));
937         }
938 
939         @Override
hashCode()940         public int hashCode() {
941             int hash = mType;
942             hash = hash * 31 + mProtocol;
943             hash = hash * 31 + (mCustomProtocol != null ? mCustomProtocol.hashCode() : 0);
944             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
945             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
946             return hash;
947         }
948 
949         @Override
toString()950         public String toString() {
951             return String.format(
952                     "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", mType,
953                     mProtocol, mCustomProtocol, mAddress, mIsPrimary);
954         }
955 
956         @Override
getEntryLabel()957         public final EntryLabel getEntryLabel() {
958             return EntryLabel.IM;
959         }
960 
getAddress()961         public String getAddress() {
962             return mAddress;
963         }
964 
965         /**
966          * One of the value available for {@link Im#PROTOCOL}. e.g.
967          * {@link Im#PROTOCOL_GOOGLE_TALK}
968          */
getProtocol()969         public int getProtocol() {
970             return mProtocol;
971         }
972 
getCustomProtocol()973         public String getCustomProtocol() {
974             return mCustomProtocol;
975         }
976 
getType()977         public int getType() {
978             return mType;
979         }
980 
isPrimary()981         public boolean isPrimary() {
982             return mIsPrimary;
983         }
984     }
985 
986     public static class PhotoData implements EntryElement {
987         // private static final String FORMAT_FLASH = "SWF";
988 
989         // used when type is not defined in ContactsContract.
990         private final String mFormat;
991         private final boolean mIsPrimary;
992 
993         private final byte[] mBytes;
994 
995         private Integer mHashCode = null;
996 
PhotoData(String format, byte[] photoBytes, boolean isPrimary)997         public PhotoData(String format, byte[] photoBytes, boolean isPrimary) {
998             mFormat = format;
999             mBytes = photoBytes;
1000             mIsPrimary = isPrimary;
1001         }
1002 
1003         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1004         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1005                 int backReferenceIndex) {
1006             final ContentProviderOperation.Builder builder = ContentProviderOperation
1007                     .newInsert(Data.CONTENT_URI);
1008             builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex);
1009             builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
1010             builder.withValue(Photo.PHOTO, mBytes);
1011             if (mIsPrimary) {
1012                 builder.withValue(Photo.IS_PRIMARY, 1);
1013             }
1014             operationList.add(builder.build());
1015         }
1016 
1017         @Override
isEmpty()1018         public boolean isEmpty() {
1019             return mBytes == null || mBytes.length == 0;
1020         }
1021 
1022         @Override
equals(Object obj)1023         public boolean equals(Object obj) {
1024             if (this == obj) {
1025                 return true;
1026             }
1027             if (!(obj instanceof PhotoData)) {
1028                 return false;
1029             }
1030             PhotoData photoData = (PhotoData) obj;
1031             return (TextUtils.equals(mFormat, photoData.mFormat)
1032                     && Arrays.equals(mBytes, photoData.mBytes)
1033                     && (mIsPrimary == photoData.mIsPrimary));
1034         }
1035 
1036         @Override
hashCode()1037         public int hashCode() {
1038             if (mHashCode != null) {
1039                 return mHashCode;
1040             }
1041 
1042             int hash = mFormat != null ? mFormat.hashCode() : 0;
1043             hash = hash * 31;
1044             if (mBytes != null) {
1045                 for (byte b : mBytes) {
1046                     hash += b;
1047                 }
1048             }
1049 
1050             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
1051             mHashCode = hash;
1052             return hash;
1053         }
1054 
1055         @Override
toString()1056         public String toString() {
1057             return String.format("format: %s: size: %d, isPrimary: %s", mFormat, mBytes.length,
1058                     mIsPrimary);
1059         }
1060 
1061         @Override
getEntryLabel()1062         public final EntryLabel getEntryLabel() {
1063             return EntryLabel.PHOTO;
1064         }
1065 
getFormat()1066         public String getFormat() {
1067             return mFormat;
1068         }
1069 
getBytes()1070         public byte[] getBytes() {
1071             return mBytes;
1072         }
1073 
isPrimary()1074         public boolean isPrimary() {
1075             return mIsPrimary;
1076         }
1077     }
1078 
1079     public static class NicknameData implements EntryElement {
1080         private final String mNickname;
1081 
NicknameData(String nickname)1082         public NicknameData(String nickname) {
1083             mNickname = nickname;
1084         }
1085 
1086         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1087         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1088                 int backReferenceIndex) {
1089             final ContentProviderOperation.Builder builder = ContentProviderOperation
1090                     .newInsert(Data.CONTENT_URI);
1091             builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex);
1092             builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
1093             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1094             builder.withValue(Nickname.NAME, mNickname);
1095             operationList.add(builder.build());
1096         }
1097 
1098         @Override
isEmpty()1099         public boolean isEmpty() {
1100             return TextUtils.isEmpty(mNickname);
1101         }
1102 
1103         @Override
equals(Object obj)1104         public boolean equals(Object obj) {
1105             if (!(obj instanceof NicknameData)) {
1106                 return false;
1107             }
1108             NicknameData nicknameData = (NicknameData) obj;
1109             return TextUtils.equals(mNickname, nicknameData.mNickname);
1110         }
1111 
1112         @Override
hashCode()1113         public int hashCode() {
1114             return mNickname != null ? mNickname.hashCode() : 0;
1115         }
1116 
1117         @Override
toString()1118         public String toString() {
1119             return "nickname: " + mNickname;
1120         }
1121 
1122         @Override
getEntryLabel()1123         public EntryLabel getEntryLabel() {
1124             return EntryLabel.NICKNAME;
1125         }
1126 
getNickname()1127         public String getNickname() {
1128             return mNickname;
1129         }
1130     }
1131 
1132     public static class NoteData implements EntryElement {
1133         public final String mNote;
1134 
NoteData(String note)1135         public NoteData(String note) {
1136             mNote = note;
1137         }
1138 
1139         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1140         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1141                 int backReferenceIndex) {
1142             final ContentProviderOperation.Builder builder = ContentProviderOperation
1143                     .newInsert(Data.CONTENT_URI);
1144             builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex);
1145             builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
1146             builder.withValue(Note.NOTE, mNote);
1147             operationList.add(builder.build());
1148         }
1149 
1150         @Override
isEmpty()1151         public boolean isEmpty() {
1152             return TextUtils.isEmpty(mNote);
1153         }
1154 
1155         @Override
equals(Object obj)1156         public boolean equals(Object obj) {
1157             if (this == obj) {
1158                 return true;
1159             }
1160             if (!(obj instanceof NoteData)) {
1161                 return false;
1162             }
1163             NoteData noteData = (NoteData) obj;
1164             return TextUtils.equals(mNote, noteData.mNote);
1165         }
1166 
1167         @Override
hashCode()1168         public int hashCode() {
1169             return mNote != null ? mNote.hashCode() : 0;
1170         }
1171 
1172         @Override
toString()1173         public String toString() {
1174             return "note: " + mNote;
1175         }
1176 
1177         @Override
getEntryLabel()1178         public EntryLabel getEntryLabel() {
1179             return EntryLabel.NOTE;
1180         }
1181 
getNote()1182         public String getNote() {
1183             return mNote;
1184         }
1185     }
1186 
1187     public static class WebsiteData implements EntryElement {
1188         private final String mWebsite;
1189 
WebsiteData(String website)1190         public WebsiteData(String website) {
1191             mWebsite = website;
1192         }
1193 
1194         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1195         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1196                 int backReferenceIndex) {
1197             final ContentProviderOperation.Builder builder = ContentProviderOperation
1198                     .newInsert(Data.CONTENT_URI);
1199             builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex);
1200             builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
1201             builder.withValue(Website.URL, mWebsite);
1202             // There's no information about the type of URL in vCard.
1203             // We use TYPE_HOMEPAGE for safety.
1204             builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
1205             operationList.add(builder.build());
1206         }
1207 
1208         @Override
isEmpty()1209         public boolean isEmpty() {
1210             return TextUtils.isEmpty(mWebsite);
1211         }
1212 
1213         @Override
equals(Object obj)1214         public boolean equals(Object obj) {
1215             if (this == obj) {
1216                 return true;
1217             }
1218             if (!(obj instanceof WebsiteData)) {
1219                 return false;
1220             }
1221             WebsiteData websiteData = (WebsiteData) obj;
1222             return TextUtils.equals(mWebsite, websiteData.mWebsite);
1223         }
1224 
1225         @Override
hashCode()1226         public int hashCode() {
1227             return mWebsite != null ? mWebsite.hashCode() : 0;
1228         }
1229 
1230         @Override
toString()1231         public String toString() {
1232             return "website: " + mWebsite;
1233         }
1234 
1235         @Override
getEntryLabel()1236         public EntryLabel getEntryLabel() {
1237             return EntryLabel.WEBSITE;
1238         }
1239 
getWebsite()1240         public String getWebsite() {
1241             return mWebsite;
1242         }
1243     }
1244 
1245     public static class BirthdayData implements EntryElement {
1246         private final String mBirthday;
1247 
BirthdayData(String birthday)1248         public BirthdayData(String birthday) {
1249             mBirthday = birthday;
1250         }
1251 
1252         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1253         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1254                 int backReferenceIndex) {
1255             final ContentProviderOperation.Builder builder = ContentProviderOperation
1256                     .newInsert(Data.CONTENT_URI);
1257             builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
1258             builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1259             builder.withValue(Event.START_DATE, mBirthday);
1260             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1261             operationList.add(builder.build());
1262         }
1263 
1264         @Override
isEmpty()1265         public boolean isEmpty() {
1266             return TextUtils.isEmpty(mBirthday);
1267         }
1268 
1269         @Override
equals(Object obj)1270         public boolean equals(Object obj) {
1271             if (this == obj) {
1272                 return true;
1273             }
1274             if (!(obj instanceof BirthdayData)) {
1275                 return false;
1276             }
1277             BirthdayData birthdayData = (BirthdayData) obj;
1278             return TextUtils.equals(mBirthday, birthdayData.mBirthday);
1279         }
1280 
1281         @Override
hashCode()1282         public int hashCode() {
1283             return mBirthday != null ? mBirthday.hashCode() : 0;
1284         }
1285 
1286         @Override
toString()1287         public String toString() {
1288             return "birthday: " + mBirthday;
1289         }
1290 
1291         @Override
getEntryLabel()1292         public EntryLabel getEntryLabel() {
1293             return EntryLabel.BIRTHDAY;
1294         }
1295 
getBirthday()1296         public String getBirthday() {
1297             return mBirthday;
1298         }
1299     }
1300 
1301     public static class AnniversaryData implements EntryElement {
1302         private final String mAnniversary;
1303 
AnniversaryData(String anniversary)1304         public AnniversaryData(String anniversary) {
1305             mAnniversary = anniversary;
1306         }
1307 
1308         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1309         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1310                 int backReferenceIndex) {
1311             final ContentProviderOperation.Builder builder = ContentProviderOperation
1312                     .newInsert(Data.CONTENT_URI);
1313             builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
1314             builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1315             builder.withValue(Event.START_DATE, mAnniversary);
1316             builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
1317             operationList.add(builder.build());
1318         }
1319 
1320         @Override
isEmpty()1321         public boolean isEmpty() {
1322             return TextUtils.isEmpty(mAnniversary);
1323         }
1324 
1325         @Override
equals(Object obj)1326         public boolean equals(Object obj) {
1327             if (this == obj) {
1328                 return true;
1329             }
1330             if (!(obj instanceof AnniversaryData)) {
1331                 return false;
1332             }
1333             AnniversaryData anniversaryData = (AnniversaryData) obj;
1334             return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary);
1335         }
1336 
1337         @Override
hashCode()1338         public int hashCode() {
1339             return mAnniversary != null ? mAnniversary.hashCode() : 0;
1340         }
1341 
1342         @Override
toString()1343         public String toString() {
1344             return "anniversary: " + mAnniversary;
1345         }
1346 
1347         @Override
getEntryLabel()1348         public EntryLabel getEntryLabel() {
1349             return EntryLabel.ANNIVERSARY;
1350         }
1351 
getAnniversary()1352         public String getAnniversary() { return mAnniversary; }
1353     }
1354 
1355     public static class SipData implements EntryElement {
1356         /**
1357          * Note that schema part ("sip:") is automatically removed. e.g.
1358          * "sip:username:password@host:port" becomes
1359          * "username:password@host:port"
1360          */
1361         private final String mAddress;
1362         private final int mType;
1363         private final String mLabel;
1364         private final boolean mIsPrimary;
1365 
SipData(String rawSip, int type, String label, boolean isPrimary)1366         public SipData(String rawSip, int type, String label, boolean isPrimary) {
1367             if (rawSip.startsWith("sip:")) {
1368                 mAddress = rawSip.substring(4);
1369             } else {
1370                 mAddress = rawSip;
1371             }
1372             mType = type;
1373             mLabel = label;
1374             mIsPrimary = isPrimary;
1375         }
1376 
1377         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1378         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1379                 int backReferenceIndex) {
1380             final ContentProviderOperation.Builder builder = ContentProviderOperation
1381                     .newInsert(Data.CONTENT_URI);
1382             builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex);
1383             builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
1384             builder.withValue(SipAddress.SIP_ADDRESS, mAddress);
1385             builder.withValue(SipAddress.TYPE, mType);
1386             if (mType == SipAddress.TYPE_CUSTOM) {
1387                 builder.withValue(SipAddress.LABEL, mLabel);
1388             }
1389             if (mIsPrimary) {
1390                 builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary);
1391             }
1392             operationList.add(builder.build());
1393         }
1394 
1395         @Override
isEmpty()1396         public boolean isEmpty() {
1397             return TextUtils.isEmpty(mAddress);
1398         }
1399 
1400         @Override
equals(Object obj)1401         public boolean equals(Object obj) {
1402             if (this == obj) {
1403                 return true;
1404             }
1405             if (!(obj instanceof SipData)) {
1406                 return false;
1407             }
1408             SipData sipData = (SipData) obj;
1409             return (mType == sipData.mType
1410                     && TextUtils.equals(mLabel, sipData.mLabel)
1411                     && TextUtils.equals(mAddress, sipData.mAddress)
1412                     && (mIsPrimary == sipData.mIsPrimary));
1413         }
1414 
1415         @Override
hashCode()1416         public int hashCode() {
1417             int hash = mType;
1418             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
1419             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
1420             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
1421             return hash;
1422         }
1423 
1424         @Override
toString()1425         public String toString() {
1426             return "sip: " + mAddress;
1427         }
1428 
1429         @Override
getEntryLabel()1430         public EntryLabel getEntryLabel() {
1431             return EntryLabel.SIP;
1432         }
1433 
1434         /**
1435          * @return Address part of the sip data. The schema ("sip:") isn't contained here.
1436          */
getAddress()1437         public String getAddress() { return mAddress; }
getType()1438         public int getType() { return mType; }
getLabel()1439         public String getLabel() { return mLabel; }
1440     }
1441 
1442     /**
1443      * Some Contacts data in Android cannot be converted to vCard
1444      * representation. VCardEntry preserves those data using this class.
1445      */
1446     public static class AndroidCustomData implements EntryElement {
1447         private final String mMimeType;
1448 
1449         private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN
1450 
AndroidCustomData(String mimeType, List<String> dataList)1451         public AndroidCustomData(String mimeType, List<String> dataList) {
1452             mMimeType = mimeType;
1453             mDataList = dataList;
1454         }
1455 
constructAndroidCustomData(List<String> list)1456         public static AndroidCustomData constructAndroidCustomData(List<String> list) {
1457             String mimeType;
1458             List<String> dataList;
1459 
1460             if (list == null) {
1461                 mimeType = null;
1462                 dataList = null;
1463             } else if (list.size() < 2) {
1464                 mimeType = list.get(0);
1465                 dataList = null;
1466             } else {
1467                 final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size()
1468                         : VCardConstants.MAX_DATA_COLUMN + 1;
1469                 mimeType = list.get(0);
1470                 dataList = list.subList(1, max);
1471             }
1472 
1473             return new AndroidCustomData(mimeType, dataList);
1474         }
1475 
1476         @Override
constructInsertOperation(List<ContentProviderOperation> operationList, int backReferenceIndex)1477         public void constructInsertOperation(List<ContentProviderOperation> operationList,
1478                 int backReferenceIndex) {
1479             final ContentProviderOperation.Builder builder = ContentProviderOperation
1480                     .newInsert(Data.CONTENT_URI);
1481             builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex);
1482             builder.withValue(Data.MIMETYPE, mMimeType);
1483             for (int i = 0; i < mDataList.size(); i++) {
1484                 String value = mDataList.get(i);
1485                 if (!TextUtils.isEmpty(value)) {
1486                     // 1-origin
1487                     builder.withValue("data" + (i + 1), value);
1488                 }
1489             }
1490             operationList.add(builder.build());
1491         }
1492 
1493         @Override
isEmpty()1494         public boolean isEmpty() {
1495             return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0;
1496         }
1497 
1498         @Override
equals(Object obj)1499         public boolean equals(Object obj) {
1500             if (this == obj) {
1501                 return true;
1502             }
1503             if (!(obj instanceof AndroidCustomData)) {
1504                 return false;
1505             }
1506             AndroidCustomData data = (AndroidCustomData) obj;
1507             if (!TextUtils.equals(mMimeType, data.mMimeType)) {
1508                 return false;
1509             }
1510             if (mDataList == null) {
1511                 return data.mDataList == null;
1512             } else {
1513                 final int size = mDataList.size();
1514                 if (size != data.mDataList.size()) {
1515                     return false;
1516                 }
1517                 for (int i = 0; i < size; i++) {
1518                     if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) {
1519                         return false;
1520                     }
1521                 }
1522                 return true;
1523             }
1524         }
1525 
1526         @Override
hashCode()1527         public int hashCode() {
1528             int hash = mMimeType != null ? mMimeType.hashCode() : 0;
1529             if (mDataList != null) {
1530                 for (String data : mDataList) {
1531                     hash = hash * 31 + (data != null ? data.hashCode() : 0);
1532                 }
1533             }
1534             return hash;
1535         }
1536 
1537         @Override
toString()1538         public String toString() {
1539             final StringBuilder builder = new StringBuilder();
1540             builder.append("android-custom: " + mMimeType + ", data: ");
1541             builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray()));
1542             return builder.toString();
1543         }
1544 
1545         @Override
getEntryLabel()1546         public EntryLabel getEntryLabel() {
1547             return EntryLabel.ANDROID_CUSTOM;
1548         }
1549 
getMimeType()1550         public String getMimeType() { return mMimeType; }
getDataList()1551         public List<String> getDataList() { return mDataList; }
1552     }
1553 
1554     private final NameData mNameData = new NameData();
1555     private List<PhoneData> mPhoneList;
1556     private List<EmailData> mEmailList;
1557     private List<PostalData> mPostalList;
1558     private List<OrganizationData> mOrganizationList;
1559     private List<ImData> mImList;
1560     private List<PhotoData> mPhotoList;
1561     private List<WebsiteData> mWebsiteList;
1562     private List<SipData> mSipList;
1563     private List<NicknameData> mNicknameList;
1564     private List<NoteData> mNoteList;
1565     private List<AndroidCustomData> mAndroidCustomDataList;
1566     private BirthdayData mBirthday;
1567     private AnniversaryData mAnniversary;
1568     private List<Pair<String, String>> mUnknownXData;
1569 
1570     /**
1571      * Inner iterator interface.
1572      */
1573     public interface EntryElementIterator {
onIterationStarted()1574         public void onIterationStarted();
1575 
onIterationEnded()1576         public void onIterationEnded();
1577 
1578         /**
1579          * Called when there are one or more {@link EntryElement} instances
1580          * associated with {@link EntryLabel}.
1581          */
onElementGroupStarted(EntryLabel label)1582         public void onElementGroupStarted(EntryLabel label);
1583 
1584         /**
1585          * Called after all {@link EntryElement} instances for
1586          * {@link EntryLabel} provided on {@link #onElementGroupStarted(EntryLabel)}
1587          * being processed by {@link #onElement(EntryElement)}
1588          */
onElementGroupEnded()1589         public void onElementGroupEnded();
1590 
1591         /**
1592          * @return should be true when child wants to continue the operation.
1593          *         False otherwise.
1594          */
onElement(EntryElement elem)1595         public boolean onElement(EntryElement elem);
1596     }
1597 
iterateAllData(EntryElementIterator iterator)1598     public final void iterateAllData(EntryElementIterator iterator) {
1599         iterator.onIterationStarted();
1600         iterator.onElementGroupStarted(mNameData.getEntryLabel());
1601         iterator.onElement(mNameData);
1602         iterator.onElementGroupEnded();
1603 
1604         iterateOneList(mPhoneList, iterator);
1605         iterateOneList(mEmailList, iterator);
1606         iterateOneList(mPostalList, iterator);
1607         iterateOneList(mOrganizationList, iterator);
1608         iterateOneList(mImList, iterator);
1609         iterateOneList(mPhotoList, iterator);
1610         iterateOneList(mWebsiteList, iterator);
1611         iterateOneList(mSipList, iterator);
1612         iterateOneList(mNicknameList, iterator);
1613         iterateOneList(mNoteList, iterator);
1614         iterateOneList(mAndroidCustomDataList, iterator);
1615 
1616         if (mBirthday != null) {
1617             iterator.onElementGroupStarted(mBirthday.getEntryLabel());
1618             iterator.onElement(mBirthday);
1619             iterator.onElementGroupEnded();
1620         }
1621         if (mAnniversary != null) {
1622             iterator.onElementGroupStarted(mAnniversary.getEntryLabel());
1623             iterator.onElement(mAnniversary);
1624             iterator.onElementGroupEnded();
1625         }
1626         iterator.onIterationEnded();
1627     }
1628 
iterateOneList(List<? extends EntryElement> elemList, EntryElementIterator iterator)1629     private void iterateOneList(List<? extends EntryElement> elemList,
1630             EntryElementIterator iterator) {
1631         if (elemList != null && elemList.size() > 0) {
1632             iterator.onElementGroupStarted(elemList.get(0).getEntryLabel());
1633             for (EntryElement elem : elemList) {
1634                 iterator.onElement(elem);
1635             }
1636             iterator.onElementGroupEnded();
1637         }
1638     }
1639 
1640     private class IsIgnorableIterator implements EntryElementIterator {
1641         private boolean mEmpty = true;
1642 
1643         @Override
onIterationStarted()1644         public void onIterationStarted() {
1645         }
1646 
1647         @Override
onIterationEnded()1648         public void onIterationEnded() {
1649         }
1650 
1651         @Override
onElementGroupStarted(EntryLabel label)1652         public void onElementGroupStarted(EntryLabel label) {
1653         }
1654 
1655         @Override
onElementGroupEnded()1656         public void onElementGroupEnded() {
1657         }
1658 
1659         @Override
onElement(EntryElement elem)1660         public boolean onElement(EntryElement elem) {
1661             if (!elem.isEmpty()) {
1662                 mEmpty = false;
1663                 // exit now
1664                 return false;
1665             } else {
1666                 return true;
1667             }
1668         }
1669 
getResult()1670         public boolean getResult() {
1671             return mEmpty;
1672         }
1673     }
1674 
1675     private class ToStringIterator implements EntryElementIterator {
1676         private StringBuilder mBuilder;
1677 
1678         private boolean mFirstElement;
1679 
1680         @Override
onIterationStarted()1681         public void onIterationStarted() {
1682             mBuilder = new StringBuilder();
1683             mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n");
1684         }
1685 
1686         @Override
onElementGroupStarted(EntryLabel label)1687         public void onElementGroupStarted(EntryLabel label) {
1688             mBuilder.append(label.toString() + ": ");
1689             mFirstElement = true;
1690         }
1691 
1692         @Override
onElement(EntryElement elem)1693         public boolean onElement(EntryElement elem) {
1694             if (!mFirstElement) {
1695                 mBuilder.append(", ");
1696                 mFirstElement = false;
1697             }
1698             mBuilder.append("[").append(elem.toString()).append("]");
1699             return true;
1700         }
1701 
1702         @Override
onElementGroupEnded()1703         public void onElementGroupEnded() {
1704             mBuilder.append("\n");
1705         }
1706 
1707         @Override
onIterationEnded()1708         public void onIterationEnded() {
1709             mBuilder.append("]]\n");
1710         }
1711 
1712         @Override
toString()1713         public String toString() {
1714             return mBuilder.toString();
1715         }
1716     }
1717 
1718     private class InsertOperationConstrutor implements EntryElementIterator {
1719         private final List<ContentProviderOperation> mOperationList;
1720 
1721         private final int mBackReferenceIndex;
1722 
InsertOperationConstrutor(List<ContentProviderOperation> operationList, int backReferenceIndex)1723         public InsertOperationConstrutor(List<ContentProviderOperation> operationList,
1724                 int backReferenceIndex) {
1725             mOperationList = operationList;
1726             mBackReferenceIndex = backReferenceIndex;
1727         }
1728 
1729         @Override
onIterationStarted()1730         public void onIterationStarted() {
1731         }
1732 
1733         @Override
onIterationEnded()1734         public void onIterationEnded() {
1735         }
1736 
1737         @Override
onElementGroupStarted(EntryLabel label)1738         public void onElementGroupStarted(EntryLabel label) {
1739         }
1740 
1741         @Override
onElementGroupEnded()1742         public void onElementGroupEnded() {
1743         }
1744 
1745         @Override
onElement(EntryElement elem)1746         public boolean onElement(EntryElement elem) {
1747             if (!elem.isEmpty()) {
1748                 elem.constructInsertOperation(mOperationList, mBackReferenceIndex);
1749             }
1750             return true;
1751         }
1752     }
1753 
1754     private final int mVCardType;
1755     private Account mAccount;
1756 
getAccount()1757     public Account getAccount() {
1758         return mAccount;
1759     }
1760 
setAccount(Account account)1761     public void setAccount(Account account) {
1762         mAccount = account;
1763     }
1764 
1765     private List<VCardEntry> mChildren;
1766 
1767     @Override
toString()1768     public String toString() {
1769         ToStringIterator iterator = new ToStringIterator();
1770         iterateAllData(iterator);
1771         return iterator.toString();
1772     }
1773 
VCardEntry()1774     public VCardEntry() {
1775         this(VCardConfig.VCARD_TYPE_V21_GENERIC);
1776     }
1777 
VCardEntry(int vcardType)1778     public VCardEntry(int vcardType) {
1779         this(vcardType, null);
1780     }
1781 
VCardEntry(int vcardType, Account account)1782     public VCardEntry(int vcardType, Account account) {
1783         mVCardType = vcardType;
1784         mAccount = account;
1785     }
1786 
addPhone(int type, String data, String label, boolean isPrimary)1787     private void addPhone(int type, String data, String label, boolean isPrimary) {
1788         if (mPhoneList == null) {
1789             mPhoneList = new ArrayList<PhoneData>();
1790         }
1791         final StringBuilder builder = new StringBuilder();
1792         final String trimmed = data.trim();
1793         final String formattedNumber;
1794         if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
1795             formattedNumber = trimmed;
1796         } else {
1797             // TODO: from the view of vCard spec these auto conversions should be removed.
1798             // Note that some other codes (like the phone number formatter) or modules expect this
1799             // auto conversion (bug 5178723), so just omitting this code won't be preferable enough
1800             // (bug 4177894)
1801             boolean hasPauseOrWait = false;
1802             final int length = trimmed.length();
1803             for (int i = 0; i < length; i++) {
1804                 char ch = trimmed.charAt(i);
1805                 // See RFC 3601 and docs for PhoneNumberUtils for more info.
1806                 if (ch == 'p' || ch == 'P') {
1807                     builder.append(PhoneNumberUtils.PAUSE);
1808                     hasPauseOrWait = true;
1809                 } else if (ch == 'w' || ch == 'W') {
1810                     builder.append(PhoneNumberUtils.WAIT);
1811                     hasPauseOrWait = true;
1812                 } else if (PhoneNumberUtils.is12Key(ch) || (i == 0 && ch == '+')) {
1813                     builder.append(ch);
1814                 }
1815             }
1816             if (!hasPauseOrWait) {
1817                 final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
1818                 formattedNumber = PhoneNumberUtilsPort.formatNumber(
1819                         builder.toString(), formattingType);
1820             } else {
1821                 formattedNumber = builder.toString();
1822             }
1823         }
1824         PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary);
1825         mPhoneList.add(phoneData);
1826     }
1827 
addSip(String sipData, int type, String label, boolean isPrimary)1828     private void addSip(String sipData, int type, String label, boolean isPrimary) {
1829         if (mSipList == null) {
1830             mSipList = new ArrayList<SipData>();
1831         }
1832         mSipList.add(new SipData(sipData, type, label, isPrimary));
1833     }
1834 
addNickName(final String nickName)1835     private void addNickName(final String nickName) {
1836         if (mNicknameList == null) {
1837             mNicknameList = new ArrayList<NicknameData>();
1838         }
1839         mNicknameList.add(new NicknameData(nickName));
1840     }
1841 
addEmail(int type, String data, String label, boolean isPrimary)1842     private void addEmail(int type, String data, String label, boolean isPrimary) {
1843         if (mEmailList == null) {
1844             mEmailList = new ArrayList<EmailData>();
1845         }
1846         mEmailList.add(new EmailData(data, type, label, isPrimary));
1847     }
1848 
addPostal(int type, List<String> propValueList, String label, boolean isPrimary)1849     private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) {
1850         if (mPostalList == null) {
1851             mPostalList = new ArrayList<PostalData>(0);
1852         }
1853         mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary,
1854                 mVCardType));
1855     }
1856 
1857     /**
1858      * Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or
1859      * {@link #handleTitleValue(String)}.
1860      */
addNewOrganization(final String organizationName, final String departmentName, final String titleName, final String phoneticName, int type, final boolean isPrimary)1861     private void addNewOrganization(final String organizationName, final String departmentName,
1862             final String titleName, final String phoneticName, int type, final boolean isPrimary) {
1863         if (mOrganizationList == null) {
1864             mOrganizationList = new ArrayList<OrganizationData>();
1865         }
1866         mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName,
1867                 phoneticName, type, isPrimary));
1868     }
1869 
1870     private static final List<String> sEmptyList = Collections
1871             .unmodifiableList(new ArrayList<String>(0));
1872 
buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap)1873     private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
1874         final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
1875         if (sortAsCollection != null && sortAsCollection.size() != 0) {
1876             if (sortAsCollection.size() > 1) {
1877                 Log.w(LOG_TAG,
1878                         "Incorrect multiple SORT_AS parameters detected: "
1879                                 + Arrays.toString(sortAsCollection.toArray()));
1880             }
1881             final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
1882                     .iterator().next(), mVCardType);
1883             final StringBuilder builder = new StringBuilder();
1884             for (final String elem : sortNames) {
1885                 builder.append(elem);
1886             }
1887             return builder.toString();
1888         } else {
1889             return null;
1890         }
1891     }
1892 
1893     /**
1894      * Set "ORG" related values to the appropriate data. If there's more than
1895      * one {@link OrganizationData} objects, this input data are attached to the
1896      * last one which does not have valid values (not including empty but only
1897      * null). If there's no {@link OrganizationData} object, a new
1898      * {@link OrganizationData} is created, whose title is set to null.
1899      */
handleOrgValue(final int type, List<String> orgList, Map<String, Collection<String>> paramMap, boolean isPrimary)1900     private void handleOrgValue(final int type, List<String> orgList,
1901             Map<String, Collection<String>> paramMap, boolean isPrimary) {
1902         final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
1903         if (orgList == null) {
1904             orgList = sEmptyList;
1905         }
1906         final String organizationName;
1907         final String departmentName;
1908         final int size = orgList.size();
1909         switch (size) {
1910         case 0: {
1911             organizationName = "";
1912             departmentName = null;
1913             break;
1914         }
1915         case 1: {
1916             organizationName = orgList.get(0);
1917             departmentName = null;
1918             break;
1919         }
1920         default: { // More than 1.
1921             organizationName = orgList.get(0);
1922             // We're not sure which is the correct string for department.
1923             // In order to keep all the data, concatinate the rest of elements.
1924             StringBuilder builder = new StringBuilder();
1925             for (int i = 1; i < size; i++) {
1926                 if (i > 1) {
1927                     builder.append(' ');
1928                 }
1929                 builder.append(orgList.get(i));
1930             }
1931             departmentName = builder.toString();
1932         }
1933         }
1934         if (mOrganizationList == null) {
1935             // Create new first organization entry, with "null" title which may be
1936             // added via handleTitleValue().
1937             addNewOrganization(organizationName, departmentName, null, phoneticName, type,
1938                     isPrimary);
1939             return;
1940         }
1941         for (OrganizationData organizationData : mOrganizationList) {
1942             // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
1943             // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
1944             if (organizationData.mOrganizationName == null
1945                     && organizationData.mDepartmentName == null) {
1946                 // Probably the "TITLE" property comes before the "ORG" property via
1947                 // handleTitleLine().
1948                 organizationData.mOrganizationName = organizationName;
1949                 organizationData.mDepartmentName = departmentName;
1950                 organizationData.mIsPrimary = isPrimary;
1951                 return;
1952             }
1953         }
1954         // No OrganizatioData is available. Create another one, with "null" title, which may be
1955         // added via handleTitleValue().
1956         addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
1957     }
1958 
1959     /**
1960      * Set "title" value to the appropriate data. If there's more than one
1961      * OrganizationData objects, this input is attached to the last one which
1962      * does not have valid title value (not including empty but only null). If
1963      * there's no OrganizationData object, a new OrganizationData is created,
1964      * whose company name is set to null.
1965      */
handleTitleValue(final String title)1966     private void handleTitleValue(final String title) {
1967         if (mOrganizationList == null) {
1968             // Create new first organization entry, with "null" other info, which may be
1969             // added via handleOrgValue().
1970             addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
1971             return;
1972         }
1973         for (OrganizationData organizationData : mOrganizationList) {
1974             if (organizationData.mTitle == null) {
1975                 organizationData.mTitle = title;
1976                 return;
1977             }
1978         }
1979         // No Organization is available. Create another one, with "null" other info, which may be
1980         // added via handleOrgValue().
1981         addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
1982     }
1983 
addIm(int protocol, String customProtocol, String propValue, int type, boolean isPrimary)1984     private void addIm(int protocol, String customProtocol, String propValue, int type,
1985             boolean isPrimary) {
1986         if (mImList == null) {
1987             mImList = new ArrayList<ImData>();
1988         }
1989         mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary));
1990     }
1991 
addNote(final String note)1992     private void addNote(final String note) {
1993         if (mNoteList == null) {
1994             mNoteList = new ArrayList<NoteData>(1);
1995         }
1996         mNoteList.add(new NoteData(note));
1997     }
1998 
addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary)1999     private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
2000         if (mPhotoList == null) {
2001             mPhotoList = new ArrayList<PhotoData>(1);
2002         }
2003         final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary);
2004         mPhotoList.add(photoData);
2005     }
2006 
2007     /**
2008      * Tries to extract paramMap, constructs SORT-AS parameter values, and store
2009      * them in appropriate phonetic name variables. This method does not care
2010      * the vCard version. Even when we have SORT-AS parameters in invalid
2011      * versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't
2012      * drop meaningful information. If we had this parameter in the N field of
2013      * vCard 3.0, and the contact data also have SORT-STRING, we will prefer
2014      * SORT-STRING, since it is regitimate property to be understood.
2015      */
tryHandleSortAsName(final Map<String, Collection<String>> paramMap)2016     private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
2017         if (VCardConfig.isVersion30(mVCardType)
2018                 && !(TextUtils.isEmpty(mNameData.mPhoneticFamily)
2019                         && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
2020                         .isEmpty(mNameData.mPhoneticGiven))) {
2021             return;
2022         }
2023 
2024         final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
2025         if (sortAsCollection != null && sortAsCollection.size() != 0) {
2026             if (sortAsCollection.size() > 1) {
2027                 Log.w(LOG_TAG,
2028                         "Incorrect multiple SORT_AS parameters detected: "
2029                                 + Arrays.toString(sortAsCollection.toArray()));
2030             }
2031             final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
2032                     .iterator().next(), mVCardType);
2033             int size = sortNames.size();
2034             if (size > 3) {
2035                 size = 3;
2036             }
2037             switch (size) {
2038             case 3:
2039                 mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$
2040             case 2:
2041                 mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$
2042             default:
2043                 mNameData.mPhoneticFamily = sortNames.get(0);
2044                 break;
2045             }
2046         }
2047     }
2048 
2049     @SuppressWarnings("fallthrough")
handleNProperty(final List<String> paramValues, Map<String, Collection<String>> paramMap)2050     private void handleNProperty(final List<String> paramValues,
2051             Map<String, Collection<String>> paramMap) {
2052         // in vCard 4.0, SORT-AS parameter is available.
2053         tryHandleSortAsName(paramMap);
2054 
2055         // Family, Given, Middle, Prefix, Suffix. (1 - 5)
2056         int size;
2057         if (paramValues == null || (size = paramValues.size()) < 1) {
2058             return;
2059         }
2060         if (size > 5) {
2061             size = 5;
2062         }
2063 
2064         switch (size) {
2065         // Fall-through.
2066         case 5:
2067             mNameData.mSuffix = paramValues.get(4);
2068         case 4:
2069             mNameData.mPrefix = paramValues.get(3);
2070         case 3:
2071             mNameData.mMiddle = paramValues.get(2);
2072         case 2:
2073             mNameData.mGiven = paramValues.get(1);
2074         default:
2075             mNameData.mFamily = paramValues.get(0);
2076         }
2077     }
2078 
2079     /**
2080      * Note: Some Japanese mobile phones use this field for phonetic name, since
2081      * vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the
2082      * field has some ';'s in it. Assume the ';' means the same meaning in N
2083      * property
2084      */
2085     @SuppressWarnings("fallthrough")
handlePhoneticNameFromSound(List<String> elems)2086     private void handlePhoneticNameFromSound(List<String> elems) {
2087         if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily)
2088                 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
2089                 .isEmpty(mNameData.mPhoneticGiven))) {
2090             // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
2091             // Ignore "SOUND;X-IRMC-N".
2092             return;
2093         }
2094 
2095         int size;
2096         if (elems == null || (size = elems.size()) < 1) {
2097             return;
2098         }
2099 
2100         // Assume that the order is "Family, Given, Middle".
2101         // This is not from specification but mere assumption. Some Japanese
2102         // phones use this order.
2103         if (size > 3) {
2104             size = 3;
2105         }
2106 
2107         if (elems.get(0).length() > 0) {
2108             boolean onlyFirstElemIsNonEmpty = true;
2109             for (int i = 1; i < size; i++) {
2110                 if (elems.get(i).length() > 0) {
2111                     onlyFirstElemIsNonEmpty = false;
2112                     break;
2113                 }
2114             }
2115             if (onlyFirstElemIsNonEmpty) {
2116                 final String[] namesArray = elems.get(0).split(" ");
2117                 final int nameArrayLength = namesArray.length;
2118                 if (nameArrayLength == 3) {
2119                     // Assume the string is "Family Middle Given".
2120                     mNameData.mPhoneticFamily = namesArray[0];
2121                     mNameData.mPhoneticMiddle = namesArray[1];
2122                     mNameData.mPhoneticGiven = namesArray[2];
2123                 } else if (nameArrayLength == 2) {
2124                     // Assume the string is "Family Given" based on the Japanese mobile
2125                     // phones' preference.
2126                     mNameData.mPhoneticFamily = namesArray[0];
2127                     mNameData.mPhoneticGiven = namesArray[1];
2128                 } else {
2129                     mNameData.mPhoneticGiven = elems.get(0);
2130                 }
2131                 return;
2132             }
2133         }
2134 
2135         switch (size) {
2136         // fallthrough
2137         case 3:
2138             mNameData.mPhoneticMiddle = elems.get(2);
2139         case 2:
2140             mNameData.mPhoneticGiven = elems.get(1);
2141         default:
2142             mNameData.mPhoneticFamily = elems.get(0);
2143         }
2144     }
2145 
addProperty(final VCardProperty property)2146     public void addProperty(final VCardProperty property) {
2147         final String propertyName = property.getName();
2148         final Map<String, Collection<String>> paramMap = property.getParameterMap();
2149         final List<String> propertyValueList = property.getValueList();
2150         byte[] propertyBytes = property.getByteValue();
2151 
2152         if ((propertyValueList == null || propertyValueList.size() == 0)
2153                 && propertyBytes == null) {
2154             return;
2155         }
2156         final String propValue = (propertyValueList != null
2157                 ? listToString(propertyValueList).trim()
2158                 : null);
2159 
2160         if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) {
2161             // vCard version. Ignore this.
2162         } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) {
2163             mNameData.mFormatted = propValue;
2164         } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) {
2165             // Only in vCard 3.0. Use this if FN doesn't exist though it is
2166             // required in vCard 3.0.
2167             if (TextUtils.isEmpty(mNameData.mFormatted)) {
2168                 mNameData.mFormatted = propValue;
2169             }
2170         } else if (propertyName.equals(VCardConstants.PROPERTY_N)) {
2171             handleNProperty(propertyValueList, paramMap);
2172         } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
2173             mNameData.mSortString = propValue;
2174         } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME)
2175                 || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
2176             addNickName(propValue);
2177         } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) {
2178             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2179             if (typeCollection != null
2180                     && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
2181                 // As of 2009-10-08, Parser side does not split a property value into separated
2182                 // values using ';' (in other words, propValueList.size() == 1),
2183                 // which is correct behavior from the view of vCard 2.1.
2184                 // But we want it to be separated, so do the separation here.
2185                 final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue,
2186                         mVCardType);
2187                 handlePhoneticNameFromSound(phoneticNameList);
2188             } else {
2189                 // Ignore this field since Android cannot understand what it is.
2190             }
2191         } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) {
2192             boolean valuesAreAllEmpty = true;
2193             for (String value : propertyValueList) {
2194                 if (!TextUtils.isEmpty(value)) {
2195                     valuesAreAllEmpty = false;
2196                     break;
2197                 }
2198             }
2199             if (valuesAreAllEmpty) {
2200                 return;
2201             }
2202 
2203             int type = -1;
2204             String label = null;
2205             boolean isPrimary = false;
2206             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2207             if (typeCollection != null) {
2208                 for (final String typeStringOrg : typeCollection) {
2209                     final String typeStringUpperCase = typeStringOrg.toUpperCase();
2210                     if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2211                         isPrimary = true;
2212                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2213                         type = StructuredPostal.TYPE_HOME;
2214                         label = null;
2215                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)
2216                             || typeStringUpperCase
2217                                     .equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
2218                         // "COMPANY" seems emitted by Windows Mobile, which is not
2219                         // specifically supported by vCard 2.1. We assume this is same
2220                         // as "WORK".
2221                         type = StructuredPostal.TYPE_WORK;
2222                         label = null;
2223                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL)
2224                             || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM)
2225                             || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
2226                         // We do not have any appropriate way to store this information.
2227                     } else if (type < 0) { // If no other type is specified before.
2228                         type = StructuredPostal.TYPE_CUSTOM;
2229                         if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2230                             label = typeStringOrg.substring(2);
2231                         } else {
2232                             label = typeStringOrg;
2233                         }
2234                         // {@link ContactsContract} has a {@link StructuredPostal.TYPE_OTHER}, so
2235                         // if the custom type is "other", map it from {@code TYPE_CUSTOM} to
2236                         // {@code TYPE_OTHER}.
2237                         if (VCardConstants.PARAM_ADR_EXTRA_TYPE_OTHER.equals(label.toUpperCase())) {
2238                             type = StructuredPostal.TYPE_OTHER;
2239                             label = null;
2240                         }
2241                     }
2242                 }
2243             }
2244             // We use "HOME" as default
2245             if (type < 0) {
2246                 type = StructuredPostal.TYPE_HOME;
2247             }
2248 
2249             addPostal(type, propertyValueList, label, isPrimary);
2250         } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) {
2251             int type = -1;
2252             String label = null;
2253             boolean isPrimary = false;
2254             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2255             if (typeCollection != null) {
2256                 for (final String typeStringOrg : typeCollection) {
2257                     final String typeStringUpperCase = typeStringOrg.toUpperCase();
2258                     if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2259                         isPrimary = true;
2260                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2261                         type = Email.TYPE_HOME;
2262                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
2263                         type = Email.TYPE_WORK;
2264                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
2265                         type = Email.TYPE_MOBILE;
2266                     } else if (type < 0) { // If no other type is specified before
2267                         if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2268                             label = typeStringOrg.substring(2);
2269                         } else {
2270                             label = typeStringOrg;
2271                         }
2272                         type = Email.TYPE_CUSTOM;
2273                     }
2274                 }
2275             }
2276             if (type < 0) {
2277                 type = Email.TYPE_OTHER;
2278             }
2279             addEmail(type, propValue, label, isPrimary);
2280         } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) {
2281             // vCard specification does not specify other types.
2282             final int type = Organization.TYPE_WORK;
2283             boolean isPrimary = false;
2284             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2285             if (typeCollection != null) {
2286                 for (String typeString : typeCollection) {
2287                     if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
2288                         isPrimary = true;
2289                     }
2290                 }
2291             }
2292             handleOrgValue(type, propertyValueList, paramMap, isPrimary);
2293         } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) {
2294             handleTitleValue(propValue);
2295         } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) {
2296             // This conflicts with TITLE. Ignore for now...
2297             // handleTitleValue(propValue);
2298         } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO)
2299                 || propertyName.equals(VCardConstants.PROPERTY_LOGO)) {
2300             Collection<String> paramMapValue = paramMap.get("VALUE");
2301             if (paramMapValue != null && paramMapValue.contains("URL")) {
2302                 // Currently we do not have appropriate example for testing this case.
2303             } else {
2304                 final Collection<String> typeCollection = paramMap.get("TYPE");
2305                 String formatName = null;
2306                 boolean isPrimary = false;
2307                 if (typeCollection != null) {
2308                     for (String typeValue : typeCollection) {
2309                         if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
2310                             isPrimary = true;
2311                         } else if (formatName == null) {
2312                             formatName = typeValue;
2313                         }
2314                     }
2315                 }
2316                 addPhotoBytes(formatName, propertyBytes, isPrimary);
2317             }
2318         } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) {
2319             String phoneNumber = null;
2320             boolean isSip = false;
2321             if (VCardConfig.isVersion40(mVCardType)) {
2322                 // Given propValue is in URI format, not in phone number format used until
2323                 // vCard 3.0.
2324                 if (propValue.startsWith("sip:")) {
2325                     isSip = true;
2326                 } else if (propValue.startsWith("tel:")) {
2327                     phoneNumber = propValue.substring(4);
2328                 } else {
2329                     // We don't know appropriate way to handle the other schemas. Also,
2330                     // we may still have non-URI phone number. To keep given data as much as
2331                     // we can, just save original value here.
2332                     phoneNumber = propValue;
2333                 }
2334             } else {
2335                 phoneNumber = propValue;
2336             }
2337 
2338             if (isSip) {
2339                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2340                 handleSipCase(propValue, typeCollection);
2341             } else {
2342                 if (propValue.length() == 0) {
2343                     return;
2344                 }
2345 
2346                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2347                 final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection,
2348                         phoneNumber);
2349                 final int type;
2350                 final String label;
2351                 if (typeObject instanceof Integer) {
2352                     type = (Integer) typeObject;
2353                     label = null;
2354                 } else {
2355                     type = Phone.TYPE_CUSTOM;
2356                     label = typeObject.toString();
2357                 }
2358 
2359                 final boolean isPrimary;
2360                 if (typeCollection != null &&
2361                         typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
2362                     isPrimary = true;
2363                 } else {
2364                     isPrimary = false;
2365                 }
2366 
2367                 addPhone(type, phoneNumber, label, isPrimary);
2368             }
2369         } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
2370             // The phone number available via Skype.
2371             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2372             final int type = Phone.TYPE_OTHER;
2373             final boolean isPrimary;
2374             if (typeCollection != null
2375                     && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
2376                 isPrimary = true;
2377             } else {
2378                 isPrimary = false;
2379             }
2380             addPhone(type, propValue, null, isPrimary);
2381         } else if (sImMap.containsKey(propertyName)) {
2382             final int protocol = sImMap.get(propertyName);
2383             boolean isPrimary = false;
2384             int type = -1;
2385             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2386             if (typeCollection != null) {
2387                 for (String typeString : typeCollection) {
2388                     if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
2389                         isPrimary = true;
2390                     } else if (type < 0) {
2391                         if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
2392                             type = Im.TYPE_HOME;
2393                         } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
2394                             type = Im.TYPE_WORK;
2395                         }
2396                     }
2397                 }
2398             }
2399             if (type < 0) {
2400                 type = Im.TYPE_HOME;
2401             }
2402             addIm(protocol, null, propValue, type, isPrimary);
2403         } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) {
2404             addNote(propValue);
2405         } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) {
2406             if (mWebsiteList == null) {
2407                 mWebsiteList = new ArrayList<WebsiteData>(1);
2408             }
2409             mWebsiteList.add(new WebsiteData(propValue));
2410         } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) {
2411             mBirthday = new BirthdayData(propValue);
2412         } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
2413             mAnniversary = new AnniversaryData(propValue);
2414         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
2415             mNameData.mPhoneticGiven = propValue;
2416         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
2417             mNameData.mPhoneticMiddle = propValue;
2418         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
2419             mNameData.mPhoneticFamily = propValue;
2420         } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) {
2421             // See also RFC 4770 (for vCard 3.0)
2422             if (propValue.startsWith("sip:")) {
2423                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2424                 handleSipCase(propValue, typeCollection);
2425             }
2426         } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) {
2427             if (!TextUtils.isEmpty(propValue)) {
2428                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2429                 handleSipCase(propValue, typeCollection);
2430             }
2431         } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
2432             final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue,
2433                     mVCardType);
2434             handleAndroidCustomProperty(customPropertyList);
2435         } else if (propertyName.toUpperCase().startsWith("X-")) {
2436             // Catch all for X- properties. The caller can decide what to do with these.
2437             if (mUnknownXData == null) {
2438                 mUnknownXData = new ArrayList<Pair<String, String>>();
2439             }
2440             mUnknownXData.add(new Pair<String, String>(propertyName, propValue));
2441         } else {
2442         }
2443         // Be careful when adding some logic here, as some blocks above may use "return".
2444     }
2445 
2446     /**
2447      * @param propValue may contain "sip:" at the beginning.
2448      * @param typeCollection
2449      */
handleSipCase(String propValue, Collection<String> typeCollection)2450     private void handleSipCase(String propValue, Collection<String> typeCollection) {
2451         if (TextUtils.isEmpty(propValue)) {
2452             return;
2453         }
2454         if (propValue.startsWith("sip:")) {
2455             propValue = propValue.substring(4);
2456             if (propValue.length() == 0) {
2457                 return;
2458             }
2459         }
2460 
2461         int type = -1;
2462         String label = null;
2463         boolean isPrimary = false;
2464         if (typeCollection != null) {
2465             for (final String typeStringOrg : typeCollection) {
2466                 final String typeStringUpperCase = typeStringOrg.toUpperCase();
2467                 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2468                     isPrimary = true;
2469                 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2470                     type = SipAddress.TYPE_HOME;
2471                 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
2472                     type = SipAddress.TYPE_WORK;
2473                 } else if (type < 0) { // If no other type is specified before
2474                     if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2475                         label = typeStringOrg.substring(2);
2476                     } else {
2477                         label = typeStringOrg;
2478                     }
2479                     type = SipAddress.TYPE_CUSTOM;
2480                 }
2481             }
2482         }
2483         if (type < 0) {
2484             type = SipAddress.TYPE_OTHER;
2485         }
2486         addSip(propValue, type, label, isPrimary);
2487     }
2488 
addChild(VCardEntry child)2489     public void addChild(VCardEntry child) {
2490         if (mChildren == null) {
2491             mChildren = new ArrayList<VCardEntry>();
2492         }
2493         mChildren.add(child);
2494     }
2495 
handleAndroidCustomProperty(final List<String> customPropertyList)2496     private void handleAndroidCustomProperty(final List<String> customPropertyList) {
2497         if (mAndroidCustomDataList == null) {
2498             mAndroidCustomDataList = new ArrayList<AndroidCustomData>();
2499         }
2500         mAndroidCustomDataList
2501                 .add(AndroidCustomData.constructAndroidCustomData(customPropertyList));
2502     }
2503 
2504     /**
2505      * Construct the display name. The constructed data must not be null.
2506      */
constructDisplayName()2507     private String constructDisplayName() {
2508         String displayName = null;
2509         // FullName (created via "FN" or "NAME" field) is prefered.
2510         if (!TextUtils.isEmpty(mNameData.mFormatted)) {
2511             displayName = mNameData.mFormatted;
2512         } else if (!mNameData.emptyStructuredName()) {
2513             displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily,
2514                     mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix);
2515         } else if (!mNameData.emptyPhoneticStructuredName()) {
2516             displayName = VCardUtils.constructNameFromElements(mVCardType,
2517                     mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven);
2518         } else if (mEmailList != null && mEmailList.size() > 0) {
2519             displayName = mEmailList.get(0).mAddress;
2520         } else if (mPhoneList != null && mPhoneList.size() > 0) {
2521             displayName = mPhoneList.get(0).mNumber;
2522         } else if (mPostalList != null && mPostalList.size() > 0) {
2523             displayName = mPostalList.get(0).getFormattedAddress(mVCardType);
2524         } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
2525             displayName = mOrganizationList.get(0).getFormattedString();
2526         }
2527         if (displayName == null) {
2528             displayName = "";
2529         }
2530         return displayName;
2531     }
2532 
2533     /**
2534      * Consolidate several fielsds (like mName) using name candidates,
2535      */
consolidateFields()2536     public void consolidateFields() {
2537         mNameData.displayName = constructDisplayName();
2538     }
2539 
2540     /**
2541      * @return true when this object has nothing meaningful for Android's
2542      *         Contacts, and thus is "ignorable" for Android's Contacts. This
2543      *         does not mean an original vCard is really empty. Even when the
2544      *         original vCard has some fields, this may ignore it if those
2545      *         fields cannot be transcoded into Android's Contacts
2546      *         representation.
2547      */
isIgnorable()2548     public boolean isIgnorable() {
2549         IsIgnorableIterator iterator = new IsIgnorableIterator();
2550         iterateAllData(iterator);
2551         return iterator.getResult();
2552     }
2553 
2554     /**
2555      * Constructs the list of insert operation for this object. When the
2556      * operationList argument is null, this method creates a new ArrayList and
2557      * return it. The returned object is filled with new insert operations for
2558      * this object. When operationList argument is not null, this method appends
2559      * those new operations into the object instead of creating a new ArrayList.
2560      *
2561      * @param resolver {@link ContentResolver} object to be used in this method.
2562      * @param operationList object to be filled. You can use this argument to
2563      *            concatinate operation lists. If null, this method creates a
2564      *            new array object.
2565      * @return If operationList argument is null, new object with new insert
2566      *         operations. If it is not null, the operationList object with
2567      *         operations inserted by this method.
2568      */
constructInsertOperations(ContentResolver resolver, ArrayList<ContentProviderOperation> operationList)2569     public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver,
2570             ArrayList<ContentProviderOperation> operationList) {
2571         if (operationList == null) {
2572             operationList = new ArrayList<ContentProviderOperation>();
2573         }
2574 
2575         if (isIgnorable()) {
2576             return operationList;
2577         }
2578 
2579         final int backReferenceIndex = operationList.size();
2580 
2581         // After applying the batch the first result's Uri is returned so it is important that
2582         // the RawContact is the first operation that gets inserted into the list.
2583         ContentProviderOperation.Builder builder = ContentProviderOperation
2584                 .newInsert(RawContacts.CONTENT_URI);
2585         if (mAccount != null) {
2586             builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
2587             builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
2588         } else {
2589             builder.withValue(RawContacts.ACCOUNT_NAME, null);
2590             builder.withValue(RawContacts.ACCOUNT_TYPE, null);
2591         }
2592         // contacts favorites
2593         if (getStarred()) {
2594             builder.withValue(RawContacts.STARRED, 1);
2595         }
2596         operationList.add(builder.build());
2597 
2598         int start = operationList.size();
2599         iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex));
2600         int end = operationList.size();
2601 
2602         return operationList;
2603     }
2604 
buildFromResolver(ContentResolver resolver)2605     public static VCardEntry buildFromResolver(ContentResolver resolver) {
2606         return buildFromResolver(resolver, Contacts.CONTENT_URI);
2607     }
2608 
buildFromResolver(ContentResolver resolver, Uri uri)2609     public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
2610         return null;
2611     }
2612 
listToString(List<String> list)2613     private String listToString(List<String> list) {
2614         final int size = list.size();
2615         if (size > 1) {
2616             StringBuilder builder = new StringBuilder();
2617             int i = 0;
2618             for (String type : list) {
2619                 builder.append(type);
2620                 if (i < size - 1) {
2621                     builder.append(";");
2622                 }
2623             }
2624             return builder.toString();
2625         } else if (size == 1) {
2626             return list.get(0);
2627         } else {
2628             return "";
2629         }
2630     }
2631 
getNameData()2632     public final NameData getNameData() {
2633         return mNameData;
2634     }
2635 
getNickNameList()2636     public final List<NicknameData> getNickNameList() {
2637         return mNicknameList;
2638     }
2639 
getBirthday()2640     public final String getBirthday() {
2641         return mBirthday != null ? mBirthday.mBirthday : null;
2642     }
2643 
getNotes()2644     public final List<NoteData> getNotes() {
2645         return mNoteList;
2646     }
2647 
getPhoneList()2648     public final List<PhoneData> getPhoneList() {
2649         return mPhoneList;
2650     }
2651 
getEmailList()2652     public final List<EmailData> getEmailList() {
2653         return mEmailList;
2654     }
2655 
getPostalList()2656     public final List<PostalData> getPostalList() {
2657         return mPostalList;
2658     }
2659 
getOrganizationList()2660     public final List<OrganizationData> getOrganizationList() {
2661         return mOrganizationList;
2662     }
2663 
getImList()2664     public final List<ImData> getImList() {
2665         return mImList;
2666     }
2667 
getPhotoList()2668     public final List<PhotoData> getPhotoList() {
2669         return mPhotoList;
2670     }
2671 
getWebsiteList()2672     public final List<WebsiteData> getWebsiteList() {
2673         return mWebsiteList;
2674     }
2675 
2676     /**
2677      * @hide this interface may be changed for better support of vCard 4.0 (UID)
2678      */
getChildlen()2679     public final List<VCardEntry> getChildlen() {
2680         return mChildren;
2681     }
2682 
getDisplayName()2683     public String getDisplayName() {
2684         if (mNameData.displayName == null) {
2685             mNameData.displayName = constructDisplayName();
2686         }
2687         return mNameData.displayName;
2688     }
2689 
getUnknownXData()2690     public List<Pair<String, String>> getUnknownXData() {
2691         return mUnknownXData;
2692     }
2693 }
2694