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