VCardEntry.java revision 517540b6e3903371def5eb4ca44c2bb2ff91ae30
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        // TODO: make this private. Currently the app outside this class refers to this.
980        public final byte[] photoBytes;
981
982        private Integer mHashCode = null;
983
984        public PhotoData(String format, byte[] photoBytes, boolean isPrimary) {
985            mFormat = format;
986            this.photoBytes = photoBytes;
987            mIsPrimary = isPrimary;
988        }
989
990        @Override
991        public void constructInsertOperation(List<ContentProviderOperation> operationList,
992                int backReferenceIndex) {
993            final ContentProviderOperation.Builder builder = ContentProviderOperation
994                    .newInsert(Data.CONTENT_URI);
995            builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex);
996            builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
997            builder.withValue(Photo.PHOTO, photoBytes);
998            if (mIsPrimary) {
999                builder.withValue(Photo.IS_PRIMARY, 1);
1000            }
1001            operationList.add(builder.build());
1002        }
1003
1004        @Override
1005        public boolean isEmpty() {
1006            return photoBytes == null || photoBytes.length == 0;
1007        }
1008
1009        @Override
1010        public boolean equals(Object obj) {
1011            if (this == obj) {
1012                return true;
1013            }
1014            if (!(obj instanceof PhotoData)) {
1015                return false;
1016            }
1017            PhotoData photoData = (PhotoData) obj;
1018            return (TextUtils.equals(mFormat, photoData.mFormat)
1019                    && Arrays.equals(photoBytes, photoData.photoBytes)
1020                    && (mIsPrimary == photoData.mIsPrimary));
1021        }
1022
1023        @Override
1024        public int hashCode() {
1025            if (mHashCode != null) {
1026                return mHashCode;
1027            }
1028
1029            int hash = mFormat != null ? mFormat.hashCode() : 0;
1030            hash = hash * 31;
1031            if (photoBytes != null) {
1032                for (byte b : photoBytes) {
1033                    hash += b;
1034                }
1035            }
1036
1037            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
1038            mHashCode = hash;
1039            return hash;
1040        }
1041
1042        @Override
1043        public String toString() {
1044            return String.format("format: %s: size: %d, isPrimary: %s", mFormat, photoBytes.length,
1045                    mIsPrimary);
1046        }
1047
1048        @Override
1049        public final EntryLabel getEntryLabel() {
1050            return EntryLabel.PHOTO;
1051        }
1052
1053        public String getFormat() {
1054            return mFormat;
1055        }
1056
1057        public byte[] getBytes() {
1058            return photoBytes;
1059        }
1060
1061        public boolean isPrimary() {
1062            return mIsPrimary;
1063        }
1064    }
1065
1066    public static class NicknameData implements EntryElement {
1067        private final String mNickname;
1068
1069        public NicknameData(String nickname) {
1070            mNickname = nickname;
1071        }
1072
1073        @Override
1074        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1075                int backReferenceIndex) {
1076            final ContentProviderOperation.Builder builder = ContentProviderOperation
1077                    .newInsert(Data.CONTENT_URI);
1078            builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex);
1079            builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
1080            builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1081            builder.withValue(Nickname.NAME, mNickname);
1082            operationList.add(builder.build());
1083        }
1084
1085        @Override
1086        public boolean isEmpty() {
1087            return TextUtils.isEmpty(mNickname);
1088        }
1089
1090        @Override
1091        public boolean equals(Object obj) {
1092            if (!(obj instanceof NicknameData)) {
1093                return false;
1094            }
1095            NicknameData nicknameData = (NicknameData) obj;
1096            return TextUtils.equals(mNickname, nicknameData.mNickname);
1097        }
1098
1099        @Override
1100        public int hashCode() {
1101            return mNickname != null ? mNickname.hashCode() : 0;
1102        }
1103
1104        @Override
1105        public String toString() {
1106            return "nickname: " + mNickname;
1107        }
1108
1109        @Override
1110        public EntryLabel getEntryLabel() {
1111            return EntryLabel.NICKNAME;
1112        }
1113
1114        public String getNickname() {
1115            return mNickname;
1116        }
1117    }
1118
1119    public static class NoteData implements EntryElement {
1120        public final String mNote;
1121
1122        public NoteData(String note) {
1123            mNote = note;
1124        }
1125
1126        @Override
1127        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1128                int backReferenceIndex) {
1129            final ContentProviderOperation.Builder builder = ContentProviderOperation
1130                    .newInsert(Data.CONTENT_URI);
1131            builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex);
1132            builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
1133            builder.withValue(Note.NOTE, mNote);
1134            operationList.add(builder.build());
1135        }
1136
1137        @Override
1138        public boolean isEmpty() {
1139            return TextUtils.isEmpty(mNote);
1140        }
1141
1142        @Override
1143        public boolean equals(Object obj) {
1144            if (this == obj) {
1145                return true;
1146            }
1147            if (!(obj instanceof NoteData)) {
1148                return false;
1149            }
1150            NoteData noteData = (NoteData) obj;
1151            return TextUtils.equals(mNote, noteData.mNote);
1152        }
1153
1154        @Override
1155        public int hashCode() {
1156            return mNote != null ? mNote.hashCode() : 0;
1157        }
1158
1159        @Override
1160        public String toString() {
1161            return "note: " + mNote;
1162        }
1163
1164        @Override
1165        public EntryLabel getEntryLabel() {
1166            return EntryLabel.NOTE;
1167        }
1168
1169        public String getNote() {
1170            return mNote;
1171        }
1172    }
1173
1174    public static class WebsiteData implements EntryElement {
1175        private final String mWebsite;
1176
1177        public WebsiteData(String website) {
1178            mWebsite = website;
1179        }
1180
1181        @Override
1182        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1183                int backReferenceIndex) {
1184            final ContentProviderOperation.Builder builder = ContentProviderOperation
1185                    .newInsert(Data.CONTENT_URI);
1186            builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex);
1187            builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
1188            builder.withValue(Website.URL, mWebsite);
1189            // There's no information about the type of URL in vCard.
1190            // We use TYPE_HOMEPAGE for safety.
1191            builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
1192            operationList.add(builder.build());
1193        }
1194
1195        @Override
1196        public boolean isEmpty() {
1197            return TextUtils.isEmpty(mWebsite);
1198        }
1199
1200        @Override
1201        public boolean equals(Object obj) {
1202            if (this == obj) {
1203                return true;
1204            }
1205            if (!(obj instanceof WebsiteData)) {
1206                return false;
1207            }
1208            WebsiteData websiteData = (WebsiteData) obj;
1209            return TextUtils.equals(mWebsite, websiteData.mWebsite);
1210        }
1211
1212        @Override
1213        public int hashCode() {
1214            return mWebsite != null ? mWebsite.hashCode() : 0;
1215        }
1216
1217        @Override
1218        public String toString() {
1219            return "website: " + mWebsite;
1220        }
1221
1222        @Override
1223        public EntryLabel getEntryLabel() {
1224            return EntryLabel.WEBSITE;
1225        }
1226
1227        public String getWebsite() {
1228            return mWebsite;
1229        }
1230    }
1231
1232    public static class BirthdayData implements EntryElement {
1233        private final String mBirthday;
1234
1235        public BirthdayData(String birthday) {
1236            mBirthday = birthday;
1237        }
1238
1239        @Override
1240        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1241                int backReferenceIndex) {
1242            final ContentProviderOperation.Builder builder = ContentProviderOperation
1243                    .newInsert(Data.CONTENT_URI);
1244            builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
1245            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1246            builder.withValue(Event.START_DATE, mBirthday);
1247            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1248            operationList.add(builder.build());
1249        }
1250
1251        @Override
1252        public boolean isEmpty() {
1253            return TextUtils.isEmpty(mBirthday);
1254        }
1255
1256        @Override
1257        public boolean equals(Object obj) {
1258            if (this == obj) {
1259                return true;
1260            }
1261            if (!(obj instanceof BirthdayData)) {
1262                return false;
1263            }
1264            BirthdayData birthdayData = (BirthdayData) obj;
1265            return TextUtils.equals(mBirthday, birthdayData.mBirthday);
1266        }
1267
1268        @Override
1269        public int hashCode() {
1270            return mBirthday != null ? mBirthday.hashCode() : 0;
1271        }
1272
1273        @Override
1274        public String toString() {
1275            return "birthday: " + mBirthday;
1276        }
1277
1278        @Override
1279        public EntryLabel getEntryLabel() {
1280            return EntryLabel.BIRTHDAY;
1281        }
1282
1283        public String getBirthday() {
1284            return mBirthday;
1285        }
1286    }
1287
1288    public static class AnniversaryData implements EntryElement {
1289        private final String mAnniversary;
1290
1291        public AnniversaryData(String anniversary) {
1292            mAnniversary = anniversary;
1293        }
1294
1295        @Override
1296        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1297                int backReferenceIndex) {
1298            final ContentProviderOperation.Builder builder = ContentProviderOperation
1299                    .newInsert(Data.CONTENT_URI);
1300            builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
1301            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1302            builder.withValue(Event.START_DATE, mAnniversary);
1303            builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
1304            operationList.add(builder.build());
1305        }
1306
1307        @Override
1308        public boolean isEmpty() {
1309            return TextUtils.isEmpty(mAnniversary);
1310        }
1311
1312        @Override
1313        public boolean equals(Object obj) {
1314            if (this == obj) {
1315                return true;
1316            }
1317            if (!(obj instanceof AnniversaryData)) {
1318                return false;
1319            }
1320            AnniversaryData anniversaryData = (AnniversaryData) obj;
1321            return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary);
1322        }
1323
1324        @Override
1325        public int hashCode() {
1326            return mAnniversary != null ? mAnniversary.hashCode() : 0;
1327        }
1328
1329        @Override
1330        public String toString() {
1331            return "anniversary: " + mAnniversary;
1332        }
1333
1334        @Override
1335        public EntryLabel getEntryLabel() {
1336            return EntryLabel.ANNIVERSARY;
1337        }
1338
1339        public String getAnniversary() { return mAnniversary; }
1340    }
1341
1342    public static class SipData implements EntryElement {
1343        /**
1344         * Note that schema part ("sip:") is automatically removed. e.g.
1345         * "sip:username:password@host:port" becomes
1346         * "username:password@host:port"
1347         */
1348        private final String mAddress;
1349        private final int mType;
1350        private final String mLabel;
1351        private final boolean mIsPrimary;
1352
1353        public SipData(String rawSip, int type, String label, boolean isPrimary) {
1354            if (rawSip.startsWith("sip:")) {
1355                mAddress = rawSip.substring(4);
1356            } else {
1357                mAddress = rawSip;
1358            }
1359            mType = type;
1360            mLabel = label;
1361            mIsPrimary = isPrimary;
1362        }
1363
1364        @Override
1365        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1366                int backReferenceIndex) {
1367            final ContentProviderOperation.Builder builder = ContentProviderOperation
1368                    .newInsert(Data.CONTENT_URI);
1369            builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex);
1370            builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
1371            builder.withValue(SipAddress.SIP_ADDRESS, mAddress);
1372            builder.withValue(SipAddress.TYPE, mType);
1373            if (mType == SipAddress.TYPE_CUSTOM) {
1374                builder.withValue(SipAddress.LABEL, mLabel);
1375            }
1376            if (mIsPrimary) {
1377                builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary);
1378            }
1379            operationList.add(builder.build());
1380        }
1381
1382        @Override
1383        public boolean isEmpty() {
1384            return TextUtils.isEmpty(mAddress);
1385        }
1386
1387        @Override
1388        public boolean equals(Object obj) {
1389            if (this == obj) {
1390                return true;
1391            }
1392            if (!(obj instanceof SipData)) {
1393                return false;
1394            }
1395            SipData sipData = (SipData) obj;
1396            return (mType == sipData.mType
1397                    && TextUtils.equals(mLabel, sipData.mLabel)
1398                    && TextUtils.equals(mAddress, sipData.mAddress)
1399                    && (mIsPrimary == sipData.mIsPrimary));
1400        }
1401
1402        @Override
1403        public int hashCode() {
1404            int hash = mType;
1405            hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
1406            hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
1407            hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
1408            return hash;
1409        }
1410
1411        @Override
1412        public String toString() {
1413            return "sip: " + mAddress;
1414        }
1415
1416        @Override
1417        public EntryLabel getEntryLabel() {
1418            return EntryLabel.SIP;
1419        }
1420
1421        /**
1422         * @return Address part of the sip data. The schema ("sip:") isn't contained here.
1423         */
1424        public String getAddress() { return mAddress; }
1425        public int getType() { return mType; }
1426        public String getLabel() { return mLabel; }
1427    }
1428
1429    /**
1430     * Some Contacts data in Android cannot be converted to vCard
1431     * representation. VCardEntry preserves those data using this class.
1432     */
1433    public static class AndroidCustomData implements EntryElement {
1434        private final String mMimeType;
1435
1436        private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN
1437
1438        public AndroidCustomData(String mimeType, List<String> dataList) {
1439            mMimeType = mimeType;
1440            mDataList = dataList;
1441        }
1442
1443        public static AndroidCustomData constructAndroidCustomData(List<String> list) {
1444            String mimeType;
1445            List<String> dataList;
1446
1447            if (list == null) {
1448                mimeType = null;
1449                dataList = null;
1450            } else if (list.size() < 2) {
1451                mimeType = list.get(0);
1452                dataList = null;
1453            } else {
1454                final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size()
1455                        : VCardConstants.MAX_DATA_COLUMN + 1;
1456                mimeType = list.get(0);
1457                dataList = list.subList(1, max);
1458            }
1459
1460            return new AndroidCustomData(mimeType, dataList);
1461        }
1462
1463        @Override
1464        public void constructInsertOperation(List<ContentProviderOperation> operationList,
1465                int backReferenceIndex) {
1466            final ContentProviderOperation.Builder builder = ContentProviderOperation
1467                    .newInsert(Data.CONTENT_URI);
1468            builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex);
1469            builder.withValue(Data.MIMETYPE, mMimeType);
1470            for (int i = 0; i < mDataList.size(); i++) {
1471                String value = mDataList.get(i);
1472                if (!TextUtils.isEmpty(value)) {
1473                    // 1-origin
1474                    builder.withValue("data" + (i + 1), value);
1475                }
1476            }
1477            operationList.add(builder.build());
1478        }
1479
1480        @Override
1481        public boolean isEmpty() {
1482            return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0;
1483        }
1484
1485        @Override
1486        public boolean equals(Object obj) {
1487            if (this == obj) {
1488                return true;
1489            }
1490            if (!(obj instanceof AndroidCustomData)) {
1491                return false;
1492            }
1493            AndroidCustomData data = (AndroidCustomData) obj;
1494            if (!TextUtils.equals(mMimeType, data.mMimeType)) {
1495                return false;
1496            }
1497            if (mDataList == null) {
1498                return data.mDataList == null;
1499            } else {
1500                final int size = mDataList.size();
1501                if (size != data.mDataList.size()) {
1502                    return false;
1503                }
1504                for (int i = 0; i < size; i++) {
1505                    if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) {
1506                        return false;
1507                    }
1508                }
1509                return true;
1510            }
1511        }
1512
1513        @Override
1514        public int hashCode() {
1515            int hash = mMimeType != null ? mMimeType.hashCode() : 0;
1516            if (mDataList != null) {
1517                for (String data : mDataList) {
1518                    hash = hash * 31 + (data != null ? data.hashCode() : 0);
1519                }
1520            }
1521            return hash;
1522        }
1523
1524        @Override
1525        public String toString() {
1526            final StringBuilder builder = new StringBuilder();
1527            builder.append("android-custom: " + mMimeType + ", data: ");
1528            builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray()));
1529            return builder.toString();
1530        }
1531
1532        @Override
1533        public EntryLabel getEntryLabel() {
1534            return EntryLabel.ANDROID_CUSTOM;
1535        }
1536
1537        public String getMimeType() { return mMimeType; }
1538        public List<String> getDataList() { return mDataList; }
1539    }
1540
1541    private final NameData mNameData = new NameData();
1542    private List<PhoneData> mPhoneList;
1543    private List<EmailData> mEmailList;
1544    private List<PostalData> mPostalList;
1545    private List<OrganizationData> mOrganizationList;
1546    private List<ImData> mImList;
1547    private List<PhotoData> mPhotoList;
1548    private List<WebsiteData> mWebsiteList;
1549    private List<SipData> mSipList;
1550    private List<NicknameData> mNicknameList;
1551    private List<NoteData> mNoteList;
1552    private List<AndroidCustomData> mAndroidCustomDataList;
1553    private BirthdayData mBirthday;
1554    private AnniversaryData mAnniversary;
1555
1556    /**
1557     * Inner iterator interface.
1558     */
1559    public interface EntryElementIterator {
1560        public void onIterationStarted();
1561
1562        public void onIterationEnded();
1563
1564        /**
1565         * Called when there are one or more {@link EntryElement} instances
1566         * associated with {@link EntryLabel}.
1567         */
1568        public void onElementGroupStarted(EntryLabel label);
1569
1570        /**
1571         * Called after all {@link EntryElement} instances for
1572         * {@link EntryLabel} provided on {@link #onElementGroupStarted(EntryLabel)}
1573         * being processed by {@link #onElement(EntryElement)}
1574         */
1575        public void onElementGroupEnded();
1576
1577        /**
1578         * @return should be true when child wants to continue the operation.
1579         *         False otherwise.
1580         */
1581        public boolean onElement(EntryElement elem);
1582    }
1583
1584    public final void iterateAllData(EntryElementIterator iterator) {
1585        iterator.onIterationStarted();
1586        iterator.onElementGroupStarted(mNameData.getEntryLabel());
1587        iterator.onElement(mNameData);
1588        iterator.onElementGroupEnded();
1589
1590        iterateOneList(mPhoneList, iterator);
1591        iterateOneList(mEmailList, iterator);
1592        iterateOneList(mPostalList, iterator);
1593        iterateOneList(mOrganizationList, iterator);
1594        iterateOneList(mImList, iterator);
1595        iterateOneList(mPhotoList, iterator);
1596        iterateOneList(mWebsiteList, iterator);
1597        iterateOneList(mSipList, iterator);
1598        iterateOneList(mNicknameList, iterator);
1599        iterateOneList(mNoteList, iterator);
1600        iterateOneList(mAndroidCustomDataList, iterator);
1601
1602        if (mBirthday != null) {
1603            iterator.onElementGroupStarted(mBirthday.getEntryLabel());
1604            iterator.onElement(mBirthday);
1605            iterator.onElementGroupEnded();
1606        }
1607        if (mAnniversary != null) {
1608            iterator.onElementGroupStarted(mAnniversary.getEntryLabel());
1609            iterator.onElement(mAnniversary);
1610            iterator.onElementGroupEnded();
1611        }
1612        iterator.onIterationEnded();
1613    }
1614
1615    private void iterateOneList(List<? extends EntryElement> elemList,
1616            EntryElementIterator iterator) {
1617        if (elemList != null && elemList.size() > 0) {
1618            iterator.onElementGroupStarted(elemList.get(0).getEntryLabel());
1619            for (EntryElement elem : elemList) {
1620                iterator.onElement(elem);
1621            }
1622            iterator.onElementGroupEnded();
1623        }
1624    }
1625
1626    private class IsIgnorableIterator implements EntryElementIterator {
1627        private boolean mEmpty = true;
1628
1629        @Override
1630        public void onIterationStarted() {
1631        }
1632
1633        @Override
1634        public void onIterationEnded() {
1635        }
1636
1637        @Override
1638        public void onElementGroupStarted(EntryLabel label) {
1639        }
1640
1641        @Override
1642        public void onElementGroupEnded() {
1643        }
1644
1645        @Override
1646        public boolean onElement(EntryElement elem) {
1647            if (!elem.isEmpty()) {
1648                mEmpty = false;
1649                // exit now
1650                return false;
1651            } else {
1652                return true;
1653            }
1654        }
1655
1656        public boolean getResult() {
1657            return mEmpty;
1658        }
1659    }
1660
1661    private class ToStringIterator implements EntryElementIterator {
1662        private StringBuilder mBuilder;
1663
1664        private boolean mFirstElement;
1665
1666        @Override
1667        public void onIterationStarted() {
1668            mBuilder = new StringBuilder();
1669            mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n");
1670        }
1671
1672        @Override
1673        public void onElementGroupStarted(EntryLabel label) {
1674            mBuilder.append(label.toString() + ": ");
1675            mFirstElement = true;
1676        }
1677
1678        @Override
1679        public boolean onElement(EntryElement elem) {
1680            if (!mFirstElement) {
1681                mBuilder.append(", ");
1682                mFirstElement = false;
1683            }
1684            mBuilder.append("[").append(elem.toString()).append("]");
1685            return true;
1686        }
1687
1688        @Override
1689        public void onElementGroupEnded() {
1690            mBuilder.append("\n");
1691        }
1692
1693        @Override
1694        public void onIterationEnded() {
1695            mBuilder.append("]]\n");
1696        }
1697
1698        @Override
1699        public String toString() {
1700            return mBuilder.toString();
1701        }
1702    }
1703
1704    private class InsertOperationConstrutor implements EntryElementIterator {
1705        private final List<ContentProviderOperation> mOperationList;
1706
1707        private final int mBackReferenceIndex;
1708
1709        public InsertOperationConstrutor(List<ContentProviderOperation> operationList,
1710                int backReferenceIndex) {
1711            mOperationList = operationList;
1712            mBackReferenceIndex = backReferenceIndex;
1713        }
1714
1715        @Override
1716        public void onIterationStarted() {
1717        }
1718
1719        @Override
1720        public void onIterationEnded() {
1721        }
1722
1723        @Override
1724        public void onElementGroupStarted(EntryLabel label) {
1725        }
1726
1727        @Override
1728        public void onElementGroupEnded() {
1729        }
1730
1731        @Override
1732        public boolean onElement(EntryElement elem) {
1733            if (!elem.isEmpty()) {
1734                elem.constructInsertOperation(mOperationList, mBackReferenceIndex);
1735            }
1736            return true;
1737        }
1738    }
1739
1740    private final int mVCardType;
1741    private final Account mAccount;
1742
1743    private List<VCardEntry> mChildren;
1744
1745    @Override
1746    public String toString() {
1747        ToStringIterator iterator = new ToStringIterator();
1748        iterateAllData(iterator);
1749        return iterator.toString();
1750    }
1751
1752    public VCardEntry() {
1753        this(VCardConfig.VCARD_TYPE_V21_GENERIC);
1754    }
1755
1756    public VCardEntry(int vcardType) {
1757        this(vcardType, null);
1758    }
1759
1760    public VCardEntry(int vcardType, Account account) {
1761        mVCardType = vcardType;
1762        mAccount = account;
1763    }
1764
1765    private void addPhone(int type, String data, String label, boolean isPrimary) {
1766        if (mPhoneList == null) {
1767            mPhoneList = new ArrayList<PhoneData>();
1768        }
1769        final StringBuilder builder = new StringBuilder();
1770        final String trimed = data.trim();
1771        final String formattedNumber;
1772        if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
1773            formattedNumber = trimed;
1774        } else {
1775            final int length = trimed.length();
1776            for (int i = 0; i < length; i++) {
1777                char ch = trimed.charAt(i);
1778                if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) {
1779                    builder.append(ch);
1780                }
1781            }
1782
1783            final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
1784            formattedNumber = PhoneNumberUtilsPort.formatNumber(builder.toString(), formattingType);
1785        }
1786        PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary);
1787        mPhoneList.add(phoneData);
1788    }
1789
1790    private void addSip(String sipData, int type, String label, boolean isPrimary) {
1791        if (mSipList == null) {
1792            mSipList = new ArrayList<SipData>();
1793        }
1794        mSipList.add(new SipData(sipData, type, label, isPrimary));
1795    }
1796
1797    private void addNickName(final String nickName) {
1798        if (mNicknameList == null) {
1799            mNicknameList = new ArrayList<NicknameData>();
1800        }
1801        mNicknameList.add(new NicknameData(nickName));
1802    }
1803
1804    private void addEmail(int type, String data, String label, boolean isPrimary) {
1805        if (mEmailList == null) {
1806            mEmailList = new ArrayList<EmailData>();
1807        }
1808        mEmailList.add(new EmailData(data, type, label, isPrimary));
1809    }
1810
1811    private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) {
1812        if (mPostalList == null) {
1813            mPostalList = new ArrayList<PostalData>(0);
1814        }
1815        mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary,
1816                mVCardType));
1817    }
1818
1819    /**
1820     * Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or
1821     * {@link #handleTitleValue(String)}.
1822     */
1823    private void addNewOrganization(final String organizationName, final String departmentName,
1824            final String titleName, final String phoneticName, int type, final boolean isPrimary) {
1825        if (mOrganizationList == null) {
1826            mOrganizationList = new ArrayList<OrganizationData>();
1827        }
1828        mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName,
1829                phoneticName, type, isPrimary));
1830    }
1831
1832    private static final List<String> sEmptyList = Collections
1833            .unmodifiableList(new ArrayList<String>(0));
1834
1835    private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
1836        final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
1837        if (sortAsCollection != null && sortAsCollection.size() != 0) {
1838            if (sortAsCollection.size() > 1) {
1839                Log.w(LOG_TAG,
1840                        "Incorrect multiple SORT_AS parameters detected: "
1841                                + Arrays.toString(sortAsCollection.toArray()));
1842            }
1843            final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
1844                    .iterator().next(), mVCardType);
1845            final StringBuilder builder = new StringBuilder();
1846            for (final String elem : sortNames) {
1847                builder.append(elem);
1848            }
1849            return builder.toString();
1850        } else {
1851            return null;
1852        }
1853    }
1854
1855    /**
1856     * Set "ORG" related values to the appropriate data. If there's more than
1857     * one {@link OrganizationData} objects, this input data are attached to the
1858     * last one which does not have valid values (not including empty but only
1859     * null). If there's no {@link OrganizationData} object, a new
1860     * {@link OrganizationData} is created, whose title is set to null.
1861     */
1862    private void handleOrgValue(final int type, List<String> orgList,
1863            Map<String, Collection<String>> paramMap, boolean isPrimary) {
1864        final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
1865        if (orgList == null) {
1866            orgList = sEmptyList;
1867        }
1868        final String organizationName;
1869        final String departmentName;
1870        final int size = orgList.size();
1871        switch (size) {
1872        case 0: {
1873            organizationName = "";
1874            departmentName = null;
1875            break;
1876        }
1877        case 1: {
1878            organizationName = orgList.get(0);
1879            departmentName = null;
1880            break;
1881        }
1882        default: { // More than 1.
1883            organizationName = orgList.get(0);
1884            // We're not sure which is the correct string for department.
1885            // In order to keep all the data, concatinate the rest of elements.
1886            StringBuilder builder = new StringBuilder();
1887            for (int i = 1; i < size; i++) {
1888                if (i > 1) {
1889                    builder.append(' ');
1890                }
1891                builder.append(orgList.get(i));
1892            }
1893            departmentName = builder.toString();
1894        }
1895        }
1896        if (mOrganizationList == null) {
1897            // Create new first organization entry, with "null" title which may be
1898            // added via handleTitleValue().
1899            addNewOrganization(organizationName, departmentName, null, phoneticName, type,
1900                    isPrimary);
1901            return;
1902        }
1903        for (OrganizationData organizationData : mOrganizationList) {
1904            // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
1905            // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
1906            if (organizationData.mOrganizationName == null
1907                    && organizationData.mDepartmentName == null) {
1908                // Probably the "TITLE" property comes before the "ORG" property via
1909                // handleTitleLine().
1910                organizationData.mOrganizationName = organizationName;
1911                organizationData.mDepartmentName = departmentName;
1912                organizationData.mIsPrimary = isPrimary;
1913                return;
1914            }
1915        }
1916        // No OrganizatioData is available. Create another one, with "null" title, which may be
1917        // added via handleTitleValue().
1918        addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
1919    }
1920
1921    /**
1922     * Set "title" value to the appropriate data. If there's more than one
1923     * OrganizationData objects, this input is attached to the last one which
1924     * does not have valid title value (not including empty but only null). If
1925     * there's no OrganizationData object, a new OrganizationData is created,
1926     * whose company name is set to null.
1927     */
1928    private void handleTitleValue(final String title) {
1929        if (mOrganizationList == null) {
1930            // Create new first organization entry, with "null" other info, which may be
1931            // added via handleOrgValue().
1932            addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
1933            return;
1934        }
1935        for (OrganizationData organizationData : mOrganizationList) {
1936            if (organizationData.mTitle == null) {
1937                organizationData.mTitle = title;
1938                return;
1939            }
1940        }
1941        // No Organization is available. Create another one, with "null" other info, which may be
1942        // added via handleOrgValue().
1943        addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
1944    }
1945
1946    private void addIm(int protocol, String customProtocol, String propValue, int type,
1947            boolean isPrimary) {
1948        if (mImList == null) {
1949            mImList = new ArrayList<ImData>();
1950        }
1951        mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary));
1952    }
1953
1954    private void addNote(final String note) {
1955        if (mNoteList == null) {
1956            mNoteList = new ArrayList<NoteData>(1);
1957        }
1958        mNoteList.add(new NoteData(note));
1959    }
1960
1961    private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
1962        if (mPhotoList == null) {
1963            mPhotoList = new ArrayList<PhotoData>(1);
1964        }
1965        final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary);
1966        mPhotoList.add(photoData);
1967    }
1968
1969    /**
1970     * Tries to extract paramMap, constructs SORT-AS parameter values, and store
1971     * them in appropriate phonetic name variables. This method does not care
1972     * the vCard version. Even when we have SORT-AS parameters in invalid
1973     * versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't
1974     * drop meaningful information. If we had this parameter in the N field of
1975     * vCard 3.0, and the contact data also have SORT-STRING, we will prefer
1976     * SORT-STRING, since it is regitimate property to be understood.
1977     */
1978    private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
1979        if (VCardConfig.isVersion30(mVCardType)
1980                && !(TextUtils.isEmpty(mNameData.mPhoneticFamily)
1981                        && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
1982                        .isEmpty(mNameData.mPhoneticGiven))) {
1983            return;
1984        }
1985
1986        final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
1987        if (sortAsCollection != null && sortAsCollection.size() != 0) {
1988            if (sortAsCollection.size() > 1) {
1989                Log.w(LOG_TAG,
1990                        "Incorrect multiple SORT_AS parameters detected: "
1991                                + Arrays.toString(sortAsCollection.toArray()));
1992            }
1993            final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
1994                    .iterator().next(), mVCardType);
1995            int size = sortNames.size();
1996            if (size > 3) {
1997                size = 3;
1998            }
1999            switch (size) {
2000            case 3:
2001                mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$
2002            case 2:
2003                mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$
2004            default:
2005                mNameData.mPhoneticFamily = sortNames.get(0);
2006                break;
2007            }
2008        }
2009    }
2010
2011    @SuppressWarnings("fallthrough")
2012    private void handleNProperty(final List<String> paramValues,
2013            Map<String, Collection<String>> paramMap) {
2014        // in vCard 4.0, SORT-AS parameter is available.
2015        tryHandleSortAsName(paramMap);
2016
2017        // Family, Given, Middle, Prefix, Suffix. (1 - 5)
2018        int size;
2019        if (paramValues == null || (size = paramValues.size()) < 1) {
2020            return;
2021        }
2022        if (size > 5) {
2023            size = 5;
2024        }
2025
2026        switch (size) {
2027        // Fall-through.
2028        case 5:
2029            mNameData.mSuffix = paramValues.get(4);
2030        case 4:
2031            mNameData.mPrefix = paramValues.get(3);
2032        case 3:
2033            mNameData.mMiddle = paramValues.get(2);
2034        case 2:
2035            mNameData.mGiven = paramValues.get(1);
2036        default:
2037            mNameData.mFamily = paramValues.get(0);
2038        }
2039    }
2040
2041    /**
2042     * Note: Some Japanese mobile phones use this field for phonetic name, since
2043     * vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the
2044     * field has some ';'s in it. Assume the ';' means the same meaning in N
2045     * property
2046     */
2047    @SuppressWarnings("fallthrough")
2048    private void handlePhoneticNameFromSound(List<String> elems) {
2049        if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily)
2050                && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
2051                .isEmpty(mNameData.mPhoneticGiven))) {
2052            // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
2053            // Ignore "SOUND;X-IRMC-N".
2054            return;
2055        }
2056
2057        int size;
2058        if (elems == null || (size = elems.size()) < 1) {
2059            return;
2060        }
2061
2062        // Assume that the order is "Family, Given, Middle".
2063        // This is not from specification but mere assumption. Some Japanese
2064        // phones use this order.
2065        if (size > 3) {
2066            size = 3;
2067        }
2068
2069        if (elems.get(0).length() > 0) {
2070            boolean onlyFirstElemIsNonEmpty = true;
2071            for (int i = 1; i < size; i++) {
2072                if (elems.get(i).length() > 0) {
2073                    onlyFirstElemIsNonEmpty = false;
2074                    break;
2075                }
2076            }
2077            if (onlyFirstElemIsNonEmpty) {
2078                final String[] namesArray = elems.get(0).split(" ");
2079                final int nameArrayLength = namesArray.length;
2080                if (nameArrayLength == 3) {
2081                    // Assume the string is "Family Middle Given".
2082                    mNameData.mPhoneticFamily = namesArray[0];
2083                    mNameData.mPhoneticMiddle = namesArray[1];
2084                    mNameData.mPhoneticGiven = namesArray[2];
2085                } else if (nameArrayLength == 2) {
2086                    // Assume the string is "Family Given" based on the Japanese mobile
2087                    // phones' preference.
2088                    mNameData.mPhoneticFamily = namesArray[0];
2089                    mNameData.mPhoneticGiven = namesArray[1];
2090                } else {
2091                    mNameData.mPhoneticGiven = elems.get(0);
2092                }
2093                return;
2094            }
2095        }
2096
2097        switch (size) {
2098        // fallthrough
2099        case 3:
2100            mNameData.mPhoneticMiddle = elems.get(2);
2101        case 2:
2102            mNameData.mPhoneticGiven = elems.get(1);
2103        default:
2104            mNameData.mPhoneticFamily = elems.get(0);
2105        }
2106    }
2107
2108    public void addProperty(final VCardProperty property) {
2109        final String propertyName = property.getName();
2110        final Map<String, Collection<String>> paramMap = property.getParameterMap();
2111        final List<String> propertyValueList = property.getValueList();
2112        byte[] propertyBytes = property.getByteValue();
2113
2114        if ((propertyValueList == null || propertyValueList.size() == 0)
2115                && propertyBytes == null) {
2116            return;
2117        }
2118        final String propValue = (propertyValueList != null
2119                ? listToString(propertyValueList).trim()
2120                : null);
2121
2122        if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) {
2123            // vCard version. Ignore this.
2124        } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) {
2125            mNameData.mFormatted = propValue;
2126        } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) {
2127            // Only in vCard 3.0. Use this if FN doesn't exist though it is
2128            // required in vCard 3.0.
2129            if (TextUtils.isEmpty(mNameData.mFormatted)) {
2130                mNameData.mFormatted = propValue;
2131            }
2132        } else if (propertyName.equals(VCardConstants.PROPERTY_N)) {
2133            handleNProperty(propertyValueList, paramMap);
2134        } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
2135            mNameData.mSortString = propValue;
2136        } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME)
2137                || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
2138            addNickName(propValue);
2139        } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) {
2140            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2141            if (typeCollection != null
2142                    && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
2143                // As of 2009-10-08, Parser side does not split a property value into separated
2144                // values using ';' (in other words, propValueList.size() == 1),
2145                // which is correct behavior from the view of vCard 2.1.
2146                // But we want it to be separated, so do the separation here.
2147                final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue,
2148                        mVCardType);
2149                handlePhoneticNameFromSound(phoneticNameList);
2150            } else {
2151                // Ignore this field since Android cannot understand what it is.
2152            }
2153        } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) {
2154            boolean valuesAreAllEmpty = true;
2155            for (String value : propertyValueList) {
2156                if (!TextUtils.isEmpty(value)) {
2157                    valuesAreAllEmpty = false;
2158                    break;
2159                }
2160            }
2161            if (valuesAreAllEmpty) {
2162                return;
2163            }
2164
2165            int type = -1;
2166            String label = null;
2167            boolean isPrimary = false;
2168            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2169            if (typeCollection != null) {
2170                for (final String typeStringOrg : typeCollection) {
2171                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
2172                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2173                        isPrimary = true;
2174                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2175                        type = StructuredPostal.TYPE_HOME;
2176                        label = null;
2177                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)
2178                            || typeStringUpperCase
2179                                    .equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
2180                        // "COMPANY" seems emitted by Windows Mobile, which is not
2181                        // specifically supported by vCard 2.1. We assume this is same
2182                        // as "WORK".
2183                        type = StructuredPostal.TYPE_WORK;
2184                        label = null;
2185                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL)
2186                            || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM)
2187                            || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
2188                        // We do not have any appropriate way to store this information.
2189                    } else if (type < 0) { // If no other type is specified before.
2190                        type = StructuredPostal.TYPE_CUSTOM;
2191                        if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2192                            label = typeStringOrg.substring(2);
2193                        } else {
2194                            label = typeStringOrg;
2195                        }
2196                    }
2197                }
2198            }
2199            // We use "HOME" as default
2200            if (type < 0) {
2201                type = StructuredPostal.TYPE_HOME;
2202            }
2203
2204            addPostal(type, propertyValueList, label, isPrimary);
2205        } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) {
2206            int type = -1;
2207            String label = null;
2208            boolean isPrimary = false;
2209            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2210            if (typeCollection != null) {
2211                for (final String typeStringOrg : typeCollection) {
2212                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
2213                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2214                        isPrimary = true;
2215                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2216                        type = Email.TYPE_HOME;
2217                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
2218                        type = Email.TYPE_WORK;
2219                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
2220                        type = Email.TYPE_MOBILE;
2221                    } else if (type < 0) { // If no other type is specified before
2222                        if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2223                            label = typeStringOrg.substring(2);
2224                        } else {
2225                            label = typeStringOrg;
2226                        }
2227                        type = Email.TYPE_CUSTOM;
2228                    }
2229                }
2230            }
2231            if (type < 0) {
2232                type = Email.TYPE_OTHER;
2233            }
2234            addEmail(type, propValue, label, isPrimary);
2235        } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) {
2236            // vCard specification does not specify other types.
2237            final int type = Organization.TYPE_WORK;
2238            boolean isPrimary = false;
2239            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2240            if (typeCollection != null) {
2241                for (String typeString : typeCollection) {
2242                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
2243                        isPrimary = true;
2244                    }
2245                }
2246            }
2247            handleOrgValue(type, propertyValueList, paramMap, isPrimary);
2248        } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) {
2249            handleTitleValue(propValue);
2250        } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) {
2251            // This conflicts with TITLE. Ignore for now...
2252            // handleTitleValue(propValue);
2253        } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO)
2254                || propertyName.equals(VCardConstants.PROPERTY_LOGO)) {
2255            Collection<String> paramMapValue = paramMap.get("VALUE");
2256            if (paramMapValue != null && paramMapValue.contains("URL")) {
2257                // Currently we do not have appropriate example for testing this case.
2258            } else {
2259                final Collection<String> typeCollection = paramMap.get("TYPE");
2260                String formatName = null;
2261                boolean isPrimary = false;
2262                if (typeCollection != null) {
2263                    for (String typeValue : typeCollection) {
2264                        if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
2265                            isPrimary = true;
2266                        } else if (formatName == null) {
2267                            formatName = typeValue;
2268                        }
2269                    }
2270                }
2271                addPhotoBytes(formatName, propertyBytes, isPrimary);
2272            }
2273        } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) {
2274            String phoneNumber = null;
2275            boolean isSip = false;
2276            if (VCardConfig.isVersion40(mVCardType)) {
2277                // Given propValue is in URI format, not in phone number format used until
2278                // vCard 3.0.
2279                if (propValue.startsWith("sip:")) {
2280                    isSip = true;
2281                } else if (propValue.startsWith("tel:")) {
2282                    phoneNumber = propValue.substring(4);
2283                } else {
2284                    // We don't know appropriate way to handle the other schemas. Also,
2285                    // we may still have non-URI phone number. To keep given data as much as
2286                    // we can, just save original value here.
2287                    phoneNumber = propValue;
2288                }
2289            } else {
2290                phoneNumber = propValue;
2291            }
2292
2293            if (isSip) {
2294                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2295                handleSipCase(propValue, typeCollection);
2296            } else {
2297                if (propValue.length() == 0) {
2298                    return;
2299                }
2300
2301                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2302                final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection,
2303                        phoneNumber);
2304                final int type;
2305                final String label;
2306                if (typeObject instanceof Integer) {
2307                    type = (Integer) typeObject;
2308                    label = null;
2309                } else {
2310                    type = Phone.TYPE_CUSTOM;
2311                    label = typeObject.toString();
2312                }
2313
2314                final boolean isPrimary;
2315                if (typeCollection != null &&
2316                        typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
2317                    isPrimary = true;
2318                } else {
2319                    isPrimary = false;
2320                }
2321
2322                addPhone(type, phoneNumber, label, isPrimary);
2323            }
2324        } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
2325            // The phone number available via Skype.
2326            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2327            final int type = Phone.TYPE_OTHER;
2328            final boolean isPrimary;
2329            if (typeCollection != null
2330                    && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
2331                isPrimary = true;
2332            } else {
2333                isPrimary = false;
2334            }
2335            addPhone(type, propValue, null, isPrimary);
2336        } else if (sImMap.containsKey(propertyName)) {
2337            final int protocol = sImMap.get(propertyName);
2338            boolean isPrimary = false;
2339            int type = -1;
2340            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2341            if (typeCollection != null) {
2342                for (String typeString : typeCollection) {
2343                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
2344                        isPrimary = true;
2345                    } else if (type < 0) {
2346                        if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
2347                            type = Im.TYPE_HOME;
2348                        } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
2349                            type = Im.TYPE_WORK;
2350                        }
2351                    }
2352                }
2353            }
2354            if (type < 0) {
2355                type = Im.TYPE_HOME;
2356            }
2357            addIm(protocol, null, propValue, type, isPrimary);
2358        } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) {
2359            addNote(propValue);
2360        } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) {
2361            if (mWebsiteList == null) {
2362                mWebsiteList = new ArrayList<WebsiteData>(1);
2363            }
2364            mWebsiteList.add(new WebsiteData(propValue));
2365        } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) {
2366            mBirthday = new BirthdayData(propValue);
2367        } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
2368            mAnniversary = new AnniversaryData(propValue);
2369        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
2370            mNameData.mPhoneticGiven = propValue;
2371        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
2372            mNameData.mPhoneticMiddle = propValue;
2373        } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
2374            mNameData.mPhoneticFamily = propValue;
2375        } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) {
2376            // See also RFC 4770 (for vCard 3.0)
2377            if (propValue.startsWith("sip:")) {
2378                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2379                handleSipCase(propValue, typeCollection);
2380            }
2381        } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) {
2382            if (!TextUtils.isEmpty(propValue)) {
2383                final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
2384                handleSipCase(propValue, typeCollection);
2385            }
2386        } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
2387            final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue,
2388                    mVCardType);
2389            handleAndroidCustomProperty(customPropertyList);
2390        } else {
2391        }
2392        // Be careful when adding some logic here, as some blocks above may use "return".
2393    }
2394
2395    /**
2396     * @param propValue may contain "sip:" at the beginning.
2397     * @param typeCollection
2398     */
2399    private void handleSipCase(String propValue, Collection<String> typeCollection) {
2400        if (TextUtils.isEmpty(propValue)) {
2401            return;
2402        }
2403        if (propValue.startsWith("sip:")) {
2404            propValue = propValue.substring(4);
2405            if (propValue.length() == 0) {
2406                return;
2407            }
2408        }
2409
2410        int type = -1;
2411        String label = null;
2412        boolean isPrimary = false;
2413        if (typeCollection != null) {
2414            for (final String typeStringOrg : typeCollection) {
2415                final String typeStringUpperCase = typeStringOrg.toUpperCase();
2416                if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
2417                    isPrimary = true;
2418                } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
2419                    type = SipAddress.TYPE_HOME;
2420                } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
2421                    type = SipAddress.TYPE_WORK;
2422                } else if (type < 0) { // If no other type is specified before
2423                    if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
2424                        label = typeStringOrg.substring(2);
2425                    } else {
2426                        label = typeStringOrg;
2427                    }
2428                    type = SipAddress.TYPE_CUSTOM;
2429                }
2430            }
2431        }
2432        if (type < 0) {
2433            type = SipAddress.TYPE_OTHER;
2434        }
2435        addSip(propValue, type, label, isPrimary);
2436    }
2437
2438    public void addChild(VCardEntry child) {
2439        if (mChildren == null) {
2440            mChildren = new ArrayList<VCardEntry>();
2441        }
2442        mChildren.add(child);
2443    }
2444
2445    private void handleAndroidCustomProperty(final List<String> customPropertyList) {
2446        if (mAndroidCustomDataList == null) {
2447            mAndroidCustomDataList = new ArrayList<AndroidCustomData>();
2448        }
2449        mAndroidCustomDataList
2450                .add(AndroidCustomData.constructAndroidCustomData(customPropertyList));
2451    }
2452
2453    /**
2454     * Construct the display name. The constructed data must not be null.
2455     */
2456    private String constructDisplayName() {
2457        String displayName = null;
2458        // FullName (created via "FN" or "NAME" field) is prefered.
2459        if (!TextUtils.isEmpty(mNameData.mFormatted)) {
2460            displayName = mNameData.mFormatted;
2461        } else if (!mNameData.emptyStructuredName()) {
2462            displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily,
2463                    mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix);
2464        } else if (!mNameData.emptyPhoneticStructuredName()) {
2465            displayName = VCardUtils.constructNameFromElements(mVCardType,
2466                    mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven);
2467        } else if (mEmailList != null && mEmailList.size() > 0) {
2468            displayName = mEmailList.get(0).mAddress;
2469        } else if (mPhoneList != null && mPhoneList.size() > 0) {
2470            displayName = mPhoneList.get(0).mNumber;
2471        } else if (mPostalList != null && mPostalList.size() > 0) {
2472            displayName = mPostalList.get(0).getFormattedAddress(mVCardType);
2473        } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
2474            displayName = mOrganizationList.get(0).getFormattedString();
2475        }
2476        if (displayName == null) {
2477            displayName = "";
2478        }
2479        return displayName;
2480    }
2481
2482    /**
2483     * Consolidate several fielsds (like mName) using name candidates,
2484     */
2485    public void consolidateFields() {
2486        mNameData.displayName = constructDisplayName();
2487    }
2488
2489    /**
2490     * @return true when this object has nothing meaningful for Android's
2491     *         Contacts, and thus is "ignorable" for Android's Contacts. This
2492     *         does not mean an original vCard is really empty. Even when the
2493     *         original vCard has some fields, this may ignore it if those
2494     *         fields cannot be transcoded into Android's Contacts
2495     *         representation.
2496     */
2497    public boolean isIgnorable() {
2498        IsIgnorableIterator iterator = new IsIgnorableIterator();
2499        iterateAllData(iterator);
2500        return iterator.getResult();
2501    }
2502
2503    /**
2504     * Constructs the list of insert operation for this object. When the
2505     * operationList argument is null, this method creates a new ArrayList and
2506     * return it. The returned object is filled with new insert operations for
2507     * this object. When operationList argument is not null, this method appends
2508     * those new operations into the object instead of creating a new ArrayList.
2509     *
2510     * @param resolver {@link ContentResolver} object to be used in this method.
2511     * @param operationList object to be filled. You can use this argument to
2512     *            concatinate operation lists. If null, this method creates a
2513     *            new array object.
2514     * @return If operationList argument is null, new object with new insert
2515     *         operations. If it is not null, the operationList object with
2516     *         operations inserted by this method.
2517     */
2518    public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver,
2519            ArrayList<ContentProviderOperation> operationList) {
2520        if (operationList == null) {
2521            operationList = new ArrayList<ContentProviderOperation>();
2522        }
2523
2524        if (isIgnorable()) {
2525            return operationList;
2526        }
2527
2528        final int backReferenceIndex = operationList.size();
2529
2530        // After applying the batch the first result's Uri is returned so it is important that
2531        // the RawContact is the first operation that gets inserted into the list.
2532        ContentProviderOperation.Builder builder = ContentProviderOperation
2533                .newInsert(RawContacts.CONTENT_URI);
2534        if (mAccount != null) {
2535            builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
2536            builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
2537        } else {
2538            builder.withValue(RawContacts.ACCOUNT_NAME, null);
2539            builder.withValue(RawContacts.ACCOUNT_TYPE, null);
2540        }
2541        operationList.add(builder.build());
2542
2543        int start = operationList.size();
2544        iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex));
2545        int end = operationList.size();
2546
2547        return operationList;
2548    }
2549
2550    public static VCardEntry buildFromResolver(ContentResolver resolver) {
2551        return buildFromResolver(resolver, Contacts.CONTENT_URI);
2552    }
2553
2554    public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
2555        return null;
2556    }
2557
2558    private String listToString(List<String> list) {
2559        final int size = list.size();
2560        if (size > 1) {
2561            StringBuilder builder = new StringBuilder();
2562            int i = 0;
2563            for (String type : list) {
2564                builder.append(type);
2565                if (i < size - 1) {
2566                    builder.append(";");
2567                }
2568            }
2569            return builder.toString();
2570        } else if (size == 1) {
2571            return list.get(0);
2572        } else {
2573            return "";
2574        }
2575    }
2576
2577    public final NameData getNameData() {
2578        return mNameData;
2579    }
2580
2581    public final List<NicknameData> getNickNameList() {
2582        return mNicknameList;
2583    }
2584
2585    public final String getBirthday() {
2586        return mBirthday != null ? mBirthday.mBirthday : null;
2587    }
2588
2589    public final List<NoteData> getNotes() {
2590        return mNoteList;
2591    }
2592
2593    public final List<PhoneData> getPhoneList() {
2594        return mPhoneList;
2595    }
2596
2597    public final List<EmailData> getEmailList() {
2598        return mEmailList;
2599    }
2600
2601    public final List<PostalData> getPostalList() {
2602        return mPostalList;
2603    }
2604
2605    public final List<OrganizationData> getOrganizationList() {
2606        return mOrganizationList;
2607    }
2608
2609    public final List<ImData> getImList() {
2610        return mImList;
2611    }
2612
2613    public final List<PhotoData> getPhotoList() {
2614        return mPhotoList;
2615    }
2616
2617    public final List<WebsiteData> getWebsiteList() {
2618        return mWebsiteList;
2619    }
2620
2621    /**
2622     * @hide this interface may be changed for better support of vCard 4.0 (UID)
2623     */
2624    public final List<VCardEntry> getChildlen() {
2625        return mChildren;
2626    }
2627
2628    public String getDisplayName() {
2629        if (mNameData.displayName == null) {
2630            mNameData.displayName = constructDisplayName();
2631        }
2632        return mNameData.displayName;
2633    }
2634}
2635