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