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