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