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 
     17 package com.android.vcard;
     18 
     19 import com.android.vcard.VCardUtils.PhoneNumberUtilsPort;
     20 
     21 import android.accounts.Account;
     22 import android.content.ContentProviderOperation;
     23 import android.content.ContentResolver;
     24 import android.net.Uri;
     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.List;
     52 import java.util.Map;
     53 
     54 /**
     55  * Represents one vCard entry, which should start with "BEGIN:VCARD" and end
     56  * with "END:VCARD". This class is for bridging between real vCard data and
     57  * Android's {@link ContactsContract}, which means some aspects of vCard are
     58  * dropped before this object being constructed. Raw vCard data should be first
     59  * supplied with {@link #addProperty(VCardProperty)}. After supplying all data,
     60  * user should call {@link #consolidateFields()} to prepare some additional
     61  * information which is constructable from supplied raw data. TODO: preserve raw
     62  * data using {@link VCardProperty}. If it may just waste memory, this at least
     63  * should contain them when it cannot convert vCard as a string to Android's
     64  * Contacts representation. Those raw properties should _not_ be used for
     65  * {@link #isIgnorable()}.
     66  */
     67 public class VCardEntry {
     68     private static final String LOG_TAG = VCardConstants.LOG_TAG;
     69 
     70     private static final int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK;
     71 
     72     private static final Map<String, Integer> sImMap = new HashMap<String, Integer>();
     73 
     74     static {
     75         sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM);
     76         sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN);
     77         sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO);
     78         sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ);
     79         sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER);
     80         sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE);
     81         sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK);
     82         sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE,
     83                 Im.PROTOCOL_GOOGLE_TALK);
     84     }
     85 
     86     public enum EntryLabel {
     87         NAME,
     88         PHONE,
     89         EMAIL,
     90         POSTAL_ADDRESS,
     91         ORGANIZATION,
     92         IM,
     93         PHOTO,
     94         WEBSITE,
     95         SIP,
     96         NICKNAME,
     97         NOTE,
     98         BIRTHDAY,
     99         ANNIVERSARY,
    100         ANDROID_CUSTOM
    101     }
    102 
    103     public static interface EntryElement {
    104         // Also need to inherit toString(), equals().
    105         public EntryLabel getEntryLabel();
    106 
    107         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    108                 int backReferenceIndex);
    109 
    110         public boolean isEmpty();
    111     }
    112 
    113     // TODO: vCard 4.0 logically has multiple formatted names and we need to
    114     // select the most preferable one using PREF parameter.
    115     //
    116     // e.g. (based on rev.13)
    117     // FN;PREF=1:John M. Doe
    118     // FN;PREF=2:John Doe
    119     // FN;PREF=3;John
    120     public static class NameData implements EntryElement {
    121         private String mFamily;
    122         private String mGiven;
    123         private String mMiddle;
    124         private String mPrefix;
    125         private String mSuffix;
    126 
    127         // Used only when no family nor given name is found.
    128         private String mFormatted;
    129 
    130         private String mPhoneticFamily;
    131         private String mPhoneticGiven;
    132         private String mPhoneticMiddle;
    133 
    134         // For "SORT-STRING" in vCard 3.0.
    135         private String mSortString;
    136 
    137         /**
    138          * Not in vCard but for {@link StructuredName#DISPLAY_NAME}. This field
    139          * is constructed by VCardEntry on demand. Consider using
    140          * {@link VCardEntry#getDisplayName()}.
    141          */
    142         // This field should reflect the other Elem fields like Email,
    143         // PostalAddress, etc., while
    144         // This is static class which cannot see other data. Thus we ask
    145         // VCardEntry to populate it.
    146         public String displayName;
    147 
    148         public boolean emptyStructuredName() {
    149             return TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mGiven)
    150                     && TextUtils.isEmpty(mMiddle) && TextUtils.isEmpty(mPrefix)
    151                     && TextUtils.isEmpty(mSuffix);
    152         }
    153 
    154         public boolean emptyPhoneticStructuredName() {
    155             return TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticGiven)
    156                     && TextUtils.isEmpty(mPhoneticMiddle);
    157         }
    158 
    159         @Override
    160         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    161                 int backReferenceIndex) {
    162             final ContentProviderOperation.Builder builder = ContentProviderOperation
    163                     .newInsert(Data.CONTENT_URI);
    164             builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, backReferenceIndex);
    165             builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
    166 
    167             if (!TextUtils.isEmpty(mGiven)) {
    168                 builder.withValue(StructuredName.GIVEN_NAME, mGiven);
    169             }
    170             if (!TextUtils.isEmpty(mFamily)) {
    171                 builder.withValue(StructuredName.FAMILY_NAME, mFamily);
    172             }
    173             if (!TextUtils.isEmpty(mMiddle)) {
    174                 builder.withValue(StructuredName.MIDDLE_NAME, mMiddle);
    175             }
    176             if (!TextUtils.isEmpty(mPrefix)) {
    177                 builder.withValue(StructuredName.PREFIX, mPrefix);
    178             }
    179             if (!TextUtils.isEmpty(mSuffix)) {
    180                 builder.withValue(StructuredName.SUFFIX, mSuffix);
    181             }
    182 
    183             boolean phoneticNameSpecified = false;
    184 
    185             if (!TextUtils.isEmpty(mPhoneticGiven)) {
    186                 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGiven);
    187                 phoneticNameSpecified = true;
    188             }
    189             if (!TextUtils.isEmpty(mPhoneticFamily)) {
    190                 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamily);
    191                 phoneticNameSpecified = true;
    192             }
    193             if (!TextUtils.isEmpty(mPhoneticMiddle)) {
    194                 builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddle);
    195                 phoneticNameSpecified = true;
    196             }
    197 
    198             // SORT-STRING is used only when phonetic names aren't specified in
    199             // the original vCard.
    200             if (!phoneticNameSpecified) {
    201                 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mSortString);
    202             }
    203 
    204             builder.withValue(StructuredName.DISPLAY_NAME, displayName);
    205             operationList.add(builder.build());
    206         }
    207 
    208         @Override
    209         public boolean isEmpty() {
    210             return (TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mMiddle)
    211                     && TextUtils.isEmpty(mGiven) && TextUtils.isEmpty(mPrefix)
    212                     && TextUtils.isEmpty(mSuffix) && TextUtils.isEmpty(mFormatted)
    213                     && TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticMiddle)
    214                     && TextUtils.isEmpty(mPhoneticGiven) && TextUtils.isEmpty(mSortString));
    215         }
    216 
    217         @Override
    218         public boolean equals(Object obj) {
    219             if (this == obj) {
    220                 return true;
    221             }
    222             if (!(obj instanceof NameData)) {
    223                 return false;
    224             }
    225             NameData nameData = (NameData) obj;
    226 
    227             return (TextUtils.equals(mFamily, nameData.mFamily)
    228                     && TextUtils.equals(mMiddle, nameData.mMiddle)
    229                     && TextUtils.equals(mGiven, nameData.mGiven)
    230                     && TextUtils.equals(mPrefix, nameData.mPrefix)
    231                     && TextUtils.equals(mSuffix, nameData.mSuffix)
    232                     && TextUtils.equals(mFormatted, nameData.mFormatted)
    233                     && TextUtils.equals(mPhoneticFamily, nameData.mPhoneticFamily)
    234                     && TextUtils.equals(mPhoneticMiddle, nameData.mPhoneticMiddle)
    235                     && TextUtils.equals(mPhoneticGiven, nameData.mPhoneticGiven)
    236                     && TextUtils.equals(mSortString, nameData.mSortString));
    237         }
    238 
    239         @Override
    240         public int hashCode() {
    241             final String[] hashTargets = new String[] {mFamily, mMiddle, mGiven, mPrefix, mSuffix,
    242                     mFormatted, mPhoneticFamily, mPhoneticMiddle,
    243                     mPhoneticGiven, mSortString};
    244             int hash = 0;
    245             for (String hashTarget : hashTargets) {
    246                 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
    247             }
    248             return hash;
    249         }
    250 
    251         @Override
    252         public String toString() {
    253             return String.format("family: %s, given: %s, middle: %s, prefix: %s, suffix: %s",
    254                     mFamily, mGiven, mMiddle, mPrefix, mSuffix);
    255         }
    256 
    257         @Override
    258         public final EntryLabel getEntryLabel() {
    259             return EntryLabel.NAME;
    260         }
    261 
    262         public String getFamily() {
    263             return mFamily;
    264         }
    265 
    266         public String getMiddle() {
    267             return mMiddle;
    268         }
    269 
    270         public String getGiven() {
    271             return mGiven;
    272         }
    273 
    274         public String getPrefix() {
    275             return mPrefix;
    276         }
    277 
    278         public String getSuffix() {
    279             return mSuffix;
    280         }
    281 
    282         public String getFormatted() {
    283             return mFormatted;
    284         }
    285 
    286         public String getSortString() {
    287             return mSortString;
    288         }
    289 
    290         /** @hide Just for testing. */
    291         public void setFamily(String family) { mFamily = family; }
    292         /** @hide Just for testing. */
    293         public void setMiddle(String middle) { mMiddle = middle; }
    294         /** @hide Just for testing. */
    295         public void setGiven(String given) { mGiven = given; }
    296         /** @hide Just for testing. */
    297         public void setPrefix(String prefix) { mPrefix = prefix; }
    298         /** @hide Just for testing. */
    299         public void setSuffix(String suffix) { mSuffix = suffix; }
    300     }
    301 
    302     public static class PhoneData implements EntryElement {
    303         private final String mNumber;
    304         private final int mType;
    305         private final String mLabel;
    306 
    307         // isPrimary is (not final but) changable, only when there's no
    308         // appropriate one existing
    309         // in the original VCard.
    310         private boolean mIsPrimary;
    311 
    312         public PhoneData(String data, int type, String label, boolean isPrimary) {
    313             mNumber = data;
    314             mType = type;
    315             mLabel = label;
    316             mIsPrimary = isPrimary;
    317         }
    318 
    319         @Override
    320         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    321                 int backReferenceIndex) {
    322             final ContentProviderOperation.Builder builder = ContentProviderOperation
    323                     .newInsert(Data.CONTENT_URI);
    324             builder.withValueBackReference(Phone.RAW_CONTACT_ID, backReferenceIndex);
    325             builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
    326 
    327             builder.withValue(Phone.TYPE, mType);
    328             if (mType == Phone.TYPE_CUSTOM) {
    329                 builder.withValue(Phone.LABEL, mLabel);
    330             }
    331             builder.withValue(Phone.NUMBER, mNumber);
    332             if (mIsPrimary) {
    333                 builder.withValue(Phone.IS_PRIMARY, 1);
    334             }
    335             operationList.add(builder.build());
    336         }
    337 
    338         @Override
    339         public boolean isEmpty() {
    340             return TextUtils.isEmpty(mNumber);
    341         }
    342 
    343         @Override
    344         public boolean equals(Object obj) {
    345             if (this == obj) {
    346                 return true;
    347             }
    348             if (!(obj instanceof PhoneData)) {
    349                 return false;
    350             }
    351             PhoneData phoneData = (PhoneData) obj;
    352             return (mType == phoneData.mType
    353                     && TextUtils.equals(mNumber, phoneData.mNumber)
    354                     && TextUtils.equals(mLabel, phoneData.mLabel)
    355                     && (mIsPrimary == phoneData.mIsPrimary));
    356         }
    357 
    358         @Override
    359         public int hashCode() {
    360             int hash = mType;
    361             hash = hash * 31 + (mNumber != null ? mNumber.hashCode() : 0);
    362             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
    363             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
    364             return hash;
    365         }
    366 
    367         @Override
    368         public String toString() {
    369             return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mNumber,
    370                     mLabel, mIsPrimary);
    371         }
    372 
    373         @Override
    374         public final EntryLabel getEntryLabel() {
    375             return EntryLabel.PHONE;
    376         }
    377 
    378         public String getNumber() {
    379             return mNumber;
    380         }
    381 
    382         public int getType() {
    383             return mType;
    384         }
    385 
    386         public String getLabel() {
    387             return mLabel;
    388         }
    389 
    390         public boolean isPrimary() {
    391             return mIsPrimary;
    392         }
    393     }
    394 
    395     public static class EmailData implements EntryElement {
    396         private final String mAddress;
    397         private final int mType;
    398         // Used only when TYPE is TYPE_CUSTOM.
    399         private final String mLabel;
    400         private final boolean mIsPrimary;
    401 
    402         public EmailData(String data, int type, String label, boolean isPrimary) {
    403             mType = type;
    404             mAddress = data;
    405             mLabel = label;
    406             mIsPrimary = isPrimary;
    407         }
    408 
    409         @Override
    410         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    411                 int backReferenceIndex) {
    412             final ContentProviderOperation.Builder builder = ContentProviderOperation
    413                     .newInsert(Data.CONTENT_URI);
    414             builder.withValueBackReference(Email.RAW_CONTACT_ID, backReferenceIndex);
    415             builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
    416 
    417             builder.withValue(Email.TYPE, mType);
    418             if (mType == Email.TYPE_CUSTOM) {
    419                 builder.withValue(Email.LABEL, mLabel);
    420             }
    421             builder.withValue(Email.DATA, mAddress);
    422             if (mIsPrimary) {
    423                 builder.withValue(Data.IS_PRIMARY, 1);
    424             }
    425             operationList.add(builder.build());
    426         }
    427 
    428         @Override
    429         public boolean isEmpty() {
    430             return TextUtils.isEmpty(mAddress);
    431         }
    432 
    433         @Override
    434         public boolean equals(Object obj) {
    435             if (this == obj) {
    436                 return true;
    437             }
    438             if (!(obj instanceof EmailData)) {
    439                 return false;
    440             }
    441             EmailData emailData = (EmailData) obj;
    442             return (mType == emailData.mType
    443                     && TextUtils.equals(mAddress, emailData.mAddress)
    444                     && TextUtils.equals(mLabel, emailData.mLabel)
    445                     && (mIsPrimary == emailData.mIsPrimary));
    446         }
    447 
    448         @Override
    449         public int hashCode() {
    450             int hash = mType;
    451             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
    452             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
    453             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
    454             return hash;
    455         }
    456 
    457         @Override
    458         public String toString() {
    459             return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mAddress,
    460                     mLabel, mIsPrimary);
    461         }
    462 
    463         @Override
    464         public final EntryLabel getEntryLabel() {
    465             return EntryLabel.EMAIL;
    466         }
    467 
    468         public String getAddress() {
    469             return mAddress;
    470         }
    471 
    472         public int getType() {
    473             return mType;
    474         }
    475 
    476         public String getLabel() {
    477             return mLabel;
    478         }
    479 
    480         public boolean isPrimary() {
    481             return mIsPrimary;
    482         }
    483     }
    484 
    485     public static class PostalData implements EntryElement {
    486         // Determined by vCard specification.
    487         // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name
    488         private static final int ADDR_MAX_DATA_SIZE = 7;
    489         private final String mPobox;
    490         private final String mExtendedAddress;
    491         private final String mStreet;
    492         private final String mLocalty;
    493         private final String mRegion;
    494         private final String mPostalCode;
    495         private final String mCountry;
    496         private final int mType;
    497         private final String mLabel;
    498         private boolean mIsPrimary;
    499 
    500         /** We keep this for {@link StructuredPostal#FORMATTED_ADDRESS} */
    501         // TODO: need better way to construct formatted address.
    502         private int mVCardType;
    503 
    504         public PostalData(String pobox, String extendedAddress, String street, String localty,
    505                 String region, String postalCode, String country, int type, String label,
    506                 boolean isPrimary, int vcardType) {
    507             mType = type;
    508             mPobox = pobox;
    509             mExtendedAddress = extendedAddress;
    510             mStreet = street;
    511             mLocalty = localty;
    512             mRegion = region;
    513             mPostalCode = postalCode;
    514             mCountry = country;
    515             mLabel = label;
    516             mIsPrimary = isPrimary;
    517             mVCardType = vcardType;
    518         }
    519 
    520         /**
    521          * Accepts raw propertyValueList in vCard and constructs PostalData.
    522          */
    523         public static PostalData constructPostalData(final List<String> propValueList,
    524                 final int type, final String label, boolean isPrimary, int vcardType) {
    525             final String[] dataArray = new String[ADDR_MAX_DATA_SIZE];
    526 
    527             int size = propValueList.size();
    528             if (size > ADDR_MAX_DATA_SIZE) {
    529                 size = ADDR_MAX_DATA_SIZE;
    530             }
    531 
    532             // adr-value = 0*6(text-value ";") text-value
    533             // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name
    534             //
    535             // Use Iterator assuming List may be LinkedList, though actually it is
    536             // always ArrayList in the current implementation.
    537             int i = 0;
    538             for (String addressElement : propValueList) {
    539                 dataArray[i] = addressElement;
    540                 if (++i >= size) {
    541                     break;
    542                 }
    543             }
    544             while (i < ADDR_MAX_DATA_SIZE) {
    545                 dataArray[i++] = null;
    546             }
    547 
    548             return new PostalData(dataArray[0], dataArray[1], dataArray[2], dataArray[3],
    549                     dataArray[4], dataArray[5], dataArray[6], type, label, isPrimary, vcardType);
    550         }
    551 
    552         @Override
    553         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    554                 int backReferenceIndex) {
    555             final ContentProviderOperation.Builder builder = ContentProviderOperation
    556                     .newInsert(Data.CONTENT_URI);
    557             builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, backReferenceIndex);
    558             builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE);
    559 
    560             builder.withValue(StructuredPostal.TYPE, mType);
    561             if (mType == StructuredPostal.TYPE_CUSTOM) {
    562                 builder.withValue(StructuredPostal.LABEL, mLabel);
    563             }
    564 
    565             final String streetString;
    566             if (TextUtils.isEmpty(mStreet)) {
    567                 if (TextUtils.isEmpty(mExtendedAddress)) {
    568                     streetString = null;
    569                 } else {
    570                     streetString = mExtendedAddress;
    571                 }
    572             } else {
    573                 if (TextUtils.isEmpty(mExtendedAddress)) {
    574                     streetString = mStreet;
    575                 } else {
    576                     streetString = mStreet + " " + mExtendedAddress;
    577                 }
    578             }
    579             builder.withValue(StructuredPostal.POBOX, mPobox);
    580             builder.withValue(StructuredPostal.STREET, streetString);
    581             builder.withValue(StructuredPostal.CITY, mLocalty);
    582             builder.withValue(StructuredPostal.REGION, mRegion);
    583             builder.withValue(StructuredPostal.POSTCODE, mPostalCode);
    584             builder.withValue(StructuredPostal.COUNTRY, mCountry);
    585 
    586             builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress(mVCardType));
    587             if (mIsPrimary) {
    588                 builder.withValue(Data.IS_PRIMARY, 1);
    589             }
    590             operationList.add(builder.build());
    591         }
    592 
    593         public String getFormattedAddress(final int vcardType) {
    594             StringBuilder builder = new StringBuilder();
    595             boolean empty = true;
    596             final String[] dataArray = new String[] {
    597                     mPobox, mExtendedAddress, mStreet, mLocalty, mRegion, mPostalCode, mCountry
    598             };
    599             if (VCardConfig.isJapaneseDevice(vcardType)) {
    600                 // In Japan, the order is reversed.
    601                 for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) {
    602                     String addressPart = dataArray[i];
    603                     if (!TextUtils.isEmpty(addressPart)) {
    604                         if (!empty) {
    605                             builder.append(' ');
    606                         } else {
    607                             empty = false;
    608                         }
    609                         builder.append(addressPart);
    610                     }
    611                 }
    612             } else {
    613                 for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) {
    614                     String addressPart = dataArray[i];
    615                     if (!TextUtils.isEmpty(addressPart)) {
    616                         if (!empty) {
    617                             builder.append(' ');
    618                         } else {
    619                             empty = false;
    620                         }
    621                         builder.append(addressPart);
    622                     }
    623                 }
    624             }
    625 
    626             return builder.toString().trim();
    627         }
    628 
    629         @Override
    630         public boolean isEmpty() {
    631             return (TextUtils.isEmpty(mPobox)
    632                     && TextUtils.isEmpty(mExtendedAddress)
    633                     && TextUtils.isEmpty(mStreet)
    634                     && TextUtils.isEmpty(mLocalty)
    635                     && TextUtils.isEmpty(mRegion)
    636                     && TextUtils.isEmpty(mPostalCode)
    637                     && TextUtils.isEmpty(mCountry));
    638         }
    639 
    640         @Override
    641         public boolean equals(Object obj) {
    642             if (this == obj) {
    643                 return true;
    644             }
    645             if (!(obj instanceof PostalData)) {
    646                 return false;
    647             }
    648             final PostalData postalData = (PostalData) obj;
    649             return (mType == postalData.mType)
    650                     && (mType == StructuredPostal.TYPE_CUSTOM ? TextUtils.equals(mLabel,
    651                             postalData.mLabel) : true)
    652                     && (mIsPrimary == postalData.mIsPrimary)
    653                     && TextUtils.equals(mPobox, postalData.mPobox)
    654                     && TextUtils.equals(mExtendedAddress, postalData.mExtendedAddress)
    655                     && TextUtils.equals(mStreet, postalData.mStreet)
    656                     && TextUtils.equals(mLocalty, postalData.mLocalty)
    657                     && TextUtils.equals(mRegion, postalData.mRegion)
    658                     && TextUtils.equals(mPostalCode, postalData.mPostalCode)
    659                     && TextUtils.equals(mCountry, postalData.mCountry);
    660         }
    661 
    662         @Override
    663         public int hashCode() {
    664             int hash = mType;
    665             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
    666             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
    667 
    668             final String[] hashTargets = new String[] {mPobox, mExtendedAddress, mStreet,
    669                     mLocalty, mRegion, mPostalCode, mCountry};
    670             for (String hashTarget : hashTargets) {
    671                 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0);
    672             }
    673             return hash;
    674         }
    675 
    676         @Override
    677         public String toString() {
    678             return String.format("type: %d, label: %s, isPrimary: %s, pobox: %s, "
    679                     + "extendedAddress: %s, street: %s, localty: %s, region: %s, postalCode %s, "
    680                     + "country: %s", mType, mLabel, mIsPrimary, mPobox, mExtendedAddress, mStreet,
    681                     mLocalty, mRegion, mPostalCode, mCountry);
    682         }
    683 
    684         @Override
    685         public final EntryLabel getEntryLabel() {
    686             return EntryLabel.POSTAL_ADDRESS;
    687         }
    688 
    689         public String getPobox() {
    690             return mPobox;
    691         }
    692 
    693         public String getExtendedAddress() {
    694             return mExtendedAddress;
    695         }
    696 
    697         public String getStreet() {
    698             return mStreet;
    699         }
    700 
    701         public String getLocalty() {
    702             return mLocalty;
    703         }
    704 
    705         public String getRegion() {
    706             return mRegion;
    707         }
    708 
    709         public String getPostalCode() {
    710             return mPostalCode;
    711         }
    712 
    713         public String getCountry() {
    714             return mCountry;
    715         }
    716 
    717         public int getType() {
    718             return mType;
    719         }
    720 
    721         public String getLabel() {
    722             return mLabel;
    723         }
    724 
    725         public boolean isPrimary() {
    726             return mIsPrimary;
    727         }
    728     }
    729 
    730     public static class OrganizationData implements EntryElement {
    731         // non-final is Intentional: we may change the values since this info is separated into
    732         // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in different
    733         // timing.
    734         private String mOrganizationName;
    735         private String mDepartmentName;
    736         private String mTitle;
    737         private final String mPhoneticName; // We won't have this in "TITLE" property.
    738         private final int mType;
    739         private boolean mIsPrimary;
    740 
    741         public OrganizationData(final String organizationName, final String departmentName,
    742                 final String titleName, final String phoneticName, int type,
    743                 final boolean isPrimary) {
    744             mType = type;
    745             mOrganizationName = organizationName;
    746             mDepartmentName = departmentName;
    747             mTitle = titleName;
    748             mPhoneticName = phoneticName;
    749             mIsPrimary = isPrimary;
    750         }
    751 
    752         public String getFormattedString() {
    753             final StringBuilder builder = new StringBuilder();
    754             if (!TextUtils.isEmpty(mOrganizationName)) {
    755                 builder.append(mOrganizationName);
    756             }
    757 
    758             if (!TextUtils.isEmpty(mDepartmentName)) {
    759                 if (builder.length() > 0) {
    760                     builder.append(", ");
    761                 }
    762                 builder.append(mDepartmentName);
    763             }
    764 
    765             if (!TextUtils.isEmpty(mTitle)) {
    766                 if (builder.length() > 0) {
    767                     builder.append(", ");
    768                 }
    769                 builder.append(mTitle);
    770             }
    771 
    772             return builder.toString();
    773         }
    774 
    775         @Override
    776         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    777                 int backReferenceIndex) {
    778             final ContentProviderOperation.Builder builder = ContentProviderOperation
    779                     .newInsert(Data.CONTENT_URI);
    780             builder.withValueBackReference(Organization.RAW_CONTACT_ID, backReferenceIndex);
    781             builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE);
    782             builder.withValue(Organization.TYPE, mType);
    783             if (mOrganizationName != null) {
    784                 builder.withValue(Organization.COMPANY, mOrganizationName);
    785             }
    786             if (mDepartmentName != null) {
    787                 builder.withValue(Organization.DEPARTMENT, mDepartmentName);
    788             }
    789             if (mTitle != null) {
    790                 builder.withValue(Organization.TITLE, mTitle);
    791             }
    792             if (mPhoneticName != null) {
    793                 builder.withValue(Organization.PHONETIC_NAME, mPhoneticName);
    794             }
    795             if (mIsPrimary) {
    796                 builder.withValue(Organization.IS_PRIMARY, 1);
    797             }
    798             operationList.add(builder.build());
    799         }
    800 
    801         @Override
    802         public boolean isEmpty() {
    803             return TextUtils.isEmpty(mOrganizationName) && TextUtils.isEmpty(mDepartmentName)
    804                     && TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mPhoneticName);
    805         }
    806 
    807         @Override
    808         public boolean equals(Object obj) {
    809             if (this == obj) {
    810                 return true;
    811             }
    812             if (!(obj instanceof OrganizationData)) {
    813                 return false;
    814             }
    815             OrganizationData organization = (OrganizationData) obj;
    816             return (mType == organization.mType
    817                     && TextUtils.equals(mOrganizationName, organization.mOrganizationName)
    818                     && TextUtils.equals(mDepartmentName, organization.mDepartmentName)
    819                     && TextUtils.equals(mTitle, organization.mTitle)
    820                     && (mIsPrimary == organization.mIsPrimary));
    821         }
    822 
    823         @Override
    824         public int hashCode() {
    825             int hash = mType;
    826             hash = hash * 31 + (mOrganizationName != null ? mOrganizationName.hashCode() : 0);
    827             hash = hash * 31 + (mDepartmentName != null ? mDepartmentName.hashCode() : 0);
    828             hash = hash * 31 + (mTitle != null ? mTitle.hashCode() : 0);
    829             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
    830             return hash;
    831         }
    832 
    833         @Override
    834         public String toString() {
    835             return String.format(
    836                     "type: %d, organization: %s, department: %s, title: %s, isPrimary: %s", mType,
    837                     mOrganizationName, mDepartmentName, mTitle, mIsPrimary);
    838         }
    839 
    840         @Override
    841         public final EntryLabel getEntryLabel() {
    842             return EntryLabel.ORGANIZATION;
    843         }
    844 
    845         public String getOrganizationName() {
    846             return mOrganizationName;
    847         }
    848 
    849         public String getDepartmentName() {
    850             return mDepartmentName;
    851         }
    852 
    853         public String getTitle() {
    854             return mTitle;
    855         }
    856 
    857         public String getPhoneticName() {
    858             return mPhoneticName;
    859         }
    860 
    861         public int getType() {
    862             return mType;
    863         }
    864 
    865         public boolean isPrimary() {
    866             return mIsPrimary;
    867         }
    868     }
    869 
    870     public static class ImData implements EntryElement {
    871         private final String mAddress;
    872         private final int mProtocol;
    873         private final String mCustomProtocol;
    874         private final int mType;
    875         private final boolean mIsPrimary;
    876 
    877         public ImData(final int protocol, final String customProtocol, final String address,
    878                 final int type, final boolean isPrimary) {
    879             mProtocol = protocol;
    880             mCustomProtocol = customProtocol;
    881             mType = type;
    882             mAddress = address;
    883             mIsPrimary = isPrimary;
    884         }
    885 
    886         @Override
    887         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    888                 int backReferenceIndex) {
    889             final ContentProviderOperation.Builder builder = ContentProviderOperation
    890                     .newInsert(Data.CONTENT_URI);
    891             builder.withValueBackReference(Im.RAW_CONTACT_ID, backReferenceIndex);
    892             builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE);
    893             builder.withValue(Im.TYPE, mType);
    894             builder.withValue(Im.PROTOCOL, mProtocol);
    895             builder.withValue(Im.DATA, mAddress);
    896             if (mProtocol == Im.PROTOCOL_CUSTOM) {
    897                 builder.withValue(Im.CUSTOM_PROTOCOL, mCustomProtocol);
    898             }
    899             if (mIsPrimary) {
    900                 builder.withValue(Data.IS_PRIMARY, 1);
    901             }
    902             operationList.add(builder.build());
    903         }
    904 
    905         @Override
    906         public boolean isEmpty() {
    907             return TextUtils.isEmpty(mAddress);
    908         }
    909 
    910         @Override
    911         public boolean equals(Object obj) {
    912             if (this == obj) {
    913                 return true;
    914             }
    915             if (!(obj instanceof ImData)) {
    916                 return false;
    917             }
    918             ImData imData = (ImData) obj;
    919             return (mType == imData.mType
    920                     && mProtocol == imData.mProtocol
    921                     && TextUtils.equals(mCustomProtocol, imData.mCustomProtocol)
    922                     && TextUtils.equals(mAddress, imData.mAddress)
    923                     && (mIsPrimary == imData.mIsPrimary));
    924         }
    925 
    926         @Override
    927         public int hashCode() {
    928             int hash = mType;
    929             hash = hash * 31 + mProtocol;
    930             hash = hash * 31 + (mCustomProtocol != null ? mCustomProtocol.hashCode() : 0);
    931             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
    932             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
    933             return hash;
    934         }
    935 
    936         @Override
    937         public String toString() {
    938             return String.format(
    939                     "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", mType,
    940                     mProtocol, mCustomProtocol, mAddress, mIsPrimary);
    941         }
    942 
    943         @Override
    944         public final EntryLabel getEntryLabel() {
    945             return EntryLabel.IM;
    946         }
    947 
    948         public String getAddress() {
    949             return mAddress;
    950         }
    951 
    952         /**
    953          * One of the value available for {@link Im#PROTOCOL}. e.g.
    954          * {@link Im#PROTOCOL_GOOGLE_TALK}
    955          */
    956         public int getProtocol() {
    957             return mProtocol;
    958         }
    959 
    960         public String getCustomProtocol() {
    961             return mCustomProtocol;
    962         }
    963 
    964         public int getType() {
    965             return mType;
    966         }
    967 
    968         public boolean isPrimary() {
    969             return mIsPrimary;
    970         }
    971     }
    972 
    973     public static class PhotoData implements EntryElement {
    974         // private static final String FORMAT_FLASH = "SWF";
    975 
    976         // used when type is not defined in ContactsContract.
    977         private final String mFormat;
    978         private final boolean mIsPrimary;
    979 
    980         private final byte[] mBytes;
    981 
    982         private Integer mHashCode = null;
    983 
    984         public PhotoData(String format, byte[] photoBytes, boolean isPrimary) {
    985             mFormat = format;
    986             mBytes = photoBytes;
    987             mIsPrimary = isPrimary;
    988         }
    989 
    990         @Override
    991         public void constructInsertOperation(List<ContentProviderOperation> operationList,
    992                 int backReferenceIndex) {
    993             final ContentProviderOperation.Builder builder = ContentProviderOperation
    994                     .newInsert(Data.CONTENT_URI);
    995             builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex);
    996             builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
    997             builder.withValue(Photo.PHOTO, mBytes);
    998             if (mIsPrimary) {
    999                 builder.withValue(Photo.IS_PRIMARY, 1);
   1000             }
   1001             operationList.add(builder.build());
   1002         }
   1003 
   1004         @Override
   1005         public boolean isEmpty() {
   1006             return mBytes == null || mBytes.length == 0;
   1007         }
   1008 
   1009         @Override
   1010         public boolean equals(Object obj) {
   1011             if (this == obj) {
   1012                 return true;
   1013             }
   1014             if (!(obj instanceof PhotoData)) {
   1015                 return false;
   1016             }
   1017             PhotoData photoData = (PhotoData) obj;
   1018             return (TextUtils.equals(mFormat, photoData.mFormat)
   1019                     && Arrays.equals(mBytes, photoData.mBytes)
   1020                     && (mIsPrimary == photoData.mIsPrimary));
   1021         }
   1022 
   1023         @Override
   1024         public int hashCode() {
   1025             if (mHashCode != null) {
   1026                 return mHashCode;
   1027             }
   1028 
   1029             int hash = mFormat != null ? mFormat.hashCode() : 0;
   1030             hash = hash * 31;
   1031             if (mBytes != null) {
   1032                 for (byte b : mBytes) {
   1033                     hash += b;
   1034                 }
   1035             }
   1036 
   1037             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
   1038             mHashCode = hash;
   1039             return hash;
   1040         }
   1041 
   1042         @Override
   1043         public String toString() {
   1044             return String.format("format: %s: size: %d, isPrimary: %s", mFormat, mBytes.length,
   1045                     mIsPrimary);
   1046         }
   1047 
   1048         @Override
   1049         public final EntryLabel getEntryLabel() {
   1050             return EntryLabel.PHOTO;
   1051         }
   1052 
   1053         public String getFormat() {
   1054             return mFormat;
   1055         }
   1056 
   1057         public byte[] getBytes() {
   1058             return mBytes;
   1059         }
   1060 
   1061         public boolean isPrimary() {
   1062             return mIsPrimary;
   1063         }
   1064     }
   1065 
   1066     public static class NicknameData implements EntryElement {
   1067         private final String mNickname;
   1068 
   1069         public NicknameData(String nickname) {
   1070             mNickname = nickname;
   1071         }
   1072 
   1073         @Override
   1074         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1075                 int backReferenceIndex) {
   1076             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1077                     .newInsert(Data.CONTENT_URI);
   1078             builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex);
   1079             builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE);
   1080             builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT);
   1081             builder.withValue(Nickname.NAME, mNickname);
   1082             operationList.add(builder.build());
   1083         }
   1084 
   1085         @Override
   1086         public boolean isEmpty() {
   1087             return TextUtils.isEmpty(mNickname);
   1088         }
   1089 
   1090         @Override
   1091         public boolean equals(Object obj) {
   1092             if (!(obj instanceof NicknameData)) {
   1093                 return false;
   1094             }
   1095             NicknameData nicknameData = (NicknameData) obj;
   1096             return TextUtils.equals(mNickname, nicknameData.mNickname);
   1097         }
   1098 
   1099         @Override
   1100         public int hashCode() {
   1101             return mNickname != null ? mNickname.hashCode() : 0;
   1102         }
   1103 
   1104         @Override
   1105         public String toString() {
   1106             return "nickname: " + mNickname;
   1107         }
   1108 
   1109         @Override
   1110         public EntryLabel getEntryLabel() {
   1111             return EntryLabel.NICKNAME;
   1112         }
   1113 
   1114         public String getNickname() {
   1115             return mNickname;
   1116         }
   1117     }
   1118 
   1119     public static class NoteData implements EntryElement {
   1120         public final String mNote;
   1121 
   1122         public NoteData(String note) {
   1123             mNote = note;
   1124         }
   1125 
   1126         @Override
   1127         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1128                 int backReferenceIndex) {
   1129             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1130                     .newInsert(Data.CONTENT_URI);
   1131             builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex);
   1132             builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE);
   1133             builder.withValue(Note.NOTE, mNote);
   1134             operationList.add(builder.build());
   1135         }
   1136 
   1137         @Override
   1138         public boolean isEmpty() {
   1139             return TextUtils.isEmpty(mNote);
   1140         }
   1141 
   1142         @Override
   1143         public boolean equals(Object obj) {
   1144             if (this == obj) {
   1145                 return true;
   1146             }
   1147             if (!(obj instanceof NoteData)) {
   1148                 return false;
   1149             }
   1150             NoteData noteData = (NoteData) obj;
   1151             return TextUtils.equals(mNote, noteData.mNote);
   1152         }
   1153 
   1154         @Override
   1155         public int hashCode() {
   1156             return mNote != null ? mNote.hashCode() : 0;
   1157         }
   1158 
   1159         @Override
   1160         public String toString() {
   1161             return "note: " + mNote;
   1162         }
   1163 
   1164         @Override
   1165         public EntryLabel getEntryLabel() {
   1166             return EntryLabel.NOTE;
   1167         }
   1168 
   1169         public String getNote() {
   1170             return mNote;
   1171         }
   1172     }
   1173 
   1174     public static class WebsiteData implements EntryElement {
   1175         private final String mWebsite;
   1176 
   1177         public WebsiteData(String website) {
   1178             mWebsite = website;
   1179         }
   1180 
   1181         @Override
   1182         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1183                 int backReferenceIndex) {
   1184             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1185                     .newInsert(Data.CONTENT_URI);
   1186             builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex);
   1187             builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE);
   1188             builder.withValue(Website.URL, mWebsite);
   1189             // There's no information about the type of URL in vCard.
   1190             // We use TYPE_HOMEPAGE for safety.
   1191             builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE);
   1192             operationList.add(builder.build());
   1193         }
   1194 
   1195         @Override
   1196         public boolean isEmpty() {
   1197             return TextUtils.isEmpty(mWebsite);
   1198         }
   1199 
   1200         @Override
   1201         public boolean equals(Object obj) {
   1202             if (this == obj) {
   1203                 return true;
   1204             }
   1205             if (!(obj instanceof WebsiteData)) {
   1206                 return false;
   1207             }
   1208             WebsiteData websiteData = (WebsiteData) obj;
   1209             return TextUtils.equals(mWebsite, websiteData.mWebsite);
   1210         }
   1211 
   1212         @Override
   1213         public int hashCode() {
   1214             return mWebsite != null ? mWebsite.hashCode() : 0;
   1215         }
   1216 
   1217         @Override
   1218         public String toString() {
   1219             return "website: " + mWebsite;
   1220         }
   1221 
   1222         @Override
   1223         public EntryLabel getEntryLabel() {
   1224             return EntryLabel.WEBSITE;
   1225         }
   1226 
   1227         public String getWebsite() {
   1228             return mWebsite;
   1229         }
   1230     }
   1231 
   1232     public static class BirthdayData implements EntryElement {
   1233         private final String mBirthday;
   1234 
   1235         public BirthdayData(String birthday) {
   1236             mBirthday = birthday;
   1237         }
   1238 
   1239         @Override
   1240         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1241                 int backReferenceIndex) {
   1242             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1243                     .newInsert(Data.CONTENT_URI);
   1244             builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
   1245             builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
   1246             builder.withValue(Event.START_DATE, mBirthday);
   1247             builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY);
   1248             operationList.add(builder.build());
   1249         }
   1250 
   1251         @Override
   1252         public boolean isEmpty() {
   1253             return TextUtils.isEmpty(mBirthday);
   1254         }
   1255 
   1256         @Override
   1257         public boolean equals(Object obj) {
   1258             if (this == obj) {
   1259                 return true;
   1260             }
   1261             if (!(obj instanceof BirthdayData)) {
   1262                 return false;
   1263             }
   1264             BirthdayData birthdayData = (BirthdayData) obj;
   1265             return TextUtils.equals(mBirthday, birthdayData.mBirthday);
   1266         }
   1267 
   1268         @Override
   1269         public int hashCode() {
   1270             return mBirthday != null ? mBirthday.hashCode() : 0;
   1271         }
   1272 
   1273         @Override
   1274         public String toString() {
   1275             return "birthday: " + mBirthday;
   1276         }
   1277 
   1278         @Override
   1279         public EntryLabel getEntryLabel() {
   1280             return EntryLabel.BIRTHDAY;
   1281         }
   1282 
   1283         public String getBirthday() {
   1284             return mBirthday;
   1285         }
   1286     }
   1287 
   1288     public static class AnniversaryData implements EntryElement {
   1289         private final String mAnniversary;
   1290 
   1291         public AnniversaryData(String anniversary) {
   1292             mAnniversary = anniversary;
   1293         }
   1294 
   1295         @Override
   1296         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1297                 int backReferenceIndex) {
   1298             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1299                     .newInsert(Data.CONTENT_URI);
   1300             builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex);
   1301             builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE);
   1302             builder.withValue(Event.START_DATE, mAnniversary);
   1303             builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY);
   1304             operationList.add(builder.build());
   1305         }
   1306 
   1307         @Override
   1308         public boolean isEmpty() {
   1309             return TextUtils.isEmpty(mAnniversary);
   1310         }
   1311 
   1312         @Override
   1313         public boolean equals(Object obj) {
   1314             if (this == obj) {
   1315                 return true;
   1316             }
   1317             if (!(obj instanceof AnniversaryData)) {
   1318                 return false;
   1319             }
   1320             AnniversaryData anniversaryData = (AnniversaryData) obj;
   1321             return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary);
   1322         }
   1323 
   1324         @Override
   1325         public int hashCode() {
   1326             return mAnniversary != null ? mAnniversary.hashCode() : 0;
   1327         }
   1328 
   1329         @Override
   1330         public String toString() {
   1331             return "anniversary: " + mAnniversary;
   1332         }
   1333 
   1334         @Override
   1335         public EntryLabel getEntryLabel() {
   1336             return EntryLabel.ANNIVERSARY;
   1337         }
   1338 
   1339         public String getAnniversary() { return mAnniversary; }
   1340     }
   1341 
   1342     public static class SipData implements EntryElement {
   1343         /**
   1344          * Note that schema part ("sip:") is automatically removed. e.g.
   1345          * "sip:username:password@host:port" becomes
   1346          * "username:password@host:port"
   1347          */
   1348         private final String mAddress;
   1349         private final int mType;
   1350         private final String mLabel;
   1351         private final boolean mIsPrimary;
   1352 
   1353         public SipData(String rawSip, int type, String label, boolean isPrimary) {
   1354             if (rawSip.startsWith("sip:")) {
   1355                 mAddress = rawSip.substring(4);
   1356             } else {
   1357                 mAddress = rawSip;
   1358             }
   1359             mType = type;
   1360             mLabel = label;
   1361             mIsPrimary = isPrimary;
   1362         }
   1363 
   1364         @Override
   1365         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1366                 int backReferenceIndex) {
   1367             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1368                     .newInsert(Data.CONTENT_URI);
   1369             builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex);
   1370             builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE);
   1371             builder.withValue(SipAddress.SIP_ADDRESS, mAddress);
   1372             builder.withValue(SipAddress.TYPE, mType);
   1373             if (mType == SipAddress.TYPE_CUSTOM) {
   1374                 builder.withValue(SipAddress.LABEL, mLabel);
   1375             }
   1376             if (mIsPrimary) {
   1377                 builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary);
   1378             }
   1379             operationList.add(builder.build());
   1380         }
   1381 
   1382         @Override
   1383         public boolean isEmpty() {
   1384             return TextUtils.isEmpty(mAddress);
   1385         }
   1386 
   1387         @Override
   1388         public boolean equals(Object obj) {
   1389             if (this == obj) {
   1390                 return true;
   1391             }
   1392             if (!(obj instanceof SipData)) {
   1393                 return false;
   1394             }
   1395             SipData sipData = (SipData) obj;
   1396             return (mType == sipData.mType
   1397                     && TextUtils.equals(mLabel, sipData.mLabel)
   1398                     && TextUtils.equals(mAddress, sipData.mAddress)
   1399                     && (mIsPrimary == sipData.mIsPrimary));
   1400         }
   1401 
   1402         @Override
   1403         public int hashCode() {
   1404             int hash = mType;
   1405             hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0);
   1406             hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0);
   1407             hash = hash * 31 + (mIsPrimary ? 1231 : 1237);
   1408             return hash;
   1409         }
   1410 
   1411         @Override
   1412         public String toString() {
   1413             return "sip: " + mAddress;
   1414         }
   1415 
   1416         @Override
   1417         public EntryLabel getEntryLabel() {
   1418             return EntryLabel.SIP;
   1419         }
   1420 
   1421         /**
   1422          * @return Address part of the sip data. The schema ("sip:") isn't contained here.
   1423          */
   1424         public String getAddress() { return mAddress; }
   1425         public int getType() { return mType; }
   1426         public String getLabel() { return mLabel; }
   1427     }
   1428 
   1429     /**
   1430      * Some Contacts data in Android cannot be converted to vCard
   1431      * representation. VCardEntry preserves those data using this class.
   1432      */
   1433     public static class AndroidCustomData implements EntryElement {
   1434         private final String mMimeType;
   1435 
   1436         private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN
   1437 
   1438         public AndroidCustomData(String mimeType, List<String> dataList) {
   1439             mMimeType = mimeType;
   1440             mDataList = dataList;
   1441         }
   1442 
   1443         public static AndroidCustomData constructAndroidCustomData(List<String> list) {
   1444             String mimeType;
   1445             List<String> dataList;
   1446 
   1447             if (list == null) {
   1448                 mimeType = null;
   1449                 dataList = null;
   1450             } else if (list.size() < 2) {
   1451                 mimeType = list.get(0);
   1452                 dataList = null;
   1453             } else {
   1454                 final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size()
   1455                         : VCardConstants.MAX_DATA_COLUMN + 1;
   1456                 mimeType = list.get(0);
   1457                 dataList = list.subList(1, max);
   1458             }
   1459 
   1460             return new AndroidCustomData(mimeType, dataList);
   1461         }
   1462 
   1463         @Override
   1464         public void constructInsertOperation(List<ContentProviderOperation> operationList,
   1465                 int backReferenceIndex) {
   1466             final ContentProviderOperation.Builder builder = ContentProviderOperation
   1467                     .newInsert(Data.CONTENT_URI);
   1468             builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex);
   1469             builder.withValue(Data.MIMETYPE, mMimeType);
   1470             for (int i = 0; i < mDataList.size(); i++) {
   1471                 String value = mDataList.get(i);
   1472                 if (!TextUtils.isEmpty(value)) {
   1473                     // 1-origin
   1474                     builder.withValue("data" + (i + 1), value);
   1475                 }
   1476             }
   1477             operationList.add(builder.build());
   1478         }
   1479 
   1480         @Override
   1481         public boolean isEmpty() {
   1482             return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0;
   1483         }
   1484 
   1485         @Override
   1486         public boolean equals(Object obj) {
   1487             if (this == obj) {
   1488                 return true;
   1489             }
   1490             if (!(obj instanceof AndroidCustomData)) {
   1491                 return false;
   1492             }
   1493             AndroidCustomData data = (AndroidCustomData) obj;
   1494             if (!TextUtils.equals(mMimeType, data.mMimeType)) {
   1495                 return false;
   1496             }
   1497             if (mDataList == null) {
   1498                 return data.mDataList == null;
   1499             } else {
   1500                 final int size = mDataList.size();
   1501                 if (size != data.mDataList.size()) {
   1502                     return false;
   1503                 }
   1504                 for (int i = 0; i < size; i++) {
   1505                     if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) {
   1506                         return false;
   1507                     }
   1508                 }
   1509                 return true;
   1510             }
   1511         }
   1512 
   1513         @Override
   1514         public int hashCode() {
   1515             int hash = mMimeType != null ? mMimeType.hashCode() : 0;
   1516             if (mDataList != null) {
   1517                 for (String data : mDataList) {
   1518                     hash = hash * 31 + (data != null ? data.hashCode() : 0);
   1519                 }
   1520             }
   1521             return hash;
   1522         }
   1523 
   1524         @Override
   1525         public String toString() {
   1526             final StringBuilder builder = new StringBuilder();
   1527             builder.append("android-custom: " + mMimeType + ", data: ");
   1528             builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray()));
   1529             return builder.toString();
   1530         }
   1531 
   1532         @Override
   1533         public EntryLabel getEntryLabel() {
   1534             return EntryLabel.ANDROID_CUSTOM;
   1535         }
   1536 
   1537         public String getMimeType() { return mMimeType; }
   1538         public List<String> getDataList() { return mDataList; }
   1539     }
   1540 
   1541     private final NameData mNameData = new NameData();
   1542     private List<PhoneData> mPhoneList;
   1543     private List<EmailData> mEmailList;
   1544     private List<PostalData> mPostalList;
   1545     private List<OrganizationData> mOrganizationList;
   1546     private List<ImData> mImList;
   1547     private List<PhotoData> mPhotoList;
   1548     private List<WebsiteData> mWebsiteList;
   1549     private List<SipData> mSipList;
   1550     private List<NicknameData> mNicknameList;
   1551     private List<NoteData> mNoteList;
   1552     private List<AndroidCustomData> mAndroidCustomDataList;
   1553     private BirthdayData mBirthday;
   1554     private AnniversaryData mAnniversary;
   1555 
   1556     /**
   1557      * Inner iterator interface.
   1558      */
   1559     public interface EntryElementIterator {
   1560         public void onIterationStarted();
   1561 
   1562         public void onIterationEnded();
   1563 
   1564         /**
   1565          * Called when there are one or more {@link EntryElement} instances
   1566          * associated with {@link EntryLabel}.
   1567          */
   1568         public void onElementGroupStarted(EntryLabel label);
   1569 
   1570         /**
   1571          * Called after all {@link EntryElement} instances for
   1572          * {@link EntryLabel} provided on {@link #onElementGroupStarted(EntryLabel)}
   1573          * being processed by {@link #onElement(EntryElement)}
   1574          */
   1575         public void onElementGroupEnded();
   1576 
   1577         /**
   1578          * @return should be true when child wants to continue the operation.
   1579          *         False otherwise.
   1580          */
   1581         public boolean onElement(EntryElement elem);
   1582     }
   1583 
   1584     public final void iterateAllData(EntryElementIterator iterator) {
   1585         iterator.onIterationStarted();
   1586         iterator.onElementGroupStarted(mNameData.getEntryLabel());
   1587         iterator.onElement(mNameData);
   1588         iterator.onElementGroupEnded();
   1589 
   1590         iterateOneList(mPhoneList, iterator);
   1591         iterateOneList(mEmailList, iterator);
   1592         iterateOneList(mPostalList, iterator);
   1593         iterateOneList(mOrganizationList, iterator);
   1594         iterateOneList(mImList, iterator);
   1595         iterateOneList(mPhotoList, iterator);
   1596         iterateOneList(mWebsiteList, iterator);
   1597         iterateOneList(mSipList, iterator);
   1598         iterateOneList(mNicknameList, iterator);
   1599         iterateOneList(mNoteList, iterator);
   1600         iterateOneList(mAndroidCustomDataList, iterator);
   1601 
   1602         if (mBirthday != null) {
   1603             iterator.onElementGroupStarted(mBirthday.getEntryLabel());
   1604             iterator.onElement(mBirthday);
   1605             iterator.onElementGroupEnded();
   1606         }
   1607         if (mAnniversary != null) {
   1608             iterator.onElementGroupStarted(mAnniversary.getEntryLabel());
   1609             iterator.onElement(mAnniversary);
   1610             iterator.onElementGroupEnded();
   1611         }
   1612         iterator.onIterationEnded();
   1613     }
   1614 
   1615     private void iterateOneList(List<? extends EntryElement> elemList,
   1616             EntryElementIterator iterator) {
   1617         if (elemList != null && elemList.size() > 0) {
   1618             iterator.onElementGroupStarted(elemList.get(0).getEntryLabel());
   1619             for (EntryElement elem : elemList) {
   1620                 iterator.onElement(elem);
   1621             }
   1622             iterator.onElementGroupEnded();
   1623         }
   1624     }
   1625 
   1626     private class IsIgnorableIterator implements EntryElementIterator {
   1627         private boolean mEmpty = true;
   1628 
   1629         @Override
   1630         public void onIterationStarted() {
   1631         }
   1632 
   1633         @Override
   1634         public void onIterationEnded() {
   1635         }
   1636 
   1637         @Override
   1638         public void onElementGroupStarted(EntryLabel label) {
   1639         }
   1640 
   1641         @Override
   1642         public void onElementGroupEnded() {
   1643         }
   1644 
   1645         @Override
   1646         public boolean onElement(EntryElement elem) {
   1647             if (!elem.isEmpty()) {
   1648                 mEmpty = false;
   1649                 // exit now
   1650                 return false;
   1651             } else {
   1652                 return true;
   1653             }
   1654         }
   1655 
   1656         public boolean getResult() {
   1657             return mEmpty;
   1658         }
   1659     }
   1660 
   1661     private class ToStringIterator implements EntryElementIterator {
   1662         private StringBuilder mBuilder;
   1663 
   1664         private boolean mFirstElement;
   1665 
   1666         @Override
   1667         public void onIterationStarted() {
   1668             mBuilder = new StringBuilder();
   1669             mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n");
   1670         }
   1671 
   1672         @Override
   1673         public void onElementGroupStarted(EntryLabel label) {
   1674             mBuilder.append(label.toString() + ": ");
   1675             mFirstElement = true;
   1676         }
   1677 
   1678         @Override
   1679         public boolean onElement(EntryElement elem) {
   1680             if (!mFirstElement) {
   1681                 mBuilder.append(", ");
   1682                 mFirstElement = false;
   1683             }
   1684             mBuilder.append("[").append(elem.toString()).append("]");
   1685             return true;
   1686         }
   1687 
   1688         @Override
   1689         public void onElementGroupEnded() {
   1690             mBuilder.append("\n");
   1691         }
   1692 
   1693         @Override
   1694         public void onIterationEnded() {
   1695             mBuilder.append("]]\n");
   1696         }
   1697 
   1698         @Override
   1699         public String toString() {
   1700             return mBuilder.toString();
   1701         }
   1702     }
   1703 
   1704     private class InsertOperationConstrutor implements EntryElementIterator {
   1705         private final List<ContentProviderOperation> mOperationList;
   1706 
   1707         private final int mBackReferenceIndex;
   1708 
   1709         public InsertOperationConstrutor(List<ContentProviderOperation> operationList,
   1710                 int backReferenceIndex) {
   1711             mOperationList = operationList;
   1712             mBackReferenceIndex = backReferenceIndex;
   1713         }
   1714 
   1715         @Override
   1716         public void onIterationStarted() {
   1717         }
   1718 
   1719         @Override
   1720         public void onIterationEnded() {
   1721         }
   1722 
   1723         @Override
   1724         public void onElementGroupStarted(EntryLabel label) {
   1725         }
   1726 
   1727         @Override
   1728         public void onElementGroupEnded() {
   1729         }
   1730 
   1731         @Override
   1732         public boolean onElement(EntryElement elem) {
   1733             if (!elem.isEmpty()) {
   1734                 elem.constructInsertOperation(mOperationList, mBackReferenceIndex);
   1735             }
   1736             return true;
   1737         }
   1738     }
   1739 
   1740     private final int mVCardType;
   1741     private final Account mAccount;
   1742 
   1743     private List<VCardEntry> mChildren;
   1744 
   1745     @Override
   1746     public String toString() {
   1747         ToStringIterator iterator = new ToStringIterator();
   1748         iterateAllData(iterator);
   1749         return iterator.toString();
   1750     }
   1751 
   1752     public VCardEntry() {
   1753         this(VCardConfig.VCARD_TYPE_V21_GENERIC);
   1754     }
   1755 
   1756     public VCardEntry(int vcardType) {
   1757         this(vcardType, null);
   1758     }
   1759 
   1760     public VCardEntry(int vcardType, Account account) {
   1761         mVCardType = vcardType;
   1762         mAccount = account;
   1763     }
   1764 
   1765     private void addPhone(int type, String data, String label, boolean isPrimary) {
   1766         if (mPhoneList == null) {
   1767             mPhoneList = new ArrayList<PhoneData>();
   1768         }
   1769         final StringBuilder builder = new StringBuilder();
   1770         final String trimmed = data.trim();
   1771         final String formattedNumber;
   1772         if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) {
   1773             formattedNumber = trimmed;
   1774         } else {
   1775             // TODO: from the view of vCard spec these auto conversions should be removed.
   1776             // Note that some other codes (like the phone number formatter) or modules expect this
   1777             // auto conversion (bug 5178723), so just omitting this code won't be preferable enough
   1778             // (bug 4177894)
   1779             boolean hasPauseOrWait = false;
   1780             final int length = trimmed.length();
   1781             for (int i = 0; i < length; i++) {
   1782                 char ch = trimmed.charAt(i);
   1783                 // See RFC 3601 and docs for PhoneNumberUtils for more info.
   1784                 if (ch == 'p' || ch == 'P') {
   1785                     builder.append(PhoneNumberUtils.PAUSE);
   1786                     hasPauseOrWait = true;
   1787                 } else if (ch == 'w' || ch == 'W') {
   1788                     builder.append(PhoneNumberUtils.WAIT);
   1789                     hasPauseOrWait = true;
   1790                 } else if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) {
   1791                     builder.append(ch);
   1792                 }
   1793             }
   1794             if (!hasPauseOrWait) {
   1795                 final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType);
   1796                 formattedNumber = PhoneNumberUtilsPort.formatNumber(
   1797                         builder.toString(), formattingType);
   1798             } else {
   1799                 formattedNumber = builder.toString();
   1800             }
   1801         }
   1802         PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary);
   1803         mPhoneList.add(phoneData);
   1804     }
   1805 
   1806     private void addSip(String sipData, int type, String label, boolean isPrimary) {
   1807         if (mSipList == null) {
   1808             mSipList = new ArrayList<SipData>();
   1809         }
   1810         mSipList.add(new SipData(sipData, type, label, isPrimary));
   1811     }
   1812 
   1813     private void addNickName(final String nickName) {
   1814         if (mNicknameList == null) {
   1815             mNicknameList = new ArrayList<NicknameData>();
   1816         }
   1817         mNicknameList.add(new NicknameData(nickName));
   1818     }
   1819 
   1820     private void addEmail(int type, String data, String label, boolean isPrimary) {
   1821         if (mEmailList == null) {
   1822             mEmailList = new ArrayList<EmailData>();
   1823         }
   1824         mEmailList.add(new EmailData(data, type, label, isPrimary));
   1825     }
   1826 
   1827     private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) {
   1828         if (mPostalList == null) {
   1829             mPostalList = new ArrayList<PostalData>(0);
   1830         }
   1831         mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary,
   1832                 mVCardType));
   1833     }
   1834 
   1835     /**
   1836      * Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or
   1837      * {@link #handleTitleValue(String)}.
   1838      */
   1839     private void addNewOrganization(final String organizationName, final String departmentName,
   1840             final String titleName, final String phoneticName, int type, final boolean isPrimary) {
   1841         if (mOrganizationList == null) {
   1842             mOrganizationList = new ArrayList<OrganizationData>();
   1843         }
   1844         mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName,
   1845                 phoneticName, type, isPrimary));
   1846     }
   1847 
   1848     private static final List<String> sEmptyList = Collections
   1849             .unmodifiableList(new ArrayList<String>(0));
   1850 
   1851     private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) {
   1852         final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
   1853         if (sortAsCollection != null && sortAsCollection.size() != 0) {
   1854             if (sortAsCollection.size() > 1) {
   1855                 Log.w(LOG_TAG,
   1856                         "Incorrect multiple SORT_AS parameters detected: "
   1857                                 + Arrays.toString(sortAsCollection.toArray()));
   1858             }
   1859             final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
   1860                     .iterator().next(), mVCardType);
   1861             final StringBuilder builder = new StringBuilder();
   1862             for (final String elem : sortNames) {
   1863                 builder.append(elem);
   1864             }
   1865             return builder.toString();
   1866         } else {
   1867             return null;
   1868         }
   1869     }
   1870 
   1871     /**
   1872      * Set "ORG" related values to the appropriate data. If there's more than
   1873      * one {@link OrganizationData} objects, this input data are attached to the
   1874      * last one which does not have valid values (not including empty but only
   1875      * null). If there's no {@link OrganizationData} object, a new
   1876      * {@link OrganizationData} is created, whose title is set to null.
   1877      */
   1878     private void handleOrgValue(final int type, List<String> orgList,
   1879             Map<String, Collection<String>> paramMap, boolean isPrimary) {
   1880         final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap);
   1881         if (orgList == null) {
   1882             orgList = sEmptyList;
   1883         }
   1884         final String organizationName;
   1885         final String departmentName;
   1886         final int size = orgList.size();
   1887         switch (size) {
   1888         case 0: {
   1889             organizationName = "";
   1890             departmentName = null;
   1891             break;
   1892         }
   1893         case 1: {
   1894             organizationName = orgList.get(0);
   1895             departmentName = null;
   1896             break;
   1897         }
   1898         default: { // More than 1.
   1899             organizationName = orgList.get(0);
   1900             // We're not sure which is the correct string for department.
   1901             // In order to keep all the data, concatinate the rest of elements.
   1902             StringBuilder builder = new StringBuilder();
   1903             for (int i = 1; i < size; i++) {
   1904                 if (i > 1) {
   1905                     builder.append(' ');
   1906                 }
   1907                 builder.append(orgList.get(i));
   1908             }
   1909             departmentName = builder.toString();
   1910         }
   1911         }
   1912         if (mOrganizationList == null) {
   1913             // Create new first organization entry, with "null" title which may be
   1914             // added via handleTitleValue().
   1915             addNewOrganization(organizationName, departmentName, null, phoneticName, type,
   1916                     isPrimary);
   1917             return;
   1918         }
   1919         for (OrganizationData organizationData : mOrganizationList) {
   1920             // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty.
   1921             // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null.
   1922             if (organizationData.mOrganizationName == null
   1923                     && organizationData.mDepartmentName == null) {
   1924                 // Probably the "TITLE" property comes before the "ORG" property via
   1925                 // handleTitleLine().
   1926                 organizationData.mOrganizationName = organizationName;
   1927                 organizationData.mDepartmentName = departmentName;
   1928                 organizationData.mIsPrimary = isPrimary;
   1929                 return;
   1930             }
   1931         }
   1932         // No OrganizatioData is available. Create another one, with "null" title, which may be
   1933         // added via handleTitleValue().
   1934         addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary);
   1935     }
   1936 
   1937     /**
   1938      * Set "title" value to the appropriate data. If there's more than one
   1939      * OrganizationData objects, this input is attached to the last one which
   1940      * does not have valid title value (not including empty but only null). If
   1941      * there's no OrganizationData object, a new OrganizationData is created,
   1942      * whose company name is set to null.
   1943      */
   1944     private void handleTitleValue(final String title) {
   1945         if (mOrganizationList == null) {
   1946             // Create new first organization entry, with "null" other info, which may be
   1947             // added via handleOrgValue().
   1948             addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
   1949             return;
   1950         }
   1951         for (OrganizationData organizationData : mOrganizationList) {
   1952             if (organizationData.mTitle == null) {
   1953                 organizationData.mTitle = title;
   1954                 return;
   1955             }
   1956         }
   1957         // No Organization is available. Create another one, with "null" other info, which may be
   1958         // added via handleOrgValue().
   1959         addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false);
   1960     }
   1961 
   1962     private void addIm(int protocol, String customProtocol, String propValue, int type,
   1963             boolean isPrimary) {
   1964         if (mImList == null) {
   1965             mImList = new ArrayList<ImData>();
   1966         }
   1967         mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary));
   1968     }
   1969 
   1970     private void addNote(final String note) {
   1971         if (mNoteList == null) {
   1972             mNoteList = new ArrayList<NoteData>(1);
   1973         }
   1974         mNoteList.add(new NoteData(note));
   1975     }
   1976 
   1977     private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) {
   1978         if (mPhotoList == null) {
   1979             mPhotoList = new ArrayList<PhotoData>(1);
   1980         }
   1981         final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary);
   1982         mPhotoList.add(photoData);
   1983     }
   1984 
   1985     /**
   1986      * Tries to extract paramMap, constructs SORT-AS parameter values, and store
   1987      * them in appropriate phonetic name variables. This method does not care
   1988      * the vCard version. Even when we have SORT-AS parameters in invalid
   1989      * versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't
   1990      * drop meaningful information. If we had this parameter in the N field of
   1991      * vCard 3.0, and the contact data also have SORT-STRING, we will prefer
   1992      * SORT-STRING, since it is regitimate property to be understood.
   1993      */
   1994     private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) {
   1995         if (VCardConfig.isVersion30(mVCardType)
   1996                 && !(TextUtils.isEmpty(mNameData.mPhoneticFamily)
   1997                         && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
   1998                         .isEmpty(mNameData.mPhoneticGiven))) {
   1999             return;
   2000         }
   2001 
   2002         final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS);
   2003         if (sortAsCollection != null && sortAsCollection.size() != 0) {
   2004             if (sortAsCollection.size() > 1) {
   2005                 Log.w(LOG_TAG,
   2006                         "Incorrect multiple SORT_AS parameters detected: "
   2007                                 + Arrays.toString(sortAsCollection.toArray()));
   2008             }
   2009             final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection
   2010                     .iterator().next(), mVCardType);
   2011             int size = sortNames.size();
   2012             if (size > 3) {
   2013                 size = 3;
   2014             }
   2015             switch (size) {
   2016             case 3:
   2017                 mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$
   2018             case 2:
   2019                 mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$
   2020             default:
   2021                 mNameData.mPhoneticFamily = sortNames.get(0);
   2022                 break;
   2023             }
   2024         }
   2025     }
   2026 
   2027     @SuppressWarnings("fallthrough")
   2028     private void handleNProperty(final List<String> paramValues,
   2029             Map<String, Collection<String>> paramMap) {
   2030         // in vCard 4.0, SORT-AS parameter is available.
   2031         tryHandleSortAsName(paramMap);
   2032 
   2033         // Family, Given, Middle, Prefix, Suffix. (1 - 5)
   2034         int size;
   2035         if (paramValues == null || (size = paramValues.size()) < 1) {
   2036             return;
   2037         }
   2038         if (size > 5) {
   2039             size = 5;
   2040         }
   2041 
   2042         switch (size) {
   2043         // Fall-through.
   2044         case 5:
   2045             mNameData.mSuffix = paramValues.get(4);
   2046         case 4:
   2047             mNameData.mPrefix = paramValues.get(3);
   2048         case 3:
   2049             mNameData.mMiddle = paramValues.get(2);
   2050         case 2:
   2051             mNameData.mGiven = paramValues.get(1);
   2052         default:
   2053             mNameData.mFamily = paramValues.get(0);
   2054         }
   2055     }
   2056 
   2057     /**
   2058      * Note: Some Japanese mobile phones use this field for phonetic name, since
   2059      * vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the
   2060      * field has some ';'s in it. Assume the ';' means the same meaning in N
   2061      * property
   2062      */
   2063     @SuppressWarnings("fallthrough")
   2064     private void handlePhoneticNameFromSound(List<String> elems) {
   2065         if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily)
   2066                 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils
   2067                 .isEmpty(mNameData.mPhoneticGiven))) {
   2068             // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found.
   2069             // Ignore "SOUND;X-IRMC-N".
   2070             return;
   2071         }
   2072 
   2073         int size;
   2074         if (elems == null || (size = elems.size()) < 1) {
   2075             return;
   2076         }
   2077 
   2078         // Assume that the order is "Family, Given, Middle".
   2079         // This is not from specification but mere assumption. Some Japanese
   2080         // phones use this order.
   2081         if (size > 3) {
   2082             size = 3;
   2083         }
   2084 
   2085         if (elems.get(0).length() > 0) {
   2086             boolean onlyFirstElemIsNonEmpty = true;
   2087             for (int i = 1; i < size; i++) {
   2088                 if (elems.get(i).length() > 0) {
   2089                     onlyFirstElemIsNonEmpty = false;
   2090                     break;
   2091                 }
   2092             }
   2093             if (onlyFirstElemIsNonEmpty) {
   2094                 final String[] namesArray = elems.get(0).split(" ");
   2095                 final int nameArrayLength = namesArray.length;
   2096                 if (nameArrayLength == 3) {
   2097                     // Assume the string is "Family Middle Given".
   2098                     mNameData.mPhoneticFamily = namesArray[0];
   2099                     mNameData.mPhoneticMiddle = namesArray[1];
   2100                     mNameData.mPhoneticGiven = namesArray[2];
   2101                 } else if (nameArrayLength == 2) {
   2102                     // Assume the string is "Family Given" based on the Japanese mobile
   2103                     // phones' preference.
   2104                     mNameData.mPhoneticFamily = namesArray[0];
   2105                     mNameData.mPhoneticGiven = namesArray[1];
   2106                 } else {
   2107                     mNameData.mPhoneticGiven = elems.get(0);
   2108                 }
   2109                 return;
   2110             }
   2111         }
   2112 
   2113         switch (size) {
   2114         // fallthrough
   2115         case 3:
   2116             mNameData.mPhoneticMiddle = elems.get(2);
   2117         case 2:
   2118             mNameData.mPhoneticGiven = elems.get(1);
   2119         default:
   2120             mNameData.mPhoneticFamily = elems.get(0);
   2121         }
   2122     }
   2123 
   2124     public void addProperty(final VCardProperty property) {
   2125         final String propertyName = property.getName();
   2126         final Map<String, Collection<String>> paramMap = property.getParameterMap();
   2127         final List<String> propertyValueList = property.getValueList();
   2128         byte[] propertyBytes = property.getByteValue();
   2129 
   2130         if ((propertyValueList == null || propertyValueList.size() == 0)
   2131                 && propertyBytes == null) {
   2132             return;
   2133         }
   2134         final String propValue = (propertyValueList != null
   2135                 ? listToString(propertyValueList).trim()
   2136                 : null);
   2137 
   2138         if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) {
   2139             // vCard version. Ignore this.
   2140         } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) {
   2141             mNameData.mFormatted = propValue;
   2142         } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) {
   2143             // Only in vCard 3.0. Use this if FN doesn't exist though it is
   2144             // required in vCard 3.0.
   2145             if (TextUtils.isEmpty(mNameData.mFormatted)) {
   2146                 mNameData.mFormatted = propValue;
   2147             }
   2148         } else if (propertyName.equals(VCardConstants.PROPERTY_N)) {
   2149             handleNProperty(propertyValueList, paramMap);
   2150         } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) {
   2151             mNameData.mSortString = propValue;
   2152         } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME)
   2153                 || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) {
   2154             addNickName(propValue);
   2155         } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) {
   2156             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2157             if (typeCollection != null
   2158                     && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) {
   2159                 // As of 2009-10-08, Parser side does not split a property value into separated
   2160                 // values using ';' (in other words, propValueList.size() == 1),
   2161                 // which is correct behavior from the view of vCard 2.1.
   2162                 // But we want it to be separated, so do the separation here.
   2163                 final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue,
   2164                         mVCardType);
   2165                 handlePhoneticNameFromSound(phoneticNameList);
   2166             } else {
   2167                 // Ignore this field since Android cannot understand what it is.
   2168             }
   2169         } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) {
   2170             boolean valuesAreAllEmpty = true;
   2171             for (String value : propertyValueList) {
   2172                 if (!TextUtils.isEmpty(value)) {
   2173                     valuesAreAllEmpty = false;
   2174                     break;
   2175                 }
   2176             }
   2177             if (valuesAreAllEmpty) {
   2178                 return;
   2179             }
   2180 
   2181             int type = -1;
   2182             String label = null;
   2183             boolean isPrimary = false;
   2184             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2185             if (typeCollection != null) {
   2186                 for (final String typeStringOrg : typeCollection) {
   2187                     final String typeStringUpperCase = typeStringOrg.toUpperCase();
   2188                     if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
   2189                         isPrimary = true;
   2190                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
   2191                         type = StructuredPostal.TYPE_HOME;
   2192                         label = null;
   2193                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)
   2194                             || typeStringUpperCase
   2195                                     .equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) {
   2196                         // "COMPANY" seems emitted by Windows Mobile, which is not
   2197                         // specifically supported by vCard 2.1. We assume this is same
   2198                         // as "WORK".
   2199                         type = StructuredPostal.TYPE_WORK;
   2200                         label = null;
   2201                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL)
   2202                             || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM)
   2203                             || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) {
   2204                         // We do not have any appropriate way to store this information.
   2205                     } else if (type < 0) { // If no other type is specified before.
   2206                         type = StructuredPostal.TYPE_CUSTOM;
   2207                         if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
   2208                             label = typeStringOrg.substring(2);
   2209                         } else {
   2210                             label = typeStringOrg;
   2211                         }
   2212                     }
   2213                 }
   2214             }
   2215             // We use "HOME" as default
   2216             if (type < 0) {
   2217                 type = StructuredPostal.TYPE_HOME;
   2218             }
   2219 
   2220             addPostal(type, propertyValueList, label, isPrimary);
   2221         } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) {
   2222             int type = -1;
   2223             String label = null;
   2224             boolean isPrimary = false;
   2225             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2226             if (typeCollection != null) {
   2227                 for (final String typeStringOrg : typeCollection) {
   2228                     final String typeStringUpperCase = typeStringOrg.toUpperCase();
   2229                     if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
   2230                         isPrimary = true;
   2231                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
   2232                         type = Email.TYPE_HOME;
   2233                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
   2234                         type = Email.TYPE_WORK;
   2235                     } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) {
   2236                         type = Email.TYPE_MOBILE;
   2237                     } else if (type < 0) { // If no other type is specified before
   2238                         if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
   2239                             label = typeStringOrg.substring(2);
   2240                         } else {
   2241                             label = typeStringOrg;
   2242                         }
   2243                         type = Email.TYPE_CUSTOM;
   2244                     }
   2245                 }
   2246             }
   2247             if (type < 0) {
   2248                 type = Email.TYPE_OTHER;
   2249             }
   2250             addEmail(type, propValue, label, isPrimary);
   2251         } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) {
   2252             // vCard specification does not specify other types.
   2253             final int type = Organization.TYPE_WORK;
   2254             boolean isPrimary = false;
   2255             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2256             if (typeCollection != null) {
   2257                 for (String typeString : typeCollection) {
   2258                     if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
   2259                         isPrimary = true;
   2260                     }
   2261                 }
   2262             }
   2263             handleOrgValue(type, propertyValueList, paramMap, isPrimary);
   2264         } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) {
   2265             handleTitleValue(propValue);
   2266         } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) {
   2267             // This conflicts with TITLE. Ignore for now...
   2268             // handleTitleValue(propValue);
   2269         } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO)
   2270                 || propertyName.equals(VCardConstants.PROPERTY_LOGO)) {
   2271             Collection<String> paramMapValue = paramMap.get("VALUE");
   2272             if (paramMapValue != null && paramMapValue.contains("URL")) {
   2273                 // Currently we do not have appropriate example for testing this case.
   2274             } else {
   2275                 final Collection<String> typeCollection = paramMap.get("TYPE");
   2276                 String formatName = null;
   2277                 boolean isPrimary = false;
   2278                 if (typeCollection != null) {
   2279                     for (String typeValue : typeCollection) {
   2280                         if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) {
   2281                             isPrimary = true;
   2282                         } else if (formatName == null) {
   2283                             formatName = typeValue;
   2284                         }
   2285                     }
   2286                 }
   2287                 addPhotoBytes(formatName, propertyBytes, isPrimary);
   2288             }
   2289         } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) {
   2290             String phoneNumber = null;
   2291             boolean isSip = false;
   2292             if (VCardConfig.isVersion40(mVCardType)) {
   2293                 // Given propValue is in URI format, not in phone number format used until
   2294                 // vCard 3.0.
   2295                 if (propValue.startsWith("sip:")) {
   2296                     isSip = true;
   2297                 } else if (propValue.startsWith("tel:")) {
   2298                     phoneNumber = propValue.substring(4);
   2299                 } else {
   2300                     // We don't know appropriate way to handle the other schemas. Also,
   2301                     // we may still have non-URI phone number. To keep given data as much as
   2302                     // we can, just save original value here.
   2303                     phoneNumber = propValue;
   2304                 }
   2305             } else {
   2306                 phoneNumber = propValue;
   2307             }
   2308 
   2309             if (isSip) {
   2310                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2311                 handleSipCase(propValue, typeCollection);
   2312             } else {
   2313                 if (propValue.length() == 0) {
   2314                     return;
   2315                 }
   2316 
   2317                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2318                 final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection,
   2319                         phoneNumber);
   2320                 final int type;
   2321                 final String label;
   2322                 if (typeObject instanceof Integer) {
   2323                     type = (Integer) typeObject;
   2324                     label = null;
   2325                 } else {
   2326                     type = Phone.TYPE_CUSTOM;
   2327                     label = typeObject.toString();
   2328                 }
   2329 
   2330                 final boolean isPrimary;
   2331                 if (typeCollection != null &&
   2332                         typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
   2333                     isPrimary = true;
   2334                 } else {
   2335                     isPrimary = false;
   2336                 }
   2337 
   2338                 addPhone(type, phoneNumber, label, isPrimary);
   2339             }
   2340         } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) {
   2341             // The phone number available via Skype.
   2342             Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2343             final int type = Phone.TYPE_OTHER;
   2344             final boolean isPrimary;
   2345             if (typeCollection != null
   2346                     && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) {
   2347                 isPrimary = true;
   2348             } else {
   2349                 isPrimary = false;
   2350             }
   2351             addPhone(type, propValue, null, isPrimary);
   2352         } else if (sImMap.containsKey(propertyName)) {
   2353             final int protocol = sImMap.get(propertyName);
   2354             boolean isPrimary = false;
   2355             int type = -1;
   2356             final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2357             if (typeCollection != null) {
   2358                 for (String typeString : typeCollection) {
   2359                     if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) {
   2360                         isPrimary = true;
   2361                     } else if (type < 0) {
   2362                         if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) {
   2363                             type = Im.TYPE_HOME;
   2364                         } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) {
   2365                             type = Im.TYPE_WORK;
   2366                         }
   2367                     }
   2368                 }
   2369             }
   2370             if (type < 0) {
   2371                 type = Im.TYPE_HOME;
   2372             }
   2373             addIm(protocol, null, propValue, type, isPrimary);
   2374         } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) {
   2375             addNote(propValue);
   2376         } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) {
   2377             if (mWebsiteList == null) {
   2378                 mWebsiteList = new ArrayList<WebsiteData>(1);
   2379             }
   2380             mWebsiteList.add(new WebsiteData(propValue));
   2381         } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) {
   2382             mBirthday = new BirthdayData(propValue);
   2383         } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) {
   2384             mAnniversary = new AnniversaryData(propValue);
   2385         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) {
   2386             mNameData.mPhoneticGiven = propValue;
   2387         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) {
   2388             mNameData.mPhoneticMiddle = propValue;
   2389         } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) {
   2390             mNameData.mPhoneticFamily = propValue;
   2391         } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) {
   2392             // See also RFC 4770 (for vCard 3.0)
   2393             if (propValue.startsWith("sip:")) {
   2394                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2395                 handleSipCase(propValue, typeCollection);
   2396             }
   2397         } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) {
   2398             if (!TextUtils.isEmpty(propValue)) {
   2399                 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE);
   2400                 handleSipCase(propValue, typeCollection);
   2401             }
   2402         } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) {
   2403             final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue,
   2404                     mVCardType);
   2405             handleAndroidCustomProperty(customPropertyList);
   2406         } else {
   2407         }
   2408         // Be careful when adding some logic here, as some blocks above may use "return".
   2409     }
   2410 
   2411     /**
   2412      * @param propValue may contain "sip:" at the beginning.
   2413      * @param typeCollection
   2414      */
   2415     private void handleSipCase(String propValue, Collection<String> typeCollection) {
   2416         if (TextUtils.isEmpty(propValue)) {
   2417             return;
   2418         }
   2419         if (propValue.startsWith("sip:")) {
   2420             propValue = propValue.substring(4);
   2421             if (propValue.length() == 0) {
   2422                 return;
   2423             }
   2424         }
   2425 
   2426         int type = -1;
   2427         String label = null;
   2428         boolean isPrimary = false;
   2429         if (typeCollection != null) {
   2430             for (final String typeStringOrg : typeCollection) {
   2431                 final String typeStringUpperCase = typeStringOrg.toUpperCase();
   2432                 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) {
   2433                     isPrimary = true;
   2434                 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) {
   2435                     type = SipAddress.TYPE_HOME;
   2436                 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) {
   2437                     type = SipAddress.TYPE_WORK;
   2438                 } else if (type < 0) { // If no other type is specified before
   2439                     if (typeStringUpperCase.startsWith("X-")) { // If X- or x-
   2440                         label = typeStringOrg.substring(2);
   2441                     } else {
   2442                         label = typeStringOrg;
   2443                     }
   2444                     type = SipAddress.TYPE_CUSTOM;
   2445                 }
   2446             }
   2447         }
   2448         if (type < 0) {
   2449             type = SipAddress.TYPE_OTHER;
   2450         }
   2451         addSip(propValue, type, label, isPrimary);
   2452     }
   2453 
   2454     public void addChild(VCardEntry child) {
   2455         if (mChildren == null) {
   2456             mChildren = new ArrayList<VCardEntry>();
   2457         }
   2458         mChildren.add(child);
   2459     }
   2460 
   2461     private void handleAndroidCustomProperty(final List<String> customPropertyList) {
   2462         if (mAndroidCustomDataList == null) {
   2463             mAndroidCustomDataList = new ArrayList<AndroidCustomData>();
   2464         }
   2465         mAndroidCustomDataList
   2466                 .add(AndroidCustomData.constructAndroidCustomData(customPropertyList));
   2467     }
   2468 
   2469     /**
   2470      * Construct the display name. The constructed data must not be null.
   2471      */
   2472     private String constructDisplayName() {
   2473         String displayName = null;
   2474         // FullName (created via "FN" or "NAME" field) is prefered.
   2475         if (!TextUtils.isEmpty(mNameData.mFormatted)) {
   2476             displayName = mNameData.mFormatted;
   2477         } else if (!mNameData.emptyStructuredName()) {
   2478             displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily,
   2479                     mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix);
   2480         } else if (!mNameData.emptyPhoneticStructuredName()) {
   2481             displayName = VCardUtils.constructNameFromElements(mVCardType,
   2482                     mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven);
   2483         } else if (mEmailList != null && mEmailList.size() > 0) {
   2484             displayName = mEmailList.get(0).mAddress;
   2485         } else if (mPhoneList != null && mPhoneList.size() > 0) {
   2486             displayName = mPhoneList.get(0).mNumber;
   2487         } else if (mPostalList != null && mPostalList.size() > 0) {
   2488             displayName = mPostalList.get(0).getFormattedAddress(mVCardType);
   2489         } else if (mOrganizationList != null && mOrganizationList.size() > 0) {
   2490             displayName = mOrganizationList.get(0).getFormattedString();
   2491         }
   2492         if (displayName == null) {
   2493             displayName = "";
   2494         }
   2495         return displayName;
   2496     }
   2497 
   2498     /**
   2499      * Consolidate several fielsds (like mName) using name candidates,
   2500      */
   2501     public void consolidateFields() {
   2502         mNameData.displayName = constructDisplayName();
   2503     }
   2504 
   2505     /**
   2506      * @return true when this object has nothing meaningful for Android's
   2507      *         Contacts, and thus is "ignorable" for Android's Contacts. This
   2508      *         does not mean an original vCard is really empty. Even when the
   2509      *         original vCard has some fields, this may ignore it if those
   2510      *         fields cannot be transcoded into Android's Contacts
   2511      *         representation.
   2512      */
   2513     public boolean isIgnorable() {
   2514         IsIgnorableIterator iterator = new IsIgnorableIterator();
   2515         iterateAllData(iterator);
   2516         return iterator.getResult();
   2517     }
   2518 
   2519     /**
   2520      * Constructs the list of insert operation for this object. When the
   2521      * operationList argument is null, this method creates a new ArrayList and
   2522      * return it. The returned object is filled with new insert operations for
   2523      * this object. When operationList argument is not null, this method appends
   2524      * those new operations into the object instead of creating a new ArrayList.
   2525      *
   2526      * @param resolver {@link ContentResolver} object to be used in this method.
   2527      * @param operationList object to be filled. You can use this argument to
   2528      *            concatinate operation lists. If null, this method creates a
   2529      *            new array object.
   2530      * @return If operationList argument is null, new object with new insert
   2531      *         operations. If it is not null, the operationList object with
   2532      *         operations inserted by this method.
   2533      */
   2534     public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver,
   2535             ArrayList<ContentProviderOperation> operationList) {
   2536         if (operationList == null) {
   2537             operationList = new ArrayList<ContentProviderOperation>();
   2538         }
   2539 
   2540         if (isIgnorable()) {
   2541             return operationList;
   2542         }
   2543 
   2544         final int backReferenceIndex = operationList.size();
   2545 
   2546         // After applying the batch the first result's Uri is returned so it is important that
   2547         // the RawContact is the first operation that gets inserted into the list.
   2548         ContentProviderOperation.Builder builder = ContentProviderOperation
   2549                 .newInsert(RawContacts.CONTENT_URI);
   2550         if (mAccount != null) {
   2551             builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name);
   2552             builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type);
   2553         } else {
   2554             builder.withValue(RawContacts.ACCOUNT_NAME, null);
   2555             builder.withValue(RawContacts.ACCOUNT_TYPE, null);
   2556         }
   2557         operationList.add(builder.build());
   2558 
   2559         int start = operationList.size();
   2560         iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex));
   2561         int end = operationList.size();
   2562 
   2563         return operationList;
   2564     }
   2565 
   2566     public static VCardEntry buildFromResolver(ContentResolver resolver) {
   2567         return buildFromResolver(resolver, Contacts.CONTENT_URI);
   2568     }
   2569 
   2570     public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) {
   2571         return null;
   2572     }
   2573 
   2574     private String listToString(List<String> list) {
   2575         final int size = list.size();
   2576         if (size > 1) {
   2577             StringBuilder builder = new StringBuilder();
   2578             int i = 0;
   2579             for (String type : list) {
   2580                 builder.append(type);
   2581                 if (i < size - 1) {
   2582                     builder.append(";");
   2583                 }
   2584             }
   2585             return builder.toString();
   2586         } else if (size == 1) {
   2587             return list.get(0);
   2588         } else {
   2589             return "";
   2590         }
   2591     }
   2592 
   2593     public final NameData getNameData() {
   2594         return mNameData;
   2595     }
   2596 
   2597     public final List<NicknameData> getNickNameList() {
   2598         return mNicknameList;
   2599     }
   2600 
   2601     public final String getBirthday() {
   2602         return mBirthday != null ? mBirthday.mBirthday : null;
   2603     }
   2604 
   2605     public final List<NoteData> getNotes() {
   2606         return mNoteList;
   2607     }
   2608 
   2609     public final List<PhoneData> getPhoneList() {
   2610         return mPhoneList;
   2611     }
   2612 
   2613     public final List<EmailData> getEmailList() {
   2614         return mEmailList;
   2615     }
   2616 
   2617     public final List<PostalData> getPostalList() {
   2618         return mPostalList;
   2619     }
   2620 
   2621     public final List<OrganizationData> getOrganizationList() {
   2622         return mOrganizationList;
   2623     }
   2624 
   2625     public final List<ImData> getImList() {
   2626         return mImList;
   2627     }
   2628 
   2629     public final List<PhotoData> getPhotoList() {
   2630         return mPhotoList;
   2631     }
   2632 
   2633     public final List<WebsiteData> getWebsiteList() {
   2634         return mWebsiteList;
   2635     }
   2636 
   2637     /**
   2638      * @hide this interface may be changed for better support of vCard 4.0 (UID)
   2639      */
   2640     public final List<VCardEntry> getChildlen() {
   2641         return mChildren;
   2642     }
   2643 
   2644     public String getDisplayName() {
   2645         if (mNameData.displayName == null) {
   2646             mNameData.displayName = constructDisplayName();
   2647         }
   2648         return mNameData.displayName;
   2649     }
   2650 }
   2651