VCardEntry.java revision c13101ba38c929d91dcb68e21165c82c19af35d7
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 */
16package com.android.vcard;
17
18import android.accounts.Account;
19import android.content.ContentProviderOperation;
20import android.content.ContentProviderResult;
21import android.content.ContentResolver;
22import android.content.OperationApplicationException;
23import android.net.Uri;
24import android.os.RemoteException;
25import android.provider.ContactsContract;
26import android.provider.ContactsContract.Contacts;
27import android.provider.ContactsContract.Data;
28import android.provider.ContactsContract.RawContacts;
29import android.provider.ContactsContract.CommonDataKinds.Email;
30import android.provider.ContactsContract.CommonDataKinds.Event;
31import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
32import android.provider.ContactsContract.CommonDataKinds.Im;
33import android.provider.ContactsContract.CommonDataKinds.Nickname;
34import android.provider.ContactsContract.CommonDataKinds.Note;
35import android.provider.ContactsContract.CommonDataKinds.Organization;
36import android.provider.ContactsContract.CommonDataKinds.Phone;
37import android.provider.ContactsContract.CommonDataKinds.Photo;
38import android.provider.ContactsContract.CommonDataKinds.StructuredName;
39import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
40import android.provider.ContactsContract.CommonDataKinds.Website;
41import android.telephony.PhoneNumberUtils;
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.HashSet;
51import java.util.List;
52import java.util.Map;
53
54/**
55 * This class bridges between data structure of Contact app and VCard data.
56 */
57public class VCardEntry {
58    private static final String LOG_TAG = "VCardEntry";
59
60    private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
61
62    private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
63
64    static {
65        sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
66        sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
67        sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
68        sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
69        sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
70        sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
71        sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
72        sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
73                Im.PROTOCOL_GOOGLE_TALK);
74    }
75
76    public static class PhoneData {
77        public final int type;
78        public final String data;
79        public final String label;
80        // isPrimary is (not final but) changable, only when there's no appropriate one existing
81        // in the original VCard.
82        public boolean isPrimary;
83        public PhoneData(int type, String data, String label, boolean isPrimary) {
84            this.type = type;
85            this.data = data;
86            this.label = label;
87            this.isPrimary = isPrimary;
88        }
89
90        @Override
91        public boolean equals(Object obj) {
92            if (!(obj instanceof PhoneData)) {
93                return false;
94            }
95            PhoneData phoneData = (PhoneData)obj;
96            return (type == phoneData.type && data.equals(phoneData.data) &&
97                    label.equals(phoneData.label) && isPrimary == phoneData.isPrimary);
98        }
99
100        @Override
101        public String toString() {
102            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
103                    type, data, label, isPrimary);
104        }
105    }
106
107    public static class EmailData {
108        public final int type;
109        public final String data;
110        // Used only when TYPE is TYPE_CUSTOM.
111        public final String label;
112        public boolean isPrimary;
113        public EmailData(int type, String data, String label, boolean isPrimary) {
114            this.type = type;
115            this.data = data;
116            this.label = label;
117            this.isPrimary = isPrimary;
118        }
119
120        @Override
121        public boolean equals(Object obj) {
122            if (!(obj instanceof EmailData)) {
123                return false;
124            }
125            EmailData emailData = (EmailData)obj;
126            return (type == emailData.type && data.equals(emailData.data) &&
127                    label.equals(emailData.label) && isPrimary == emailData.isPrimary);
128        }
129
130        @Override
131        public String toString() {
132            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
133                    type, data, label, isPrimary);
134        }
135    }
136
137    public static class PostalData {
138        // Determined by vCard specification.
139        // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
140        public static final int ADDR_MAX_DATA_SIZE = 7;
141        private final String[] dataArray;
142        public final String pobox;
143        public final String extendedAddress;
144        public final String street;
145        public final String localty;
146        public final String region;
147        public final String postalCode;
148        public final String country;
149        public final int type;
150        public final String label;
151        public boolean isPrimary;
152
153        public PostalData(final int type, final List<String> propValueList,
154                final String label, boolean isPrimary) {
155            this.type = type;
156            dataArray = new String[ADDR_MAX_DATA_SIZE];
157
158            int size = propValueList.size();
159            if (size > ADDR_MAX_DATA_SIZE) {
160                size = ADDR_MAX_DATA_SIZE;
161            }
162
163            // adr-value = 0*6(text-value ";") text-value
164            //           ; PO Box, Extended Address, Street, Locality, Region, Postal
165            //           ; Code, Country Name
166            //
167            // Use Iterator assuming List may be LinkedList, though actually it is
168            // always ArrayList in the current implementation.
169            int i = 0;
170            for (String addressElement : propValueList) {
171                dataArray[i] = addressElement;
172                if (++i >= size) {
173                    break;
174                }
175            }
176            while (i < ADDR_MAX_DATA_SIZE) {
177                dataArray[i++] = null;
178            }
179
180            this.pobox = dataArray[0];
181            this.extendedAddress = dataArray[1];
182            this.street = dataArray[2];
183            this.localty = dataArray[3];
184            this.region = dataArray[4];
185            this.postalCode = dataArray[5];
186            this.country = dataArray[6];
187            this.label = label;
188            this.isPrimary = isPrimary;
189        }
190
191        @Override
192        public boolean equals(Object obj) {
193            if (!(obj instanceof PostalData)) {
194                return false;
195            }
196            final PostalData postalData = (PostalData)obj;
197            return (Arrays.equals(dataArray, postalData.dataArray) &&
198                    (type == postalData.type &&
199                            (type == StructuredPostal.TYPE_CUSTOM ?
200                                    (label == postalData.label) : true)) &&
201                    (isPrimary == postalData.isPrimary));
202        }
203
204        public String getFormattedAddress(final int vcardType) {
205            StringBuilder builder = new StringBuilder();
206            boolean empty = true;
207            if (VCardConfig.isJapaneseDevice(vcardType)) {
208                // In Japan, the order is reversed.
209                for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
210                    String addressPart = dataArray[i];
211                    if (!TextUtils.isEmpty(addressPart)) {
212                        if (!empty) {
213                            builder.append(' ');
214                        } else {
215                            empty = false;
216                        }
217                        builder.append(addressPart);
218                    }
219                }
220            } else {
221                for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
222                    String addressPart = dataArray[i];
223                    if (!TextUtils.isEmpty(addressPart)) {
224                        if (!empty) {
225                            builder.append(' ');
226                        } else {
227                            empty = false;
228                        }
229                        builder.append(addressPart);
230                    }
231                }
232            }
233
234            return builder.toString().trim();
235        }
236
237        @Override
238        public String toString() {
239            return String.format("type: %d, label: %s, isPrimary: %s",
240                    type, label, isPrimary);
241        }
242    }
243
244    public static class OrganizationData {
245        public final int type;
246        // non-final is Intentional: we may change the values since this info is separated into
247        // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in
248        // different timing.
249        public String companyName;
250        public String departmentName;
251        public String titleName;
252        public boolean isPrimary;
253
254        public OrganizationData(int type,
255                String companyName,
256                String departmentName,
257                String titleName,
258                boolean isPrimary) {
259            this.type = type;
260            this.companyName = companyName;
261            this.departmentName = departmentName;
262            this.titleName = titleName;
263            this.isPrimary = isPrimary;
264        }
265
266        @Override
267        public boolean equals(Object obj) {
268            if (!(obj instanceof OrganizationData)) {
269                return false;
270            }
271            OrganizationData organization = (OrganizationData)obj;
272            return (type == organization.type &&
273                    TextUtils.equals(companyName, organization.companyName) &&
274                    TextUtils.equals(departmentName, organization.departmentName) &&
275                    TextUtils.equals(titleName, organization.titleName) &&
276                    isPrimary == organization.isPrimary);
277        }
278
279        public String getFormattedString() {
280            final StringBuilder builder = new StringBuilder();
281            if (!TextUtils.isEmpty(companyName)) {
282                builder.append(companyName);
283            }
284
285            if (!TextUtils.isEmpty(departmentName)) {
286                if (builder.length() > 0) {
287                    builder.append(", ");
288                }
289                builder.append(departmentName);
290            }
291
292            if (!TextUtils.isEmpty(titleName)) {
293                if (builder.length() > 0) {
294                    builder.append(", ");
295                }
296                builder.append(titleName);
297            }
298
299            return builder.toString();
300        }
301
302        @Override
303        public String toString() {
304            return String.format(
305                    "type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
306                    type, companyName, departmentName, titleName, isPrimary);
307        }
308    }
309
310    public static class ImData {
311        public final int protocol;
312        public final String customProtocol;
313        public final int type;
314        public final String data;
315        public final boolean isPrimary;
316
317        public ImData(final int protocol, final String customProtocol, final int type,
318                final String data, final boolean isPrimary) {
319            this.protocol = protocol;
320            this.customProtocol = customProtocol;
321            this.type = type;
322            this.data = data;
323            this.isPrimary = isPrimary;
324        }
325
326        @Override
327        public boolean equals(Object obj) {
328            if (!(obj instanceof ImData)) {
329                return false;
330            }
331            ImData imData = (ImData)obj;
332            return (type == imData.type && protocol == imData.protocol
333                    && (customProtocol != null ? customProtocol.equals(imData.customProtocol) :
334                        (imData.customProtocol == null))
335                    && (data != null ? data.equals(imData.data) : (imData.data == null))
336                    && isPrimary == imData.isPrimary);
337        }
338
339        @Override
340        public String toString() {
341            return String.format(
342                    "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s",
343                    type, protocol, customProtocol, data, isPrimary);
344        }
345    }
346
347    public static class PhotoData {
348        public static final String FORMAT_FLASH = "SWF";
349        public final int type;
350        public final String formatName;  // used when type is not defined in ContactsContract.
351        public final byte[] photoBytes;
352        public final boolean isPrimary;
353
354        public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
355            this.type = type;
356            this.formatName = formatName;
357            this.photoBytes = photoBytes;
358            this.isPrimary = isPrimary;
359        }
360
361        @Override
362        public boolean equals(Object obj) {
363            if (!(obj instanceof PhotoData)) {
364                return false;
365            }
366            PhotoData photoData = (PhotoData)obj;
367            return (type == photoData.type &&
368                    (formatName == null ? (photoData.formatName == null) :
369                            formatName.equals(photoData.formatName)) &&
370                    (Arrays.equals(photoBytes, photoData.photoBytes)) &&
371                    (isPrimary == photoData.isPrimary));
372        }
373
374        @Override
375        public String toString() {
376            return String.format("type: %d, format: %s: size: %d, isPrimary: %s",
377                    type, formatName, photoBytes.length, isPrimary);
378        }
379    }
380
381    /* package */ static class Property {
382        private String mPropertyName;
383        private Map<String, Collection<String>> mParameterMap =
384            new HashMap<String, Collection<String>>();
385        private List<String> mPropertyValueList = new ArrayList<String>();
386        private byte[] mPropertyBytes;
387
388        public void setPropertyName(final String propertyName) {
389            mPropertyName = propertyName;
390        }
391
392        public void addParameter(final String paramName, final String paramValue) {
393            Collection<String> values;
394            if (!mParameterMap.containsKey(paramName)) {
395                if (paramName.equals("TYPE")) {
396                    values = new HashSet<String>();
397                } else {
398                    values = new ArrayList<String>();
399                }
400                mParameterMap.put(paramName, values);
401            } else {
402                values = mParameterMap.get(paramName);
403            }
404            values.add(paramValue);
405        }
406
407        public void addToPropertyValueList(final String propertyValue) {
408            mPropertyValueList.add(propertyValue);
409        }
410
411        public void setPropertyBytes(final byte[] propertyBytes) {
412            mPropertyBytes = propertyBytes;
413        }
414
415        public final Collection<String> getParameters(String type) {
416            return mParameterMap.get(type);
417        }
418
419        public final List<String> getPropertyValueList() {
420            return mPropertyValueList;
421        }
422
423        public void clear() {
424            mPropertyName = null;
425            mParameterMap.clear();
426            mPropertyValueList.clear();
427            mPropertyBytes = null;
428        }
429    }
430
431    private String mFamilyName;
432    private String mGivenName;
433    private String mMiddleName;
434    private String mPrefix;
435    private String mSuffix;
436
437    // Used only when no family nor given name is found.
438    private String mFormattedName;
439
440    private String mPhoneticFamilyName;
441    private String mPhoneticGivenName;
442    private String mPhoneticMiddleName;
443
444    private String mPhoneticFullName;
445
446    private List<String> mNickNameList;
447
448    private String mDisplayName;
449
450    private String mBirthday;
451    private String mAnniversary;
452
453    private List<String> mNoteList;
454    private List<PhoneData> mPhoneList;
455    private List<EmailData> mEmailList;
456    private List<PostalData> mPostalList;
457    private List<OrganizationData> mOrganizationList;
458    private List<ImData> mImList;
459    private List<PhotoData> mPhotoList;
460    private List<String> mWebsiteList;
461    private List<List<String>> mAndroidCustomPropertyList;
462
463    private final int mVCardType;
464    private final Account mAccount;
465
466    public VCardEntry() {
467        this(VCardConfig.VCARD_TYPE_V21_GENERIC);
468    }
469
470    public VCardEntry(int vcardType) {
471        this(vcardType, null);
472    }
473
474    public VCardEntry(int vcardType, Account account) {
475        mVCardType = vcardType;
476        mAccount = account;
477    }
478
479    private void addPhone(int type, String data, String label, boolean isPrimary) {
480        if (mPhoneList == null) {
481            mPhoneList = new ArrayList<PhoneData>();
482        }
483        final StringBuilder builder = new StringBuilder();
484        final String trimed = data.trim();
485        final String formattedNumber;
486        if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
487            formattedNumber = trimed;
488        } else {
489            final int length = trimed.length();
490            for (int i = 0; i < length; i++) {
491                char ch = trimed.charAt(i);
492                if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) {
493                    builder.append(ch);
494                }
495            }
496
497            final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
498            formattedNumber = PhoneNumberUtils.formatNumber(builder.toString(), formattingType);
499        }
500        PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary);
501        mPhoneList.add(phoneData);
502    }
503
504    private void addNickName(final String nickName) {
505        if (mNickNameList == null) {
506            mNickNameList = new ArrayList<String>();
507        }
508        mNickNameList.add(nickName);
509    }
510
511    private void addEmail(int type, String data, String label, boolean isPrimary){
512        if (mEmailList == null) {
513            mEmailList = new ArrayList<EmailData>();
514        }
515        mEmailList.add(new EmailData(type, data, label, isPrimary));
516    }
517
518    private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){
519        if (mPostalList == null) {
520            mPostalList = new ArrayList<PostalData>(0);
521        }
522        mPostalList.add(new PostalData(type, propValueList, label, isPrimary));
523    }
524
525    /**
526     * Should be called via {@link #handleOrgValue(int, List, boolean)} or
527     * {@link #handleTitleValue(String)}.
528     */
529    private void addNewOrganization(int type, final String companyName,
530            final String departmentName,
531            final String titleName, boolean isPrimary) {
532        if (mOrganizationList == null) {
533            mOrganizationList = new ArrayList<OrganizationData>();
534        }
535        mOrganizationList.add(new OrganizationData(type, companyName,
536                departmentName, titleName, isPrimary));
537    }
538
539    private static final List<String> sEmptyList =
540            Collections.unmodifiableList(new ArrayList<String>(0));
541
542    /**
543     * Set "ORG" related values to the appropriate data. If there's more than one
544     * {@link OrganizationData} objects, this input data are attached to the last one which
545     * does not have valid values (not including empty but only null). If there's no
546     * {@link OrganizationData} object, a new {@link OrganizationData} is created,
547     * whose title is set to null.
548     */
549    private void handleOrgValue(final int type, List<String> orgList, boolean isPrimary) {
550        if (orgList == null) {
551            orgList = sEmptyList;
552        }
553        final String companyName;
554        final String departmentName;
555        final int size = orgList.size();
556        switch (size) {
557            case 0: {
558                companyName = "";
559                departmentName = null;
560                break;
561            }
562            case 1: {
563                companyName = orgList.get(0);
564                departmentName = null;
565                break;
566            }
567            default: {  // More than 1.
568                companyName = orgList.get(0);
569                // We're not sure which is the correct string for department.
570                // In order to keep all the data, concatinate the rest of elements.
571                StringBuilder builder = new StringBuilder();
572                for (int i = 1; i < size; i++) {
573                    if (i > 1) {
574                        builder.append(' ');
575                    }
576                    builder.append(orgList.get(i));
577                }
578                departmentName = builder.toString();
579            }
580        }
581        if (mOrganizationList == null) {
582            // Create new first organization entry, with "null" title which may be
583            // added via handleTitleValue().
584            addNewOrganization(type, companyName, departmentName, null, isPrimary);
585            return;
586        }
587        for (OrganizationData organizationData : mOrganizationList) {
588            // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
589            // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
590            if (organizationData.companyName == null &&
591                    organizationData.departmentName == null) {
592                // Probably the "TITLE" property comes before the "ORG" property via
593                // handleTitleLine().
594                organizationData.companyName = companyName;
595                organizationData.departmentName = departmentName;
596                organizationData.isPrimary = isPrimary;
597                return;
598            }
599        }
600        // No OrganizatioData is available. Create another one, with "null" title, which may be
601        // added via handleTitleValue().
602        addNewOrganization(type, companyName, departmentName, null, isPrimary);
603    }
604
605    /**
606     * Set "title" value to the appropriate data. If there's more than one
607     * OrganizationData objects, this input is attached to the last one which does not
608     * have valid title value (not including empty but only null). If there's no
609     * OrganizationData object, a new OrganizationData is created, whose company name is
610     * set to null.
611     */
612    private void handleTitleValue(final String title) {
613        if (mOrganizationList == null) {
614            // Create new first organization entry, with "null" other info, which may be
615            // added via handleOrgValue().
616            addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
617            return;
618        }
619        for (OrganizationData organizationData : mOrganizationList) {
620            if (organizationData.titleName == null) {
621                organizationData.titleName = title;
622                return;
623            }
624        }
625        // No Organization is available. Create another one, with "null" other info, which may be
626        // added via handleOrgValue().
627        addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, false);
628    }
629
630    private void addIm(int protocol, String customProtocol, int type,
631            String propValue, boolean isPrimary) {
632        if (mImList == null) {
633            mImList = new ArrayList<ImData>();
634        }
635        mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary));
636    }
637
638    private void addNote(final String note) {
639        if (mNoteList == null) {
640            mNoteList = new ArrayList<String>(1);
641        }
642        mNoteList.add(note);
643    }
644
645    private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
646        if (mPhotoList == null) {
647            mPhotoList = new ArrayList<PhotoData>(1);
648        }
649        final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary);
650        mPhotoList.add(photoData);
651    }
652
653    @SuppressWarnings("fallthrough")
654    private void handleNProperty(List<String> elems) {
655        // Family, Given, Middle, Prefix, Suffix. (1 - 5)
656        int size;
657        if (elems == null || (size = elems.size()) < 1) {
658            return;
659        }
660        if (size > 5) {
661            size = 5;
662        }
663
664        switch (size) {
665            // fallthrough
666            case 5: mSuffix = elems.get(4);
667            case 4: mPrefix = elems.get(3);
668            case 3: mMiddleName = elems.get(2);
669            case 2: mGivenName = elems.get(1);
670            default: mFamilyName = elems.get(0);
671        }
672    }
673
674    /**
675     * Note: Some Japanese mobile phones use this field for phonetic name,
676     *       since vCard 2.1 does not have "SORT-STRING" type.
677     *       Also, in some cases, the field has some ';'s in it.
678     *       Assume the ';' means the same meaning in N property
679     */
680    @SuppressWarnings("fallthrough")
681    private void handlePhoneticNameFromSound(List<String> elems) {
682        if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
683                TextUtils.isEmpty(mPhoneticMiddleName) &&
684                TextUtils.isEmpty(mPhoneticGivenName))) {
685            // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
686            // Ignore "SOUND;X-IRMC-N".
687            return;
688        }
689
690        int size;
691        if (elems == null || (size = elems.size()) < 1) {
692            return;
693        }
694
695        // Assume that the order is "Family, Given, Middle".
696        // This is not from specification but mere assumption. Some Japanese phones use this order.
697        if (size > 3) {
698            size = 3;
699        }
700
701        if (elems.get(0).length() > 0) {
702            boolean onlyFirstElemIsNonEmpty = true;
703            for (int i = 1; i < size; i++) {
704                if (elems.get(i).length() > 0) {
705                    onlyFirstElemIsNonEmpty = false;
706                    break;
707                }
708            }
709            if (onlyFirstElemIsNonEmpty) {
710                final String[] namesArray = elems.get(0).split(" ");
711                final int nameArrayLength = namesArray.length;
712                if (nameArrayLength == 3) {
713                    // Assume the string is "Family Middle Given".
714                    mPhoneticFamilyName = namesArray[0];
715                    mPhoneticMiddleName = namesArray[1];
716                    mPhoneticGivenName = namesArray[2];
717                } else if (nameArrayLength == 2) {
718                    // Assume the string is "Family Given" based on the Japanese mobile
719                    // phones' preference.
720                    mPhoneticFamilyName = namesArray[0];
721                    mPhoneticGivenName = namesArray[1];
722                } else {
723                    mPhoneticFullName = elems.get(0);
724                }
725                return;
726            }
727        }
728
729        switch (size) {
730            // fallthrough
731            case 3: mPhoneticMiddleName = elems.get(2);
732            case 2: mPhoneticGivenName = elems.get(1);
733            default: mPhoneticFamilyName = elems.get(0);
734        }
735    }
736
737    public void addProperty(final Property property) {
738        final String propName = property.mPropertyName;
739        final Map<String, Collection<String>> paramMap = property.mParameterMap;
740        final List<String> propValueList = property.mPropertyValueList;
741        byte[] propBytes = property.mPropertyBytes;
742
743        if (propValueList.size() == 0) {
744            return;
745        }
746        final String propValue = listToString(propValueList).trim();
747
748        if (propName.equals(VCardConstants.PROPERTY_VERSION)) {
749            // vCard version. Ignore this.
750        } else if (propName.equals(VCardConstants.PROPERTY_FN)) {
751            mFormattedName = propValue;
752        } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFormattedName == null) {
753            // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not
754            // actually exist in the real vCard data, does not exist.
755            mFormattedName = propValue;
756        } else if (propName.equals(VCardConstants.PROPERTY_N)) {
757            handleNProperty(propValueList);
758        } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
759            mPhoneticFullName = propValue;
760        } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) ||
761                propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
762            addNickName(propValue);
763        } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) {
764            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
765            if (typeCollection != null
766                    && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
767                // As of 2009-10-08, Parser side does not split a property value into separated
768                // values using ';' (in other words, propValueList.size() == 1),
769                // which is correct behavior from the view of vCard 2.1.
770                // But we want it to be separated, so do the separation here.
771                final List<String> phoneticNameList =
772                        VCardUtils.constructListFromValue(propValue,
773                                VCardConfig.isVersion30(mVCardType));
774                handlePhoneticNameFromSound(phoneticNameList);
775            } else {
776                // Ignore this field since Android cannot understand what it is.
777            }
778        } else if (propName.equals(VCardConstants.PROPERTY_ADR)) {
779            boolean valuesAreAllEmpty = true;
780            for (String value : propValueList) {
781                if (value.length() > 0) {
782                    valuesAreAllEmpty = false;
783                    break;
784                }
785            }
786            if (valuesAreAllEmpty) {
787                return;
788            }
789
790            int type = -1;
791            String label = "";
792            boolean isPrimary = false;
793            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
794            if (typeCollection != null) {
795                for (String typeString : typeCollection) {
796                    typeString = typeString.toUpperCase();
797                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
798                        isPrimary = true;
799                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
800                        type = StructuredPostal.TYPE_HOME;
801                        label = "";
802                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK) ||
803                            typeString.equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
804                        // "COMPANY" seems emitted by Windows Mobile, which is not
805                        // specifically supported by vCard 2.1. We assume this is same
806                        // as "WORK".
807                        type = StructuredPostal.TYPE_WORK;
808                        label = "";
809                    } else if (typeString.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) ||
810                            typeString.equals(VCardConstants.PARAM_ADR_TYPE_DOM) ||
811                            typeString.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
812                        // We do not have any appropriate way to store this information.
813                    } else {
814                        if (typeString.startsWith("X-") && type < 0) {
815                            typeString = typeString.substring(2);
816                        }
817                        // vCard 3.0 allows iana-token. Also some vCard 2.1 exporters
818                        // emit non-standard types. We do not handle their values now.
819                        type = StructuredPostal.TYPE_CUSTOM;
820                        label = typeString;
821                    }
822                }
823            }
824            // We use "HOME" as default
825            if (type < 0) {
826                type = StructuredPostal.TYPE_HOME;
827            }
828
829            addPostal(type, propValueList, label, isPrimary);
830        } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) {
831            int type = -1;
832            String label = null;
833            boolean isPrimary = false;
834            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
835            if (typeCollection != null) {
836                for (String typeString : typeCollection) {
837                    typeString = typeString.toUpperCase();
838                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
839                        isPrimary = true;
840                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_HOME)) {
841                        type = Email.TYPE_HOME;
842                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_WORK)) {
843                        type = Email.TYPE_WORK;
844                    } else if (typeString.equals(VCardConstants.PARAM_TYPE_CELL)) {
845                        type = Email.TYPE_MOBILE;
846                    } else {
847                        if (typeString.startsWith("X-") && type < 0) {
848                            typeString = typeString.substring(2);
849                        }
850                        // vCard 3.0 allows iana-token.
851                        // We may have INTERNET (specified in vCard spec),
852                        // SCHOOL, etc.
853                        type = Email.TYPE_CUSTOM;
854                        label = typeString;
855                    }
856                }
857            }
858            if (type < 0) {
859                type = Email.TYPE_OTHER;
860            }
861            addEmail(type, propValue, label, isPrimary);
862        } else if (propName.equals(VCardConstants.PROPERTY_ORG)) {
863            // vCard specification does not specify other types.
864            final int type = Organization.TYPE_WORK;
865            boolean isPrimary = false;
866            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
867            if (typeCollection != null) {
868                for (String typeString : typeCollection) {
869                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
870                        isPrimary = true;
871                    }
872                }
873            }
874            handleOrgValue(type, propValueList, isPrimary);
875        } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
876            handleTitleValue(propValue);
877        } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
878            // This conflicts with TITLE. Ignore for now...
879            // handleTitleValue(propValue);
880        } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) ||
881                propName.equals(VCardConstants.PROPERTY_LOGO)) {
882            Collection<String> paramMapValue = paramMap.get("VALUE");
883            if (paramMapValue != null && paramMapValue.contains("URL")) {
884                // Currently we do not have appropriate example for testing this case.
885            } else {
886                final Collection<String> typeCollection = paramMap.get("TYPE");
887                String formatName = null;
888                boolean isPrimary = false;
889                if (typeCollection != null) {
890                    for (String typeValue : typeCollection) {
891                        if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
892                            isPrimary = true;
893                        } else if (formatName == null){
894                            formatName = typeValue;
895                        }
896                    }
897                }
898                addPhotoBytes(formatName, propBytes, isPrimary);
899            }
900        } else if (propName.equals(VCardConstants.PROPERTY_TEL)) {
901            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
902            final Object typeObject =
903                VCardUtils.getPhoneTypeFromStrings(typeCollection, propValue);
904            final int type;
905            final String label;
906            if (typeObject instanceof Integer) {
907                type = (Integer)typeObject;
908                label = null;
909            } else {
910                type = Phone.TYPE_CUSTOM;
911                label = typeObject.toString();
912            }
913
914            final boolean isPrimary;
915            if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
916                isPrimary = true;
917            } else {
918                isPrimary = false;
919            }
920            addPhone(type, propValue, label, isPrimary);
921        } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
922            // The phone number available via Skype.
923            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
924            final int type = Phone.TYPE_OTHER;
925            final boolean isPrimary;
926            if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
927                isPrimary = true;
928            } else {
929                isPrimary = false;
930            }
931            addPhone(type, propValue, null, isPrimary);
932        } else if (sImMap.containsKey(propName)) {
933            final int protocol = sImMap.get(propName);
934            boolean isPrimary = false;
935            int type = -1;
936            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
937            if (typeCollection != null) {
938                for (String typeString : typeCollection) {
939                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
940                        isPrimary = true;
941                    } else if (type < 0) {
942                        if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
943                            type = Im.TYPE_HOME;
944                        } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
945                            type = Im.TYPE_WORK;
946                        }
947                    }
948                }
949            }
950            if (type < 0) {
951                type = Phone.TYPE_HOME;
952            }
953            addIm(protocol, null, type, propValue, isPrimary);
954        } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) {
955            addNote(propValue);
956        } else if (propName.equals(VCardConstants.PROPERTY_URL)) {
957            if (mWebsiteList == null) {
958                mWebsiteList = new ArrayList<String>(1);
959            }
960            mWebsiteList.add(propValue);
961        } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) {
962            mBirthday = propValue;
963        } else if (propName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
964            mAnniversary = propValue;
965        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
966            mPhoneticGivenName = propValue;
967        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
968            mPhoneticMiddleName = propValue;
969        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
970            mPhoneticFamilyName = propValue;
971        } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
972            final List<String> customPropertyList =
973                VCardUtils.constructListFromValue(propValue,
974                        VCardConfig.isVersion30(mVCardType));
975            handleAndroidCustomProperty(customPropertyList);
976        } else {
977        }
978    }
979
980    private void handleAndroidCustomProperty(final List<String> customPropertyList) {
981        if (mAndroidCustomPropertyList == null) {
982            mAndroidCustomPropertyList = new ArrayList<List<String>>();
983        }
984        mAndroidCustomPropertyList.add(customPropertyList);
985    }
986
987    /**
988     * Construct the display name. The constructed data must not be null.
989     */
990    private void constructDisplayName() {
991        // FullName (created via "FN" or "NAME" field) is prefered.
992        if (!TextUtils.isEmpty(mFormattedName)) {
993            mDisplayName = mFormattedName;
994        } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
995            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
996                    mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix);
997        } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
998                TextUtils.isEmpty(mPhoneticGivenName))) {
999            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
1000                    mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName);
1001        } else if (mEmailList != null && mEmailList.size() > 0) {
1002            mDisplayName = mEmailList.get(0).data;
1003        } else if (mPhoneList != null && mPhoneList.size() > 0) {
1004            mDisplayName = mPhoneList.get(0).data;
1005        } else if (mPostalList != null && mPostalList.size() > 0) {
1006            mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType);
1007        } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
1008            mDisplayName = mOrganizationList.get(0).getFormattedString();
1009        }
1010
1011        if (mDisplayName == null) {
1012            mDisplayName = "";
1013        }
1014    }
1015
1016    /**
1017     * Consolidate several fielsds (like mName) using name candidates,
1018     */
1019    public void consolidateFields() {
1020        constructDisplayName();
1021
1022        if (mPhoneticFullName != null) {
1023            mPhoneticFullName = mPhoneticFullName.trim();
1024        }
1025    }
1026
1027    public Uri pushIntoContentResolver(ContentResolver resolver) {
1028        ArrayList<ContentProviderOperation> operationList =
1029            new ArrayList<ContentProviderOperation>();
1030        // After applying the batch the first result's Uri is returned so it is important that
1031        // the RawContact is the first operation that gets inserted into the list
1032        ContentProviderOperation.Builder builder =
1033            ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
1034        String myGroupsId = null;
1035        if (mAccount != null) {
1036            builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
1037            builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
1038        } else {
1039            builder.withValue(RawContacts.ACCOUNT_NAME, null);
1040            builder.withValue(RawContacts.ACCOUNT_TYPE, null);
1041        }
1042        operationList.add(builder.build());
1043
1044        if (!nameFieldsAreEmpty()) {
1045            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1046            builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
1047            builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
1048
1049            builder.withValue(StructuredName.GIVEN_NAME, mGivenName);
1050            builder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
1051            builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
1052            builder.withValue(StructuredName.PREFIX, mPrefix);
1053            builder.withValue(StructuredName.SUFFIX, mSuffix);
1054
1055            if (!(TextUtils.isEmpty(mPhoneticGivenName)
1056                    && TextUtils.isEmpty(mPhoneticFamilyName)
1057                    && TextUtils.isEmpty(mPhoneticMiddleName))) {
1058                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
1059                builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
1060                builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
1061            } else if (!TextUtils.isEmpty(mPhoneticFullName)) {
1062                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName);
1063            }
1064
1065            builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
1066            operationList.add(builder.build());
1067        }
1068
1069        if (mNickNameList != null && mNickNameList.size() > 0) {
1070            for (String nickName : mNickNameList) {
1071                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1072                builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
1073                builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
1074                builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1075                builder.withValue(Nickname.NAME, nickName);
1076                operationList.add(builder.build());
1077            }
1078        }
1079
1080        if (mPhoneList != null) {
1081            for (PhoneData phoneData : mPhoneList) {
1082                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1083                builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
1084                builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
1085
1086                builder.withValue(Phone.TYPE, phoneData.type);
1087                if (phoneData.type == Phone.TYPE_CUSTOM) {
1088                    builder.withValue(Phone.LABEL, phoneData.label);
1089                }
1090                builder.withValue(Phone.NUMBER, phoneData.data);
1091                if (phoneData.isPrimary) {
1092                    builder.withValue(Phone.IS_PRIMARY, 1);
1093                }
1094                operationList.add(builder.build());
1095            }
1096        }
1097
1098        if (mOrganizationList != null) {
1099            for (OrganizationData organizationData : mOrganizationList) {
1100                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1101                builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
1102                builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
1103                builder.withValue(Organization.TYPE, organizationData.type);
1104                if (organizationData.companyName != null) {
1105                    builder.withValue(Organization.COMPANY, organizationData.companyName);
1106                }
1107                if (organizationData.departmentName != null) {
1108                    builder.withValue(Organization.DEPARTMENT, organizationData.departmentName);
1109                }
1110                if (organizationData.titleName != null) {
1111                    builder.withValue(Organization.TITLE, organizationData.titleName);
1112                }
1113                if (organizationData.isPrimary) {
1114                    builder.withValue(Organization.IS_PRIMARY, 1);
1115                }
1116                operationList.add(builder.build());
1117            }
1118        }
1119
1120        if (mEmailList != null) {
1121            for (EmailData emailData : mEmailList) {
1122                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1123                builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
1124                builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
1125
1126                builder.withValue(Email.TYPE, emailData.type);
1127                if (emailData.type == Email.TYPE_CUSTOM) {
1128                    builder.withValue(Email.LABEL, emailData.label);
1129                }
1130                builder.withValue(Email.DATA, emailData.data);
1131                if (emailData.isPrimary) {
1132                    builder.withValue(Data.IS_PRIMARY, 1);
1133                }
1134                operationList.add(builder.build());
1135            }
1136        }
1137
1138        if (mPostalList != null) {
1139            for (PostalData postalData : mPostalList) {
1140                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1141                VCardUtils.insertStructuredPostalDataUsingContactsStruct(
1142                        mVCardType, builder, postalData);
1143                operationList.add(builder.build());
1144            }
1145        }
1146
1147        if (mImList != null) {
1148            for (ImData imData : mImList) {
1149                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1150                builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
1151                builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
1152                builder.withValue(Im.TYPE, imData.type);
1153                builder.withValue(Im.PROTOCOL, imData.protocol);
1154                if (imData.protocol == Im.PROTOCOL_CUSTOM) {
1155                    builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol);
1156                }
1157                if (imData.isPrimary) {
1158                    builder.withValue(Data.IS_PRIMARY, 1);
1159                }
1160            }
1161        }
1162
1163        if (mNoteList != null) {
1164            for (String note : mNoteList) {
1165                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1166                builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
1167                builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
1168                builder.withValue(Note.NOTE, note);
1169                operationList.add(builder.build());
1170            }
1171        }
1172
1173        if (mPhotoList != null) {
1174            for (PhotoData photoData : mPhotoList) {
1175                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1176                builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
1177                builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
1178                builder.withValue(Photo.PHOTO, photoData.photoBytes);
1179                if (photoData.isPrimary) {
1180                    builder.withValue(Photo.IS_PRIMARY, 1);
1181                }
1182                operationList.add(builder.build());
1183            }
1184        }
1185
1186        if (mWebsiteList != null) {
1187            for (String website : mWebsiteList) {
1188                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1189                builder.withValueBackReference(Website.RAW_CONTACT_ID, 0);
1190                builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
1191                builder.withValue(Website.URL, website);
1192                // There's no information about the type of URL in vCard.
1193                // We use TYPE_HOMEPAGE for safety.
1194                builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
1195                operationList.add(builder.build());
1196            }
1197        }
1198
1199        if (!TextUtils.isEmpty(mBirthday)) {
1200            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1201            builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1202            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1203            builder.withValue(Event.START_DATE, mBirthday);
1204            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1205            operationList.add(builder.build());
1206        }
1207
1208        if (!TextUtils.isEmpty(mAnniversary)) {
1209            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1210            builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1211            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1212            builder.withValue(Event.START_DATE, mBirthday);
1213            builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
1214            operationList.add(builder.build());
1215        }
1216
1217        if (mAndroidCustomPropertyList != null) {
1218            for (List<String> customPropertyList : mAndroidCustomPropertyList) {
1219                int size = customPropertyList.size();
1220                if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) {
1221                    continue;
1222                } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) {
1223                    size = VCardConstants.MAX_DATA_COLUMN + 1;
1224                    customPropertyList =
1225                        customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2);
1226                }
1227
1228                int i = 0;
1229                for (final String customPropertyValue : customPropertyList) {
1230                    if (i == 0) {
1231                        final String mimeType = customPropertyValue;
1232                        builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1233                        builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
1234                        builder.withValue(Data.MIMETYPE, mimeType);
1235                    } else {  // 1 <= i && i <= MAX_DATA_COLUMNS
1236                        if (!TextUtils.isEmpty(customPropertyValue)) {
1237                            builder.withValue("data" + i, customPropertyValue);
1238                        }
1239                    }
1240
1241                    i++;
1242                }
1243                operationList.add(builder.build());
1244            }
1245        }
1246
1247        if (myGroupsId != null) {
1248            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1249            builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
1250            builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1251            builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
1252            operationList.add(builder.build());
1253        }
1254
1255        try {
1256            ContentProviderResult[] results = resolver.applyBatch(
1257                        ContactsContract.AUTHORITY, operationList);
1258            // the first result is always the raw_contact. return it's uri so
1259            // that it can be found later. do null checking for badly behaving
1260            // ContentResolvers
1261            return (results == null || results.length == 0 || results[0] == null)
1262                ? null
1263                : results[0].uri;
1264        } catch (RemoteException e) {
1265            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
1266            return null;
1267        } catch (OperationApplicationException e) {
1268            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
1269            return null;
1270        }
1271    }
1272
1273    public static VCardEntry buildFromResolver(ContentResolver resolver) {
1274        return buildFromResolver(resolver, Contacts.CONTENT_URI);
1275    }
1276
1277    public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
1278
1279        return null;
1280    }
1281
1282    private boolean nameFieldsAreEmpty() {
1283        return (TextUtils.isEmpty(mFamilyName)
1284                && TextUtils.isEmpty(mMiddleName)
1285                && TextUtils.isEmpty(mGivenName)
1286                && TextUtils.isEmpty(mPrefix)
1287                && TextUtils.isEmpty(mSuffix)
1288                && TextUtils.isEmpty(mFormattedName)
1289                && TextUtils.isEmpty(mPhoneticFamilyName)
1290                && TextUtils.isEmpty(mPhoneticMiddleName)
1291                && TextUtils.isEmpty(mPhoneticGivenName)
1292                && TextUtils.isEmpty(mPhoneticFullName));
1293    }
1294
1295    public boolean isIgnorable() {
1296        return getDisplayName().length() == 0;
1297    }
1298
1299    private String listToString(List<String> list){
1300        final int size = list.size();
1301        if (size > 1) {
1302            StringBuilder builder = new StringBuilder();
1303            int i = 0;
1304            for (String type : list) {
1305                builder.append(type);
1306                if (i < size - 1) {
1307                    builder.append(";");
1308                }
1309            }
1310            return builder.toString();
1311        } else if (size == 1) {
1312            return list.get(0);
1313        } else {
1314            return "";
1315        }
1316    }
1317
1318    // All getter methods should be used carefully, since they may change
1319    // in the future as of 2009-10-05, on which I cannot be sure this structure
1320    // is completely consolidated.
1321    //
1322    // Also note that these getter methods should be used only after
1323    // all properties being pushed into this object. If not, incorrect
1324    // value will "be stored in the local cache and" be returned to you.
1325
1326    public String getFamilyName() {
1327        return mFamilyName;
1328    }
1329
1330    public String getGivenName() {
1331        return mGivenName;
1332    }
1333
1334    public String getMiddleName() {
1335        return mMiddleName;
1336    }
1337
1338    public String getPrefix() {
1339        return mPrefix;
1340    }
1341
1342    public String getSuffix() {
1343        return mSuffix;
1344    }
1345
1346    public String getFullName() {
1347        return mFormattedName;
1348    }
1349
1350    public String getPhoneticFamilyName() {
1351        return mPhoneticFamilyName;
1352    }
1353
1354    public String getPhoneticGivenName() {
1355        return mPhoneticGivenName;
1356    }
1357
1358    public String getPhoneticMiddleName() {
1359        return mPhoneticMiddleName;
1360    }
1361
1362    public String getPhoneticFullName() {
1363        return mPhoneticFullName;
1364    }
1365
1366    public final List<String> getNickNameList() {
1367        return mNickNameList;
1368    }
1369
1370    public String getBirthday() {
1371        return mBirthday;
1372    }
1373
1374    public final List<String> getNotes() {
1375        return mNoteList;
1376    }
1377
1378    public final List<PhoneData> getPhoneList() {
1379        return mPhoneList;
1380    }
1381
1382    public final List<EmailData> getEmailList() {
1383        return mEmailList;
1384    }
1385
1386    public final List<PostalData> getPostalList() {
1387        return mPostalList;
1388    }
1389
1390    public final List<OrganizationData> getOrganizationList() {
1391        return mOrganizationList;
1392    }
1393
1394    public final List<ImData> getImList() {
1395        return mImList;
1396    }
1397
1398    public final List<PhotoData> getPhotoList() {
1399        return mPhotoList;
1400    }
1401
1402    public final List<String> getWebsiteList() {
1403        return mWebsiteList;
1404    }
1405
1406    public String getDisplayName() {
1407        if (mDisplayName == null) {
1408            constructDisplayName();
1409        }
1410        return mDisplayName;
1411    }
1412}
1413