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