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