VCardEntry.java revision 9919ad2126c06dbf2eb54a11e6158f87f316bc22
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 com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
19
20import android.accounts.Account;
21import android.content.ContentProviderOperation;
22import android.content.ContentProviderResult;
23import android.content.ContentResolver;
24import android.content.OperationApplicationException;
25import android.net.Uri;
26import android.os.RemoteException;
27import android.provider.ContactsContract;
28import android.provider.ContactsContract.CommonDataKinds.Email;
29import android.provider.ContactsContract.CommonDataKinds.Event;
30import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
31import android.provider.ContactsContract.CommonDataKinds.Im;
32import android.provider.ContactsContract.CommonDataKinds.Nickname;
33import android.provider.ContactsContract.CommonDataKinds.Note;
34import android.provider.ContactsContract.CommonDataKinds.Organization;
35import android.provider.ContactsContract.CommonDataKinds.Phone;
36import android.provider.ContactsContract.CommonDataKinds.Photo;
37import android.provider.ContactsContract.CommonDataKinds.SipAddress;
38import android.provider.ContactsContract.CommonDataKinds.StructuredName;
39import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
40import android.provider.ContactsContract.CommonDataKinds.Website;
41import android.provider.ContactsContract.Contacts;
42import android.provider.ContactsContract.Data;
43import android.provider.ContactsContract.RawContacts;
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.LinkedHashSet;
54import java.util.List;
55import java.util.Map;
56import java.util.Set;
57
58/**
59 * This class bridges between data structure of Contact app and VCard data.
60 */
61public class VCardEntry {
62    private static final String LOG_TAG = "VCardEntry";
63
64    private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
65
66    private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
67
68    static {
69        sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
70        sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
71        sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
72        sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
73        sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
74        sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
75        sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
76        sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
77                Im.PROTOCOL_GOOGLE_TALK);
78    }
79
80    public static class PhoneData {
81        public final int type;
82        public final String data;
83        public final String label;
84        // isPrimary is (not final but) changable, only when there's no appropriate one existing
85        // in the original VCard.
86        public boolean isPrimary;
87        public PhoneData(int type, String data, String label, boolean isPrimary) {
88            this.type = type;
89            this.data = data;
90            this.label = label;
91            this.isPrimary = isPrimary;
92        }
93
94        @Override
95        public boolean equals(Object obj) {
96            if (!(obj instanceof PhoneData)) {
97                return false;
98            }
99            PhoneData phoneData = (PhoneData)obj;
100            return (type == phoneData.type && data.equals(phoneData.data) &&
101                    label.equals(phoneData.label) && isPrimary == phoneData.isPrimary);
102        }
103
104        @Override
105        public String toString() {
106            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
107                    type, data, label, isPrimary);
108        }
109    }
110
111    public static class EmailData {
112        public final int type;
113        public final String data;
114        // Used only when TYPE is TYPE_CUSTOM.
115        public final String label;
116        public boolean isPrimary;
117        public EmailData(int type, String data, String label, boolean isPrimary) {
118            this.type = type;
119            this.data = data;
120            this.label = label;
121            this.isPrimary = isPrimary;
122        }
123
124        @Override
125        public boolean equals(Object obj) {
126            if (!(obj instanceof EmailData)) {
127                return false;
128            }
129            EmailData emailData = (EmailData)obj;
130            return (type == emailData.type && data.equals(emailData.data) &&
131                    label.equals(emailData.label) && isPrimary == emailData.isPrimary);
132        }
133
134        @Override
135        public String toString() {
136            return String.format("type: %d, data: %s, label: %s, isPrimary: %s",
137                    type, data, label, isPrimary);
138        }
139    }
140
141    public static class PostalData {
142        // Determined by vCard specification.
143        // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
144        public static final int ADDR_MAX_DATA_SIZE = 7;
145        private final String[] dataArray;
146        public final String pobox;
147        public final String extendedAddress;
148        public final String street;
149        public final String localty;
150        public final String region;
151        public final String postalCode;
152        public final String country;
153        public final int type;
154        public final String label;
155        public boolean isPrimary;
156
157        public PostalData(final int type, final List<String> propValueList,
158                final String label, boolean isPrimary) {
159            this.type = type;
160            dataArray = new String[ADDR_MAX_DATA_SIZE];
161
162            int size = propValueList.size();
163            if (size > ADDR_MAX_DATA_SIZE) {
164                size = ADDR_MAX_DATA_SIZE;
165            }
166
167            // adr-value = 0*6(text-value ";") text-value
168            //           ; PO Box, Extended Address, Street, Locality, Region, Postal
169            //           ; Code, Country Name
170            //
171            // Use Iterator assuming List may be LinkedList, though actually it is
172            // always ArrayList in the current implementation.
173            int i = 0;
174            for (String addressElement : propValueList) {
175                dataArray[i] = addressElement;
176                if (++i >= size) {
177                    break;
178                }
179            }
180            while (i < ADDR_MAX_DATA_SIZE) {
181                dataArray[i++] = null;
182            }
183
184            this.pobox = dataArray[0];
185            this.extendedAddress = dataArray[1];
186            this.street = dataArray[2];
187            this.localty = dataArray[3];
188            this.region = dataArray[4];
189            this.postalCode = dataArray[5];
190            this.country = dataArray[6];
191            this.label = label;
192            this.isPrimary = isPrimary;
193        }
194
195        @Override
196        public boolean equals(Object obj) {
197            if (!(obj instanceof PostalData)) {
198                return false;
199            }
200            final PostalData postalData = (PostalData)obj;
201            return (Arrays.equals(dataArray, postalData.dataArray) &&
202                    (type == postalData.type &&
203                            (type == StructuredPostal.TYPE_CUSTOM ?
204                                    (label == postalData.label) : true)) &&
205                    (isPrimary == postalData.isPrimary));
206        }
207
208        public String getFormattedAddress(final int vcardType) {
209            StringBuilder builder = new StringBuilder();
210            boolean empty = true;
211            if (VCardConfig.isJapaneseDevice(vcardType)) {
212                // In Japan, the order is reversed.
213                for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
214                    String addressPart = dataArray[i];
215                    if (!TextUtils.isEmpty(addressPart)) {
216                        if (!empty) {
217                            builder.append(' ');
218                        } else {
219                            empty = false;
220                        }
221                        builder.append(addressPart);
222                    }
223                }
224            } else {
225                for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
226                    String addressPart = dataArray[i];
227                    if (!TextUtils.isEmpty(addressPart)) {
228                        if (!empty) {
229                            builder.append(' ');
230                        } else {
231                            empty = false;
232                        }
233                        builder.append(addressPart);
234                    }
235                }
236            }
237
238            return builder.toString().trim();
239        }
240
241        @Override
242        public String toString() {
243            return String.format("type: %d, label: %s, isPrimary: %s",
244                    type, label, isPrimary);
245        }
246    }
247
248    public static class OrganizationData {
249        public final int type;
250        // non-final is Intentional: we may change the values since this info is separated into
251        // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in
252        // different timing.
253        public String companyName;
254        public String departmentName;
255        public String titleName;
256        public final String phoneticName;  // We won't have this in "TITLE" property.
257        public boolean isPrimary;
258
259        public OrganizationData(int type,
260                final String companyName,
261                final String departmentName,
262                final String titleName,
263                final String phoneticName,
264                final boolean isPrimary) {
265            this.type = type;
266            this.companyName = companyName;
267            this.departmentName = departmentName;
268            this.titleName = titleName;
269            this.phoneticName = phoneticName;
270            this.isPrimary = isPrimary;
271        }
272
273        @Override
274        public boolean equals(Object obj) {
275            if (!(obj instanceof OrganizationData)) {
276                return false;
277            }
278            OrganizationData organization = (OrganizationData)obj;
279            return (type == organization.type &&
280                    TextUtils.equals(companyName, organization.companyName) &&
281                    TextUtils.equals(departmentName, organization.departmentName) &&
282                    TextUtils.equals(titleName, organization.titleName) &&
283                    isPrimary == organization.isPrimary);
284        }
285
286        public String getFormattedString() {
287            final StringBuilder builder = new StringBuilder();
288            if (!TextUtils.isEmpty(companyName)) {
289                builder.append(companyName);
290            }
291
292            if (!TextUtils.isEmpty(departmentName)) {
293                if (builder.length() > 0) {
294                    builder.append(", ");
295                }
296                builder.append(departmentName);
297            }
298
299            if (!TextUtils.isEmpty(titleName)) {
300                if (builder.length() > 0) {
301                    builder.append(", ");
302                }
303                builder.append(titleName);
304            }
305
306            return builder.toString();
307        }
308
309        @Override
310        public String toString() {
311            return String.format(
312                    "type: %d, company: %s, department: %s, title: %s, isPrimary: %s",
313                    type, companyName, departmentName, titleName, isPrimary);
314        }
315    }
316
317    public static class ImData {
318        public final int protocol;
319        public final String customProtocol;
320        public final int type;
321        public final String data;
322        public final boolean isPrimary;
323
324        public ImData(final int protocol, final String customProtocol, final int type,
325                final String data, final boolean isPrimary) {
326            this.protocol = protocol;
327            this.customProtocol = customProtocol;
328            this.type = type;
329            this.data = data;
330            this.isPrimary = isPrimary;
331        }
332
333        @Override
334        public boolean equals(Object obj) {
335            if (!(obj instanceof ImData)) {
336                return false;
337            }
338            ImData imData = (ImData)obj;
339            return (type == imData.type && protocol == imData.protocol
340                    && (customProtocol != null ? customProtocol.equals(imData.customProtocol) :
341                        (imData.customProtocol == null))
342                    && (data != null ? data.equals(imData.data) : (imData.data == null))
343                    && isPrimary == imData.isPrimary);
344        }
345
346        @Override
347        public String toString() {
348            return String.format(
349                    "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s",
350                    type, protocol, customProtocol, data, isPrimary);
351        }
352    }
353
354    public static class PhotoData {
355        public static final String FORMAT_FLASH = "SWF";
356        public final int type;
357        public final String formatName;  // used when type is not defined in ContactsContract.
358        public final byte[] photoBytes;
359        public final boolean isPrimary;
360
361        public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) {
362            this.type = type;
363            this.formatName = formatName;
364            this.photoBytes = photoBytes;
365            this.isPrimary = isPrimary;
366        }
367
368        @Override
369        public boolean equals(Object obj) {
370            if (!(obj instanceof PhotoData)) {
371                return false;
372            }
373            PhotoData photoData = (PhotoData)obj;
374            return (type == photoData.type &&
375                    (formatName == null ? (photoData.formatName == null) :
376                            formatName.equals(photoData.formatName)) &&
377                    (Arrays.equals(photoBytes, photoData.photoBytes)) &&
378                    (isPrimary == photoData.isPrimary));
379        }
380
381        @Override
382        public String toString() {
383            return String.format("type: %d, format: %s: size: %d, isPrimary: %s",
384                    type, formatName, photoBytes.length, isPrimary);
385        }
386    }
387
388    /* package */ static class Property {
389        private String mPropertyName;
390        private Map<String, Collection<String>> mParameterMap =
391            new HashMap<String, Collection<String>>();
392        private List<String> mPropertyValueList = new ArrayList<String>();
393        private byte[] mPropertyBytes;
394
395        public void setPropertyName(final String propertyName) {
396            mPropertyName = propertyName;
397        }
398
399        public void addParameter(final String paramName, final String paramValue) {
400            Collection<String> values;
401            if (!mParameterMap.containsKey(paramName)) {
402                if (paramName.equals("TYPE")) {
403                    values = new HashSet<String>();
404                } else {
405                    values = new ArrayList<String>();
406                }
407                mParameterMap.put(paramName, values);
408            } else {
409                values = mParameterMap.get(paramName);
410            }
411            values.add(paramValue);
412        }
413
414        public void addToPropertyValueList(final String propertyValue) {
415            mPropertyValueList.add(propertyValue);
416        }
417
418        public void setPropertyBytes(final byte[] propertyBytes) {
419            mPropertyBytes = propertyBytes;
420        }
421
422        public final Collection<String> getParameters(String type) {
423            return mParameterMap.get(type);
424        }
425
426        public final List<String> getPropertyValueList() {
427            return mPropertyValueList;
428        }
429
430        public void clear() {
431            mPropertyName = null;
432            mParameterMap.clear();
433            mPropertyValueList.clear();
434            mPropertyBytes = null;
435        }
436    }
437
438    // TODO(dmiyakawa): vCard 4.0 logically has multiple formatted names and we need to
439    // select the most preferable one using PREF parameter.
440    //
441    // e.g. (based on rev.13)
442    // FN;PREF=1:John M. Doe
443    // FN;PREF=2:John Doe
444    // FN;PREF=3;John
445
446    private String mFamilyName;
447    private String mGivenName;
448    private String mMiddleName;
449    private String mPrefix;
450    private String mSuffix;
451
452    // Used only when no family nor given name is found.
453    private String mFormattedName;
454
455    private String mPhoneticFamilyName;
456    private String mPhoneticGivenName;
457    private String mPhoneticMiddleName;
458
459    private String mPhoneticFullName;
460
461    private List<String> mNickNameList;
462
463    private String mDisplayName;
464
465    private String mBirthday;
466    private String mAnniversary;
467
468    private List<String> mNoteList;
469    private List<PhoneData> mPhoneList;
470    private List<EmailData> mEmailList;
471    private List<PostalData> mPostalList;
472    private List<OrganizationData> mOrganizationList;
473    private List<ImData> mImList;
474    private List<PhotoData> mPhotoList;
475    private List<String> mWebsiteList;
476    private Set<String> mSipSet;
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 = PhoneNumberUtilsPort.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            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
876            if (typeCollection != null) {
877                for (final String typeStringOrg : typeCollection) {
878                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
879                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
880                        isPrimary = true;
881                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
882                        type = StructuredPostal.TYPE_HOME;
883                        label = "";
884                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK) ||
885                            typeStringUpperCase.equalsIgnoreCase(
886                                        VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
887                        // "COMPANY" seems emitted by Windows Mobile, which is not
888                        // specifically supported by vCard 2.1. We assume this is same
889                        // as "WORK".
890                        type = StructuredPostal.TYPE_WORK;
891                        label = "";
892                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) ||
893                            typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM) ||
894                            typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
895                        // We do not have any appropriate way to store this information.
896                    } else if (type < 0) {  // If no other type is specified before
897                        type = StructuredPostal.TYPE_CUSTOM;
898                        if (typeStringUpperCase.startsWith("X-")) {  // If X- or x-
899                            label = typeStringOrg.substring(2);
900                        } else {
901                            label = typeStringOrg;
902                        }
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            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
917            if (typeCollection != null) {
918                for (final String typeStringOrg : typeCollection) {
919                    final String typeStringUpperCase = typeStringOrg.toUpperCase();
920                    if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
921                        isPrimary = true;
922                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
923                        type = Email.TYPE_HOME;
924                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
925                        type = Email.TYPE_WORK;
926                    } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
927                        type = Email.TYPE_MOBILE;
928                    } else if (type < 0) {  // If no other type is specified before
929                        if (typeStringUpperCase.startsWith("X-")) {  // If X- or x-
930                            label = typeStringOrg.substring(2);
931                        } else {
932                            label = typeStringOrg;
933                        }
934                        type = Email.TYPE_CUSTOM;
935                    }
936                }
937            }
938            if (type < 0) {
939                type = Email.TYPE_OTHER;
940            }
941            addEmail(type, propValue, label, isPrimary);
942        } else if (propName.equals(VCardConstants.PROPERTY_ORG)) {
943            // vCard specification does not specify other types.
944            final int type = Organization.TYPE_WORK;
945            boolean isPrimary = false;
946            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
947            if (typeCollection != null) {
948                for (String typeString : typeCollection) {
949                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
950                        isPrimary = true;
951                    }
952                }
953            }
954            handleOrgValue(type, propValueList, paramMap, isPrimary);
955        } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) {
956            handleTitleValue(propValue);
957        } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) {
958            // This conflicts with TITLE. Ignore for now...
959            // handleTitleValue(propValue);
960        } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) ||
961                propName.equals(VCardConstants.PROPERTY_LOGO)) {
962            Collection<String> paramMapValue = paramMap.get("VALUE");
963            if (paramMapValue != null && paramMapValue.contains("URL")) {
964                // Currently we do not have appropriate example for testing this case.
965            } else {
966                final Collection<String> typeCollection = paramMap.get("TYPE");
967                String formatName = null;
968                boolean isPrimary = false;
969                if (typeCollection != null) {
970                    for (String typeValue : typeCollection) {
971                        if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
972                            isPrimary = true;
973                        } else if (formatName == null){
974                            formatName = typeValue;
975                        }
976                    }
977                }
978                addPhotoBytes(formatName, propBytes, isPrimary);
979            }
980        } else if (propName.equals(VCardConstants.PROPERTY_TEL)) {
981            final String phoneNumber;
982            if (VCardConfig.isVersion40(mVCardType)) {
983                // Given propValue is in URI format, not in phone number format used until
984                // vCard 3.0.
985                if (propValue.startsWith("sip:") ) {
986                    if (propValue.length() > 4) {
987                        if (mSipSet == null) {
988                            mSipSet = new LinkedHashSet<String>();
989                        }
990                        mSipSet.add(propValue.substring(4));
991                    }
992                    return;
993                } else if (propValue.startsWith("tel:")) {
994                    phoneNumber = propValue.substring(4);
995                } else {
996                    // We don't know appropriate way to handle the other schemas. Also,
997                    // we may still have non-URI phone number. To keep given data as much as
998                    // we can, just save original value here.
999                    phoneNumber = propValue;
1000                }
1001            } else {
1002                phoneNumber = propValue;
1003            }
1004
1005            if (propValue.length() == 0) {
1006                return;
1007            }
1008            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
1009            final Object typeObject =
1010                    VCardUtils.getPhoneTypeFromStrings(typeCollection, phoneNumber);
1011            final int type;
1012            final String label;
1013            if (typeObject instanceof Integer) {
1014                type = (Integer)typeObject;
1015                label = null;
1016            } else {
1017                type = Phone.TYPE_CUSTOM;
1018                label = typeObject.toString();
1019            }
1020
1021            final boolean isPrimary;
1022            if (typeCollection != null &&
1023                    typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
1024                isPrimary = true;
1025            } else {
1026                isPrimary = false;
1027            }
1028            addPhone(type, phoneNumber, label, isPrimary);
1029        } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
1030            // The phone number available via Skype.
1031            Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
1032            final int type = Phone.TYPE_OTHER;
1033            final boolean isPrimary;
1034            if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
1035                isPrimary = true;
1036            } else {
1037                isPrimary = false;
1038            }
1039            addPhone(type, propValue, null, isPrimary);
1040        } else if (sImMap.containsKey(propName)) {
1041            final int protocol = sImMap.get(propName);
1042            boolean isPrimary = false;
1043            int type = -1;
1044            final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
1045            if (typeCollection != null) {
1046                for (String typeString : typeCollection) {
1047                    if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
1048                        isPrimary = true;
1049                    } else if (type < 0) {
1050                        if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
1051                            type = Im.TYPE_HOME;
1052                        } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
1053                            type = Im.TYPE_WORK;
1054                        }
1055                    }
1056                }
1057            }
1058            if (type < 0) {
1059                type = Im.TYPE_HOME;
1060            }
1061            addIm(protocol, null, type, propValue, isPrimary);
1062        } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) {
1063            addNote(propValue);
1064        } else if (propName.equals(VCardConstants.PROPERTY_URL)) {
1065            if (mWebsiteList == null) {
1066                mWebsiteList = new ArrayList<String>(1);
1067            }
1068            mWebsiteList.add(propValue);
1069        } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) {
1070            mBirthday = propValue;
1071        } else if (propName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
1072            mAnniversary = propValue;
1073        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
1074            mPhoneticGivenName = propValue;
1075        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
1076            mPhoneticMiddleName = propValue;
1077        } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
1078            mPhoneticFamilyName = propValue;
1079        } else if (propName.equals(VCardConstants.PROPERTY_IMPP)) {
1080            // See also RFC 4770 (for vCard 3.0)
1081            if (propValue.startsWith("sip:") && propValue.length() > 4) {
1082                if (mSipSet == null) {
1083                    mSipSet = new LinkedHashSet<String>();
1084                }
1085                mSipSet.add(propValue.substring(4));
1086            }
1087        } else if (propName.equals(VCardConstants.PROPERTY_X_SIP)) {
1088            if (!TextUtils.isEmpty(propValue)) {
1089                if (mSipSet == null) {
1090                    mSipSet = new LinkedHashSet<String>();
1091                }
1092
1093                if (propValue.startsWith("sip:")) {
1094                    if (propValue.length() > 4) {
1095                        mSipSet.add(propValue.substring(4));
1096                    } else {
1097                        // Empty sip value. Ignore.
1098                    }
1099                } else {
1100                    mSipSet.add(propValue);
1101                }
1102            }
1103        } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
1104            final List<String> customPropertyList =
1105                VCardUtils.constructListFromValue(propValue, mVCardType);
1106            handleAndroidCustomProperty(customPropertyList);
1107        } else {
1108        }
1109        // Be careful when adding some logic here, as some blocks above may use "return".
1110    }
1111
1112    private void handleAndroidCustomProperty(final List<String> customPropertyList) {
1113        if (mAndroidCustomPropertyList == null) {
1114            mAndroidCustomPropertyList = new ArrayList<List<String>>();
1115        }
1116        mAndroidCustomPropertyList.add(customPropertyList);
1117    }
1118
1119    /**
1120     * Construct the display name. The constructed data must not be null.
1121     */
1122    private void constructDisplayName() {
1123        // FullName (created via "FN" or "NAME" field) is prefered.
1124        if (!TextUtils.isEmpty(mFormattedName)) {
1125            mDisplayName = mFormattedName;
1126        } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) {
1127            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
1128                    mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix);
1129        } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) &&
1130                TextUtils.isEmpty(mPhoneticGivenName))) {
1131            mDisplayName = VCardUtils.constructNameFromElements(mVCardType,
1132                    mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName);
1133        } else if (mEmailList != null && mEmailList.size() > 0) {
1134            mDisplayName = mEmailList.get(0).data;
1135        } else if (mPhoneList != null && mPhoneList.size() > 0) {
1136            mDisplayName = mPhoneList.get(0).data;
1137        } else if (mPostalList != null && mPostalList.size() > 0) {
1138            mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType);
1139        } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
1140            mDisplayName = mOrganizationList.get(0).getFormattedString();
1141        }
1142
1143        if (mDisplayName == null) {
1144            mDisplayName = "";
1145        }
1146    }
1147
1148    /**
1149     * Consolidate several fielsds (like mName) using name candidates,
1150     */
1151    public void consolidateFields() {
1152        constructDisplayName();
1153
1154        if (mPhoneticFullName != null) {
1155            mPhoneticFullName = mPhoneticFullName.trim();
1156        }
1157    }
1158
1159    public Uri pushIntoContentResolver(ContentResolver resolver) {
1160        ArrayList<ContentProviderOperation> operationList =
1161            new ArrayList<ContentProviderOperation>();
1162        // After applying the batch the first result's Uri is returned so it is important that
1163        // the RawContact is the first operation that gets inserted into the list
1164        ContentProviderOperation.Builder builder =
1165            ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
1166        String myGroupsId = null;
1167        if (mAccount != null) {
1168            builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
1169            builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
1170        } else {
1171            builder.withValue(RawContacts.ACCOUNT_NAME, null);
1172            builder.withValue(RawContacts.ACCOUNT_TYPE, null);
1173        }
1174        operationList.add(builder.build());
1175
1176        if (!nameFieldsAreEmpty()) {
1177            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1178            builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
1179            builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
1180
1181            builder.withValue(StructuredName.GIVEN_NAME, mGivenName);
1182            builder.withValue(StructuredName.FAMILY_NAME, mFamilyName);
1183            builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName);
1184            builder.withValue(StructuredName.PREFIX, mPrefix);
1185            builder.withValue(StructuredName.SUFFIX, mSuffix);
1186
1187            if (!(TextUtils.isEmpty(mPhoneticGivenName)
1188                    && TextUtils.isEmpty(mPhoneticFamilyName)
1189                    && TextUtils.isEmpty(mPhoneticMiddleName))) {
1190                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName);
1191                builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName);
1192                builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName);
1193            } else if (!TextUtils.isEmpty(mPhoneticFullName)) {
1194                builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName);
1195            }
1196
1197            builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName());
1198            operationList.add(builder.build());
1199        }
1200
1201        if (mNickNameList != null && mNickNameList.size() > 0) {
1202            for (String nickName : mNickNameList) {
1203                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1204                builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0);
1205                builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
1206                builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
1207                builder.withValue(Nickname.NAME, nickName);
1208                operationList.add(builder.build());
1209            }
1210        }
1211
1212        if (mPhoneList != null) {
1213            for (PhoneData phoneData : mPhoneList) {
1214                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1215                builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
1216                builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
1217
1218                builder.withValue(Phone.TYPE, phoneData.type);
1219                if (phoneData.type == Phone.TYPE_CUSTOM) {
1220                    builder.withValue(Phone.LABEL, phoneData.label);
1221                }
1222                builder.withValue(Phone.NUMBER, phoneData.data);
1223                if (phoneData.isPrimary) {
1224                    builder.withValue(Phone.IS_PRIMARY, 1);
1225                }
1226                operationList.add(builder.build());
1227            }
1228        }
1229
1230        if (mOrganizationList != null) {
1231            for (OrganizationData organizationData : mOrganizationList) {
1232                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1233                builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0);
1234                builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
1235                builder.withValue(Organization.TYPE, organizationData.type);
1236                if (organizationData.companyName != null) {
1237                    builder.withValue(Organization.COMPANY, organizationData.companyName);
1238                }
1239                if (organizationData.departmentName != null) {
1240                    builder.withValue(Organization.DEPARTMENT, organizationData.departmentName);
1241                }
1242                if (organizationData.titleName != null) {
1243                    builder.withValue(Organization.TITLE, organizationData.titleName);
1244                }
1245                if (organizationData.phoneticName != null) {
1246                    builder.withValue(Organization.PHONETIC_NAME, organizationData.phoneticName);
1247                }
1248                if (organizationData.isPrimary) {
1249                    builder.withValue(Organization.IS_PRIMARY, 1);
1250                }
1251                operationList.add(builder.build());
1252            }
1253        }
1254
1255        if (mEmailList != null) {
1256            for (EmailData emailData : mEmailList) {
1257                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1258                builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
1259                builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
1260
1261                builder.withValue(Email.TYPE, emailData.type);
1262                if (emailData.type == Email.TYPE_CUSTOM) {
1263                    builder.withValue(Email.LABEL, emailData.label);
1264                }
1265                builder.withValue(Email.DATA, emailData.data);
1266                if (emailData.isPrimary) {
1267                    builder.withValue(Data.IS_PRIMARY, 1);
1268                }
1269                operationList.add(builder.build());
1270            }
1271        }
1272
1273        if (mPostalList != null) {
1274            for (PostalData postalData : mPostalList) {
1275                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1276                builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1277                VCardUtils.insertStructuredPostalDataUsingContactsStruct(
1278                        mVCardType, builder, postalData);
1279                operationList.add(builder.build());
1280            }
1281        }
1282
1283        if (mImList != null) {
1284            for (ImData imData : mImList) {
1285                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1286                builder.withValueBackReference(Im.RAW_CONTACT_ID, 0);
1287                builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
1288                builder.withValue(Im.TYPE, imData.type);
1289                builder.withValue(Im.PROTOCOL, imData.protocol);
1290                builder.withValue(Im.DATA, imData.data);
1291                if (imData.protocol == Im.PROTOCOL_CUSTOM) {
1292                    builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol);
1293                }
1294                if (imData.isPrimary) {
1295                    builder.withValue(Data.IS_PRIMARY, 1);
1296                }
1297                operationList.add(builder.build());
1298            }
1299        }
1300
1301        if (mNoteList != null) {
1302            for (String note : mNoteList) {
1303                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1304                builder.withValueBackReference(Note.RAW_CONTACT_ID, 0);
1305                builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
1306                builder.withValue(Note.NOTE, note);
1307                operationList.add(builder.build());
1308            }
1309        }
1310
1311        if (mPhotoList != null) {
1312            for (PhotoData photoData : mPhotoList) {
1313                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1314                builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0);
1315                builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
1316                builder.withValue(Photo.PHOTO, photoData.photoBytes);
1317                if (photoData.isPrimary) {
1318                    builder.withValue(Photo.IS_PRIMARY, 1);
1319                }
1320                operationList.add(builder.build());
1321            }
1322        }
1323
1324        if (mWebsiteList != null) {
1325            for (String website : mWebsiteList) {
1326                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1327                builder.withValueBackReference(Website.RAW_CONTACT_ID, 0);
1328                builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
1329                builder.withValue(Website.URL, website);
1330                // There's no information about the type of URL in vCard.
1331                // We use TYPE_HOMEPAGE for safety.
1332                builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
1333                operationList.add(builder.build());
1334            }
1335        }
1336
1337        if (!TextUtils.isEmpty(mBirthday)) {
1338            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1339            builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1340            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1341            builder.withValue(Event.START_DATE, mBirthday);
1342            builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
1343            operationList.add(builder.build());
1344        }
1345
1346        if (!TextUtils.isEmpty(mAnniversary)) {
1347            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1348            builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1349            builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
1350            builder.withValue(Event.START_DATE, mAnniversary);
1351            builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
1352            operationList.add(builder.build());
1353        }
1354
1355        if (mSipSet != null && !mSipSet.isEmpty()) {
1356            for (String sipAddress : mSipSet) {
1357                builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1358                builder.withValueBackReference(Event.RAW_CONTACT_ID, 0);
1359                builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
1360                builder.withValue(SipAddress.SIP_ADDRESS, sipAddress);
1361                operationList.add(builder.build());
1362            }
1363        }
1364
1365        if (mAndroidCustomPropertyList != null) {
1366            for (List<String> customPropertyList : mAndroidCustomPropertyList) {
1367                int size = customPropertyList.size();
1368                if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) {
1369                    continue;
1370                } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) {
1371                    size = VCardConstants.MAX_DATA_COLUMN + 1;
1372                    customPropertyList =
1373                        customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2);
1374                }
1375
1376                int i = 0;
1377                for (final String customPropertyValue : customPropertyList) {
1378                    if (i == 0) {
1379                        final String mimeType = customPropertyValue;
1380                        builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1381                        builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
1382                        builder.withValue(Data.MIMETYPE, mimeType);
1383                    } else {  // 1 <= i && i <= MAX_DATA_COLUMNS
1384                        if (!TextUtils.isEmpty(customPropertyValue)) {
1385                            builder.withValue("data" + i, customPropertyValue);
1386                        }
1387                    }
1388
1389                    i++;
1390                }
1391                operationList.add(builder.build());
1392            }
1393        }
1394
1395        if (myGroupsId != null) {
1396            builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
1397            builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
1398            builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
1399            builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
1400            operationList.add(builder.build());
1401        }
1402
1403        try {
1404            ContentProviderResult[] results = resolver.applyBatch(
1405                        ContactsContract.AUTHORITY, operationList);
1406            // the first result is always the raw_contact. return it's uri so
1407            // that it can be found later. do null checking for badly behaving
1408            // ContentResolvers
1409            return (results == null || results.length == 0 || results[0] == null)
1410                ? null
1411                : results[0].uri;
1412        } catch (RemoteException e) {
1413            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
1414            return null;
1415        } catch (OperationApplicationException e) {
1416            Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
1417            return null;
1418        }
1419    }
1420
1421    public static VCardEntry buildFromResolver(ContentResolver resolver) {
1422        return buildFromResolver(resolver, Contacts.CONTENT_URI);
1423    }
1424
1425    public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
1426
1427        return null;
1428    }
1429
1430    private boolean nameFieldsAreEmpty() {
1431        return (TextUtils.isEmpty(mFamilyName)
1432                && TextUtils.isEmpty(mMiddleName)
1433                && TextUtils.isEmpty(mGivenName)
1434                && TextUtils.isEmpty(mPrefix)
1435                && TextUtils.isEmpty(mSuffix)
1436                && TextUtils.isEmpty(mFormattedName)
1437                && TextUtils.isEmpty(mPhoneticFamilyName)
1438                && TextUtils.isEmpty(mPhoneticMiddleName)
1439                && TextUtils.isEmpty(mPhoneticGivenName)
1440                && TextUtils.isEmpty(mPhoneticFullName));
1441    }
1442
1443    public boolean isIgnorable() {
1444        return getDisplayName().length() == 0;
1445    }
1446
1447    private String listToString(List<String> list){
1448        final int size = list.size();
1449        if (size > 1) {
1450            StringBuilder builder = new StringBuilder();
1451            int i = 0;
1452            for (String type : list) {
1453                builder.append(type);
1454                if (i < size - 1) {
1455                    builder.append(";");
1456                }
1457            }
1458            return builder.toString();
1459        } else if (size == 1) {
1460            return list.get(0);
1461        } else {
1462            return "";
1463        }
1464    }
1465
1466    // All getter methods should be used carefully, since they may change
1467    // in the future as of 2009-10-05, on which I cannot be sure this structure
1468    // is completely consolidated.
1469    //
1470    // Also note that these getter methods should be used only after
1471    // all properties being pushed into this object. If not, incorrect
1472    // value will "be stored in the local cache and" be returned to you.
1473
1474    public String getFamilyName() {
1475        return mFamilyName;
1476    }
1477
1478    public String getGivenName() {
1479        return mGivenName;
1480    }
1481
1482    public String getMiddleName() {
1483        return mMiddleName;
1484    }
1485
1486    public String getPrefix() {
1487        return mPrefix;
1488    }
1489
1490    public String getSuffix() {
1491        return mSuffix;
1492    }
1493
1494    public String getFullName() {
1495        return mFormattedName;
1496    }
1497
1498    public String getPhoneticFamilyName() {
1499        return mPhoneticFamilyName;
1500    }
1501
1502    public String getPhoneticGivenName() {
1503        return mPhoneticGivenName;
1504    }
1505
1506    public String getPhoneticMiddleName() {
1507        return mPhoneticMiddleName;
1508    }
1509
1510    public String getPhoneticFullName() {
1511        return mPhoneticFullName;
1512    }
1513
1514    public final List<String> getNickNameList() {
1515        return mNickNameList;
1516    }
1517
1518    public String getBirthday() {
1519        return mBirthday;
1520    }
1521
1522    public final List<String> getNotes() {
1523        return mNoteList;
1524    }
1525
1526    public final List<PhoneData> getPhoneList() {
1527        return mPhoneList;
1528    }
1529
1530    public final List<EmailData> getEmailList() {
1531        return mEmailList;
1532    }
1533
1534    public final List<PostalData> getPostalList() {
1535        return mPostalList;
1536    }
1537
1538    public final List<OrganizationData> getOrganizationList() {
1539        return mOrganizationList;
1540    }
1541
1542    public final List<ImData> getImList() {
1543        return mImList;
1544    }
1545
1546    public final List<PhotoData> getPhotoList() {
1547        return mPhotoList;
1548    }
1549
1550    public final List<String> getWebsiteList() {
1551        return mWebsiteList;
1552    }
1553
1554    public String getDisplayName() {
1555        if (mDisplayName == null) {
1556            constructDisplayName();
1557        }
1558        return mDisplayName;
1559    }
1560}
1561