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