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