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