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