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