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