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