VCardEntry.java revision 517540b6e3903371def5eb4ca44c2bb2ff91ae30
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 // TODO: make this private. Currently the app outside this class refers to this. 980 public final byte[] photoBytes; 981 982 private Integer mHashCode = null; 983 984 public PhotoData(String format, byte[] photoBytes, boolean isPrimary) { 985 mFormat = format; 986 this.photoBytes = photoBytes; 987 mIsPrimary = isPrimary; 988 } 989 990 @Override 991 public void constructInsertOperation(List<ContentProviderOperation> operationList, 992 int backReferenceIndex) { 993 final ContentProviderOperation.Builder builder = ContentProviderOperation 994 .newInsert(Data.CONTENT_URI); 995 builder.withValueBackReference(Photo.RAW_CONTACT_ID, backReferenceIndex); 996 builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 997 builder.withValue(Photo.PHOTO, photoBytes); 998 if (mIsPrimary) { 999 builder.withValue(Photo.IS_PRIMARY, 1); 1000 } 1001 operationList.add(builder.build()); 1002 } 1003 1004 @Override 1005 public boolean isEmpty() { 1006 return photoBytes == null || photoBytes.length == 0; 1007 } 1008 1009 @Override 1010 public boolean equals(Object obj) { 1011 if (this == obj) { 1012 return true; 1013 } 1014 if (!(obj instanceof PhotoData)) { 1015 return false; 1016 } 1017 PhotoData photoData = (PhotoData) obj; 1018 return (TextUtils.equals(mFormat, photoData.mFormat) 1019 && Arrays.equals(photoBytes, photoData.photoBytes) 1020 && (mIsPrimary == photoData.mIsPrimary)); 1021 } 1022 1023 @Override 1024 public int hashCode() { 1025 if (mHashCode != null) { 1026 return mHashCode; 1027 } 1028 1029 int hash = mFormat != null ? mFormat.hashCode() : 0; 1030 hash = hash * 31; 1031 if (photoBytes != null) { 1032 for (byte b : photoBytes) { 1033 hash += b; 1034 } 1035 } 1036 1037 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 1038 mHashCode = hash; 1039 return hash; 1040 } 1041 1042 @Override 1043 public String toString() { 1044 return String.format("format: %s: size: %d, isPrimary: %s", mFormat, photoBytes.length, 1045 mIsPrimary); 1046 } 1047 1048 @Override 1049 public final EntryLabel getEntryLabel() { 1050 return EntryLabel.PHOTO; 1051 } 1052 1053 public String getFormat() { 1054 return mFormat; 1055 } 1056 1057 public byte[] getBytes() { 1058 return photoBytes; 1059 } 1060 1061 public boolean isPrimary() { 1062 return mIsPrimary; 1063 } 1064 } 1065 1066 public static class NicknameData implements EntryElement { 1067 private final String mNickname; 1068 1069 public NicknameData(String nickname) { 1070 mNickname = nickname; 1071 } 1072 1073 @Override 1074 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1075 int backReferenceIndex) { 1076 final ContentProviderOperation.Builder builder = ContentProviderOperation 1077 .newInsert(Data.CONTENT_URI); 1078 builder.withValueBackReference(Nickname.RAW_CONTACT_ID, backReferenceIndex); 1079 builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); 1080 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1081 builder.withValue(Nickname.NAME, mNickname); 1082 operationList.add(builder.build()); 1083 } 1084 1085 @Override 1086 public boolean isEmpty() { 1087 return TextUtils.isEmpty(mNickname); 1088 } 1089 1090 @Override 1091 public boolean equals(Object obj) { 1092 if (!(obj instanceof NicknameData)) { 1093 return false; 1094 } 1095 NicknameData nicknameData = (NicknameData) obj; 1096 return TextUtils.equals(mNickname, nicknameData.mNickname); 1097 } 1098 1099 @Override 1100 public int hashCode() { 1101 return mNickname != null ? mNickname.hashCode() : 0; 1102 } 1103 1104 @Override 1105 public String toString() { 1106 return "nickname: " + mNickname; 1107 } 1108 1109 @Override 1110 public EntryLabel getEntryLabel() { 1111 return EntryLabel.NICKNAME; 1112 } 1113 1114 public String getNickname() { 1115 return mNickname; 1116 } 1117 } 1118 1119 public static class NoteData implements EntryElement { 1120 public final String mNote; 1121 1122 public NoteData(String note) { 1123 mNote = note; 1124 } 1125 1126 @Override 1127 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1128 int backReferenceIndex) { 1129 final ContentProviderOperation.Builder builder = ContentProviderOperation 1130 .newInsert(Data.CONTENT_URI); 1131 builder.withValueBackReference(Note.RAW_CONTACT_ID, backReferenceIndex); 1132 builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); 1133 builder.withValue(Note.NOTE, mNote); 1134 operationList.add(builder.build()); 1135 } 1136 1137 @Override 1138 public boolean isEmpty() { 1139 return TextUtils.isEmpty(mNote); 1140 } 1141 1142 @Override 1143 public boolean equals(Object obj) { 1144 if (this == obj) { 1145 return true; 1146 } 1147 if (!(obj instanceof NoteData)) { 1148 return false; 1149 } 1150 NoteData noteData = (NoteData) obj; 1151 return TextUtils.equals(mNote, noteData.mNote); 1152 } 1153 1154 @Override 1155 public int hashCode() { 1156 return mNote != null ? mNote.hashCode() : 0; 1157 } 1158 1159 @Override 1160 public String toString() { 1161 return "note: " + mNote; 1162 } 1163 1164 @Override 1165 public EntryLabel getEntryLabel() { 1166 return EntryLabel.NOTE; 1167 } 1168 1169 public String getNote() { 1170 return mNote; 1171 } 1172 } 1173 1174 public static class WebsiteData implements EntryElement { 1175 private final String mWebsite; 1176 1177 public WebsiteData(String website) { 1178 mWebsite = website; 1179 } 1180 1181 @Override 1182 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1183 int backReferenceIndex) { 1184 final ContentProviderOperation.Builder builder = ContentProviderOperation 1185 .newInsert(Data.CONTENT_URI); 1186 builder.withValueBackReference(Website.RAW_CONTACT_ID, backReferenceIndex); 1187 builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); 1188 builder.withValue(Website.URL, mWebsite); 1189 // There's no information about the type of URL in vCard. 1190 // We use TYPE_HOMEPAGE for safety. 1191 builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); 1192 operationList.add(builder.build()); 1193 } 1194 1195 @Override 1196 public boolean isEmpty() { 1197 return TextUtils.isEmpty(mWebsite); 1198 } 1199 1200 @Override 1201 public boolean equals(Object obj) { 1202 if (this == obj) { 1203 return true; 1204 } 1205 if (!(obj instanceof WebsiteData)) { 1206 return false; 1207 } 1208 WebsiteData websiteData = (WebsiteData) obj; 1209 return TextUtils.equals(mWebsite, websiteData.mWebsite); 1210 } 1211 1212 @Override 1213 public int hashCode() { 1214 return mWebsite != null ? mWebsite.hashCode() : 0; 1215 } 1216 1217 @Override 1218 public String toString() { 1219 return "website: " + mWebsite; 1220 } 1221 1222 @Override 1223 public EntryLabel getEntryLabel() { 1224 return EntryLabel.WEBSITE; 1225 } 1226 1227 public String getWebsite() { 1228 return mWebsite; 1229 } 1230 } 1231 1232 public static class BirthdayData implements EntryElement { 1233 private final String mBirthday; 1234 1235 public BirthdayData(String birthday) { 1236 mBirthday = birthday; 1237 } 1238 1239 @Override 1240 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1241 int backReferenceIndex) { 1242 final ContentProviderOperation.Builder builder = ContentProviderOperation 1243 .newInsert(Data.CONTENT_URI); 1244 builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex); 1245 builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); 1246 builder.withValue(Event.START_DATE, mBirthday); 1247 builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); 1248 operationList.add(builder.build()); 1249 } 1250 1251 @Override 1252 public boolean isEmpty() { 1253 return TextUtils.isEmpty(mBirthday); 1254 } 1255 1256 @Override 1257 public boolean equals(Object obj) { 1258 if (this == obj) { 1259 return true; 1260 } 1261 if (!(obj instanceof BirthdayData)) { 1262 return false; 1263 } 1264 BirthdayData birthdayData = (BirthdayData) obj; 1265 return TextUtils.equals(mBirthday, birthdayData.mBirthday); 1266 } 1267 1268 @Override 1269 public int hashCode() { 1270 return mBirthday != null ? mBirthday.hashCode() : 0; 1271 } 1272 1273 @Override 1274 public String toString() { 1275 return "birthday: " + mBirthday; 1276 } 1277 1278 @Override 1279 public EntryLabel getEntryLabel() { 1280 return EntryLabel.BIRTHDAY; 1281 } 1282 1283 public String getBirthday() { 1284 return mBirthday; 1285 } 1286 } 1287 1288 public static class AnniversaryData implements EntryElement { 1289 private final String mAnniversary; 1290 1291 public AnniversaryData(String anniversary) { 1292 mAnniversary = anniversary; 1293 } 1294 1295 @Override 1296 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1297 int backReferenceIndex) { 1298 final ContentProviderOperation.Builder builder = ContentProviderOperation 1299 .newInsert(Data.CONTENT_URI); 1300 builder.withValueBackReference(Event.RAW_CONTACT_ID, backReferenceIndex); 1301 builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); 1302 builder.withValue(Event.START_DATE, mAnniversary); 1303 builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY); 1304 operationList.add(builder.build()); 1305 } 1306 1307 @Override 1308 public boolean isEmpty() { 1309 return TextUtils.isEmpty(mAnniversary); 1310 } 1311 1312 @Override 1313 public boolean equals(Object obj) { 1314 if (this == obj) { 1315 return true; 1316 } 1317 if (!(obj instanceof AnniversaryData)) { 1318 return false; 1319 } 1320 AnniversaryData anniversaryData = (AnniversaryData) obj; 1321 return TextUtils.equals(mAnniversary, anniversaryData.mAnniversary); 1322 } 1323 1324 @Override 1325 public int hashCode() { 1326 return mAnniversary != null ? mAnniversary.hashCode() : 0; 1327 } 1328 1329 @Override 1330 public String toString() { 1331 return "anniversary: " + mAnniversary; 1332 } 1333 1334 @Override 1335 public EntryLabel getEntryLabel() { 1336 return EntryLabel.ANNIVERSARY; 1337 } 1338 1339 public String getAnniversary() { return mAnniversary; } 1340 } 1341 1342 public static class SipData implements EntryElement { 1343 /** 1344 * Note that schema part ("sip:") is automatically removed. e.g. 1345 * "sip:username:password@host:port" becomes 1346 * "username:password@host:port" 1347 */ 1348 private final String mAddress; 1349 private final int mType; 1350 private final String mLabel; 1351 private final boolean mIsPrimary; 1352 1353 public SipData(String rawSip, int type, String label, boolean isPrimary) { 1354 if (rawSip.startsWith("sip:")) { 1355 mAddress = rawSip.substring(4); 1356 } else { 1357 mAddress = rawSip; 1358 } 1359 mType = type; 1360 mLabel = label; 1361 mIsPrimary = isPrimary; 1362 } 1363 1364 @Override 1365 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1366 int backReferenceIndex) { 1367 final ContentProviderOperation.Builder builder = ContentProviderOperation 1368 .newInsert(Data.CONTENT_URI); 1369 builder.withValueBackReference(SipAddress.RAW_CONTACT_ID, backReferenceIndex); 1370 builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE); 1371 builder.withValue(SipAddress.SIP_ADDRESS, mAddress); 1372 builder.withValue(SipAddress.TYPE, mType); 1373 if (mType == SipAddress.TYPE_CUSTOM) { 1374 builder.withValue(SipAddress.LABEL, mLabel); 1375 } 1376 if (mIsPrimary) { 1377 builder.withValue(SipAddress.IS_PRIMARY, mIsPrimary); 1378 } 1379 operationList.add(builder.build()); 1380 } 1381 1382 @Override 1383 public boolean isEmpty() { 1384 return TextUtils.isEmpty(mAddress); 1385 } 1386 1387 @Override 1388 public boolean equals(Object obj) { 1389 if (this == obj) { 1390 return true; 1391 } 1392 if (!(obj instanceof SipData)) { 1393 return false; 1394 } 1395 SipData sipData = (SipData) obj; 1396 return (mType == sipData.mType 1397 && TextUtils.equals(mLabel, sipData.mLabel) 1398 && TextUtils.equals(mAddress, sipData.mAddress) 1399 && (mIsPrimary == sipData.mIsPrimary)); 1400 } 1401 1402 @Override 1403 public int hashCode() { 1404 int hash = mType; 1405 hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0); 1406 hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0); 1407 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 1408 return hash; 1409 } 1410 1411 @Override 1412 public String toString() { 1413 return "sip: " + mAddress; 1414 } 1415 1416 @Override 1417 public EntryLabel getEntryLabel() { 1418 return EntryLabel.SIP; 1419 } 1420 1421 /** 1422 * @return Address part of the sip data. The schema ("sip:") isn't contained here. 1423 */ 1424 public String getAddress() { return mAddress; } 1425 public int getType() { return mType; } 1426 public String getLabel() { return mLabel; } 1427 } 1428 1429 /** 1430 * Some Contacts data in Android cannot be converted to vCard 1431 * representation. VCardEntry preserves those data using this class. 1432 */ 1433 public static class AndroidCustomData implements EntryElement { 1434 private final String mMimeType; 1435 1436 private final List<String> mDataList; // 1 .. VCardConstants.MAX_DATA_COLUMN 1437 1438 public AndroidCustomData(String mimeType, List<String> dataList) { 1439 mMimeType = mimeType; 1440 mDataList = dataList; 1441 } 1442 1443 public static AndroidCustomData constructAndroidCustomData(List<String> list) { 1444 String mimeType; 1445 List<String> dataList; 1446 1447 if (list == null) { 1448 mimeType = null; 1449 dataList = null; 1450 } else if (list.size() < 2) { 1451 mimeType = list.get(0); 1452 dataList = null; 1453 } else { 1454 final int max = (list.size() < VCardConstants.MAX_DATA_COLUMN + 1) ? list.size() 1455 : VCardConstants.MAX_DATA_COLUMN + 1; 1456 mimeType = list.get(0); 1457 dataList = list.subList(1, max); 1458 } 1459 1460 return new AndroidCustomData(mimeType, dataList); 1461 } 1462 1463 @Override 1464 public void constructInsertOperation(List<ContentProviderOperation> operationList, 1465 int backReferenceIndex) { 1466 final ContentProviderOperation.Builder builder = ContentProviderOperation 1467 .newInsert(Data.CONTENT_URI); 1468 builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, backReferenceIndex); 1469 builder.withValue(Data.MIMETYPE, mMimeType); 1470 for (int i = 0; i < mDataList.size(); i++) { 1471 String value = mDataList.get(i); 1472 if (!TextUtils.isEmpty(value)) { 1473 // 1-origin 1474 builder.withValue("data" + (i + 1), value); 1475 } 1476 } 1477 operationList.add(builder.build()); 1478 } 1479 1480 @Override 1481 public boolean isEmpty() { 1482 return TextUtils.isEmpty(mMimeType) || mDataList == null || mDataList.size() == 0; 1483 } 1484 1485 @Override 1486 public boolean equals(Object obj) { 1487 if (this == obj) { 1488 return true; 1489 } 1490 if (!(obj instanceof AndroidCustomData)) { 1491 return false; 1492 } 1493 AndroidCustomData data = (AndroidCustomData) obj; 1494 if (!TextUtils.equals(mMimeType, data.mMimeType)) { 1495 return false; 1496 } 1497 if (mDataList == null) { 1498 return data.mDataList == null; 1499 } else { 1500 final int size = mDataList.size(); 1501 if (size != data.mDataList.size()) { 1502 return false; 1503 } 1504 for (int i = 0; i < size; i++) { 1505 if (!TextUtils.equals(mDataList.get(i), data.mDataList.get(i))) { 1506 return false; 1507 } 1508 } 1509 return true; 1510 } 1511 } 1512 1513 @Override 1514 public int hashCode() { 1515 int hash = mMimeType != null ? mMimeType.hashCode() : 0; 1516 if (mDataList != null) { 1517 for (String data : mDataList) { 1518 hash = hash * 31 + (data != null ? data.hashCode() : 0); 1519 } 1520 } 1521 return hash; 1522 } 1523 1524 @Override 1525 public String toString() { 1526 final StringBuilder builder = new StringBuilder(); 1527 builder.append("android-custom: " + mMimeType + ", data: "); 1528 builder.append(mDataList == null ? "null" : Arrays.toString(mDataList.toArray())); 1529 return builder.toString(); 1530 } 1531 1532 @Override 1533 public EntryLabel getEntryLabel() { 1534 return EntryLabel.ANDROID_CUSTOM; 1535 } 1536 1537 public String getMimeType() { return mMimeType; } 1538 public List<String> getDataList() { return mDataList; } 1539 } 1540 1541 private final NameData mNameData = new NameData(); 1542 private List<PhoneData> mPhoneList; 1543 private List<EmailData> mEmailList; 1544 private List<PostalData> mPostalList; 1545 private List<OrganizationData> mOrganizationList; 1546 private List<ImData> mImList; 1547 private List<PhotoData> mPhotoList; 1548 private List<WebsiteData> mWebsiteList; 1549 private List<SipData> mSipList; 1550 private List<NicknameData> mNicknameList; 1551 private List<NoteData> mNoteList; 1552 private List<AndroidCustomData> mAndroidCustomDataList; 1553 private BirthdayData mBirthday; 1554 private AnniversaryData mAnniversary; 1555 1556 /** 1557 * Inner iterator interface. 1558 */ 1559 public interface EntryElementIterator { 1560 public void onIterationStarted(); 1561 1562 public void onIterationEnded(); 1563 1564 /** 1565 * Called when there are one or more {@link EntryElement} instances 1566 * associated with {@link EntryLabel}. 1567 */ 1568 public void onElementGroupStarted(EntryLabel label); 1569 1570 /** 1571 * Called after all {@link EntryElement} instances for 1572 * {@link EntryLabel} provided on {@link #onElementGroupStarted(EntryLabel)} 1573 * being processed by {@link #onElement(EntryElement)} 1574 */ 1575 public void onElementGroupEnded(); 1576 1577 /** 1578 * @return should be true when child wants to continue the operation. 1579 * False otherwise. 1580 */ 1581 public boolean onElement(EntryElement elem); 1582 } 1583 1584 public final void iterateAllData(EntryElementIterator iterator) { 1585 iterator.onIterationStarted(); 1586 iterator.onElementGroupStarted(mNameData.getEntryLabel()); 1587 iterator.onElement(mNameData); 1588 iterator.onElementGroupEnded(); 1589 1590 iterateOneList(mPhoneList, iterator); 1591 iterateOneList(mEmailList, iterator); 1592 iterateOneList(mPostalList, iterator); 1593 iterateOneList(mOrganizationList, iterator); 1594 iterateOneList(mImList, iterator); 1595 iterateOneList(mPhotoList, iterator); 1596 iterateOneList(mWebsiteList, iterator); 1597 iterateOneList(mSipList, iterator); 1598 iterateOneList(mNicknameList, iterator); 1599 iterateOneList(mNoteList, iterator); 1600 iterateOneList(mAndroidCustomDataList, iterator); 1601 1602 if (mBirthday != null) { 1603 iterator.onElementGroupStarted(mBirthday.getEntryLabel()); 1604 iterator.onElement(mBirthday); 1605 iterator.onElementGroupEnded(); 1606 } 1607 if (mAnniversary != null) { 1608 iterator.onElementGroupStarted(mAnniversary.getEntryLabel()); 1609 iterator.onElement(mAnniversary); 1610 iterator.onElementGroupEnded(); 1611 } 1612 iterator.onIterationEnded(); 1613 } 1614 1615 private void iterateOneList(List<? extends EntryElement> elemList, 1616 EntryElementIterator iterator) { 1617 if (elemList != null && elemList.size() > 0) { 1618 iterator.onElementGroupStarted(elemList.get(0).getEntryLabel()); 1619 for (EntryElement elem : elemList) { 1620 iterator.onElement(elem); 1621 } 1622 iterator.onElementGroupEnded(); 1623 } 1624 } 1625 1626 private class IsIgnorableIterator implements EntryElementIterator { 1627 private boolean mEmpty = true; 1628 1629 @Override 1630 public void onIterationStarted() { 1631 } 1632 1633 @Override 1634 public void onIterationEnded() { 1635 } 1636 1637 @Override 1638 public void onElementGroupStarted(EntryLabel label) { 1639 } 1640 1641 @Override 1642 public void onElementGroupEnded() { 1643 } 1644 1645 @Override 1646 public boolean onElement(EntryElement elem) { 1647 if (!elem.isEmpty()) { 1648 mEmpty = false; 1649 // exit now 1650 return false; 1651 } else { 1652 return true; 1653 } 1654 } 1655 1656 public boolean getResult() { 1657 return mEmpty; 1658 } 1659 } 1660 1661 private class ToStringIterator implements EntryElementIterator { 1662 private StringBuilder mBuilder; 1663 1664 private boolean mFirstElement; 1665 1666 @Override 1667 public void onIterationStarted() { 1668 mBuilder = new StringBuilder(); 1669 mBuilder.append("[[hash: " + VCardEntry.this.hashCode() + "\n"); 1670 } 1671 1672 @Override 1673 public void onElementGroupStarted(EntryLabel label) { 1674 mBuilder.append(label.toString() + ": "); 1675 mFirstElement = true; 1676 } 1677 1678 @Override 1679 public boolean onElement(EntryElement elem) { 1680 if (!mFirstElement) { 1681 mBuilder.append(", "); 1682 mFirstElement = false; 1683 } 1684 mBuilder.append("[").append(elem.toString()).append("]"); 1685 return true; 1686 } 1687 1688 @Override 1689 public void onElementGroupEnded() { 1690 mBuilder.append("\n"); 1691 } 1692 1693 @Override 1694 public void onIterationEnded() { 1695 mBuilder.append("]]\n"); 1696 } 1697 1698 @Override 1699 public String toString() { 1700 return mBuilder.toString(); 1701 } 1702 } 1703 1704 private class InsertOperationConstrutor implements EntryElementIterator { 1705 private final List<ContentProviderOperation> mOperationList; 1706 1707 private final int mBackReferenceIndex; 1708 1709 public InsertOperationConstrutor(List<ContentProviderOperation> operationList, 1710 int backReferenceIndex) { 1711 mOperationList = operationList; 1712 mBackReferenceIndex = backReferenceIndex; 1713 } 1714 1715 @Override 1716 public void onIterationStarted() { 1717 } 1718 1719 @Override 1720 public void onIterationEnded() { 1721 } 1722 1723 @Override 1724 public void onElementGroupStarted(EntryLabel label) { 1725 } 1726 1727 @Override 1728 public void onElementGroupEnded() { 1729 } 1730 1731 @Override 1732 public boolean onElement(EntryElement elem) { 1733 if (!elem.isEmpty()) { 1734 elem.constructInsertOperation(mOperationList, mBackReferenceIndex); 1735 } 1736 return true; 1737 } 1738 } 1739 1740 private final int mVCardType; 1741 private final Account mAccount; 1742 1743 private List<VCardEntry> mChildren; 1744 1745 @Override 1746 public String toString() { 1747 ToStringIterator iterator = new ToStringIterator(); 1748 iterateAllData(iterator); 1749 return iterator.toString(); 1750 } 1751 1752 public VCardEntry() { 1753 this(VCardConfig.VCARD_TYPE_V21_GENERIC); 1754 } 1755 1756 public VCardEntry(int vcardType) { 1757 this(vcardType, null); 1758 } 1759 1760 public VCardEntry(int vcardType, Account account) { 1761 mVCardType = vcardType; 1762 mAccount = account; 1763 } 1764 1765 private void addPhone(int type, String data, String label, boolean isPrimary) { 1766 if (mPhoneList == null) { 1767 mPhoneList = new ArrayList<PhoneData>(); 1768 } 1769 final StringBuilder builder = new StringBuilder(); 1770 final String trimed = data.trim(); 1771 final String formattedNumber; 1772 if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { 1773 formattedNumber = trimed; 1774 } else { 1775 final int length = trimed.length(); 1776 for (int i = 0; i < length; i++) { 1777 char ch = trimed.charAt(i); 1778 if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { 1779 builder.append(ch); 1780 } 1781 } 1782 1783 final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); 1784 formattedNumber = PhoneNumberUtilsPort.formatNumber(builder.toString(), formattingType); 1785 } 1786 PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary); 1787 mPhoneList.add(phoneData); 1788 } 1789 1790 private void addSip(String sipData, int type, String label, boolean isPrimary) { 1791 if (mSipList == null) { 1792 mSipList = new ArrayList<SipData>(); 1793 } 1794 mSipList.add(new SipData(sipData, type, label, isPrimary)); 1795 } 1796 1797 private void addNickName(final String nickName) { 1798 if (mNicknameList == null) { 1799 mNicknameList = new ArrayList<NicknameData>(); 1800 } 1801 mNicknameList.add(new NicknameData(nickName)); 1802 } 1803 1804 private void addEmail(int type, String data, String label, boolean isPrimary) { 1805 if (mEmailList == null) { 1806 mEmailList = new ArrayList<EmailData>(); 1807 } 1808 mEmailList.add(new EmailData(data, type, label, isPrimary)); 1809 } 1810 1811 private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) { 1812 if (mPostalList == null) { 1813 mPostalList = new ArrayList<PostalData>(0); 1814 } 1815 mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary, 1816 mVCardType)); 1817 } 1818 1819 /** 1820 * Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or 1821 * {@link #handleTitleValue(String)}. 1822 */ 1823 private void addNewOrganization(final String organizationName, final String departmentName, 1824 final String titleName, final String phoneticName, int type, final boolean isPrimary) { 1825 if (mOrganizationList == null) { 1826 mOrganizationList = new ArrayList<OrganizationData>(); 1827 } 1828 mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName, 1829 phoneticName, type, isPrimary)); 1830 } 1831 1832 private static final List<String> sEmptyList = Collections 1833 .unmodifiableList(new ArrayList<String>(0)); 1834 1835 private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) { 1836 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 1837 if (sortAsCollection != null && sortAsCollection.size() != 0) { 1838 if (sortAsCollection.size() > 1) { 1839 Log.w(LOG_TAG, 1840 "Incorrect multiple SORT_AS parameters detected: " 1841 + Arrays.toString(sortAsCollection.toArray())); 1842 } 1843 final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection 1844 .iterator().next(), mVCardType); 1845 final StringBuilder builder = new StringBuilder(); 1846 for (final String elem : sortNames) { 1847 builder.append(elem); 1848 } 1849 return builder.toString(); 1850 } else { 1851 return null; 1852 } 1853 } 1854 1855 /** 1856 * Set "ORG" related values to the appropriate data. If there's more than 1857 * one {@link OrganizationData} objects, this input data are attached to the 1858 * last one which does not have valid values (not including empty but only 1859 * null). If there's no {@link OrganizationData} object, a new 1860 * {@link OrganizationData} is created, whose title is set to null. 1861 */ 1862 private void handleOrgValue(final int type, List<String> orgList, 1863 Map<String, Collection<String>> paramMap, boolean isPrimary) { 1864 final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap); 1865 if (orgList == null) { 1866 orgList = sEmptyList; 1867 } 1868 final String organizationName; 1869 final String departmentName; 1870 final int size = orgList.size(); 1871 switch (size) { 1872 case 0: { 1873 organizationName = ""; 1874 departmentName = null; 1875 break; 1876 } 1877 case 1: { 1878 organizationName = orgList.get(0); 1879 departmentName = null; 1880 break; 1881 } 1882 default: { // More than 1. 1883 organizationName = orgList.get(0); 1884 // We're not sure which is the correct string for department. 1885 // In order to keep all the data, concatinate the rest of elements. 1886 StringBuilder builder = new StringBuilder(); 1887 for (int i = 1; i < size; i++) { 1888 if (i > 1) { 1889 builder.append(' '); 1890 } 1891 builder.append(orgList.get(i)); 1892 } 1893 departmentName = builder.toString(); 1894 } 1895 } 1896 if (mOrganizationList == null) { 1897 // Create new first organization entry, with "null" title which may be 1898 // added via handleTitleValue(). 1899 addNewOrganization(organizationName, departmentName, null, phoneticName, type, 1900 isPrimary); 1901 return; 1902 } 1903 for (OrganizationData organizationData : mOrganizationList) { 1904 // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. 1905 // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. 1906 if (organizationData.mOrganizationName == null 1907 && organizationData.mDepartmentName == null) { 1908 // Probably the "TITLE" property comes before the "ORG" property via 1909 // handleTitleLine(). 1910 organizationData.mOrganizationName = organizationName; 1911 organizationData.mDepartmentName = departmentName; 1912 organizationData.mIsPrimary = isPrimary; 1913 return; 1914 } 1915 } 1916 // No OrganizatioData is available. Create another one, with "null" title, which may be 1917 // added via handleTitleValue(). 1918 addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary); 1919 } 1920 1921 /** 1922 * Set "title" value to the appropriate data. If there's more than one 1923 * OrganizationData objects, this input is attached to the last one which 1924 * does not have valid title value (not including empty but only null). If 1925 * there's no OrganizationData object, a new OrganizationData is created, 1926 * whose company name is set to null. 1927 */ 1928 private void handleTitleValue(final String title) { 1929 if (mOrganizationList == null) { 1930 // Create new first organization entry, with "null" other info, which may be 1931 // added via handleOrgValue(). 1932 addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false); 1933 return; 1934 } 1935 for (OrganizationData organizationData : mOrganizationList) { 1936 if (organizationData.mTitle == null) { 1937 organizationData.mTitle = title; 1938 return; 1939 } 1940 } 1941 // No Organization is available. Create another one, with "null" other info, which may be 1942 // added via handleOrgValue(). 1943 addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false); 1944 } 1945 1946 private void addIm(int protocol, String customProtocol, String propValue, int type, 1947 boolean isPrimary) { 1948 if (mImList == null) { 1949 mImList = new ArrayList<ImData>(); 1950 } 1951 mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary)); 1952 } 1953 1954 private void addNote(final String note) { 1955 if (mNoteList == null) { 1956 mNoteList = new ArrayList<NoteData>(1); 1957 } 1958 mNoteList.add(new NoteData(note)); 1959 } 1960 1961 private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { 1962 if (mPhotoList == null) { 1963 mPhotoList = new ArrayList<PhotoData>(1); 1964 } 1965 final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary); 1966 mPhotoList.add(photoData); 1967 } 1968 1969 /** 1970 * Tries to extract paramMap, constructs SORT-AS parameter values, and store 1971 * them in appropriate phonetic name variables. This method does not care 1972 * the vCard version. Even when we have SORT-AS parameters in invalid 1973 * versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't 1974 * drop meaningful information. If we had this parameter in the N field of 1975 * vCard 3.0, and the contact data also have SORT-STRING, we will prefer 1976 * SORT-STRING, since it is regitimate property to be understood. 1977 */ 1978 private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) { 1979 if (VCardConfig.isVersion30(mVCardType) 1980 && !(TextUtils.isEmpty(mNameData.mPhoneticFamily) 1981 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils 1982 .isEmpty(mNameData.mPhoneticGiven))) { 1983 return; 1984 } 1985 1986 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 1987 if (sortAsCollection != null && sortAsCollection.size() != 0) { 1988 if (sortAsCollection.size() > 1) { 1989 Log.w(LOG_TAG, 1990 "Incorrect multiple SORT_AS parameters detected: " 1991 + Arrays.toString(sortAsCollection.toArray())); 1992 } 1993 final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection 1994 .iterator().next(), mVCardType); 1995 int size = sortNames.size(); 1996 if (size > 3) { 1997 size = 3; 1998 } 1999 switch (size) { 2000 case 3: 2001 mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$ 2002 case 2: 2003 mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$ 2004 default: 2005 mNameData.mPhoneticFamily = sortNames.get(0); 2006 break; 2007 } 2008 } 2009 } 2010 2011 @SuppressWarnings("fallthrough") 2012 private void handleNProperty(final List<String> paramValues, 2013 Map<String, Collection<String>> paramMap) { 2014 // in vCard 4.0, SORT-AS parameter is available. 2015 tryHandleSortAsName(paramMap); 2016 2017 // Family, Given, Middle, Prefix, Suffix. (1 - 5) 2018 int size; 2019 if (paramValues == null || (size = paramValues.size()) < 1) { 2020 return; 2021 } 2022 if (size > 5) { 2023 size = 5; 2024 } 2025 2026 switch (size) { 2027 // Fall-through. 2028 case 5: 2029 mNameData.mSuffix = paramValues.get(4); 2030 case 4: 2031 mNameData.mPrefix = paramValues.get(3); 2032 case 3: 2033 mNameData.mMiddle = paramValues.get(2); 2034 case 2: 2035 mNameData.mGiven = paramValues.get(1); 2036 default: 2037 mNameData.mFamily = paramValues.get(0); 2038 } 2039 } 2040 2041 /** 2042 * Note: Some Japanese mobile phones use this field for phonetic name, since 2043 * vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the 2044 * field has some ';'s in it. Assume the ';' means the same meaning in N 2045 * property 2046 */ 2047 @SuppressWarnings("fallthrough") 2048 private void handlePhoneticNameFromSound(List<String> elems) { 2049 if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily) 2050 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils 2051 .isEmpty(mNameData.mPhoneticGiven))) { 2052 // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. 2053 // Ignore "SOUND;X-IRMC-N". 2054 return; 2055 } 2056 2057 int size; 2058 if (elems == null || (size = elems.size()) < 1) { 2059 return; 2060 } 2061 2062 // Assume that the order is "Family, Given, Middle". 2063 // This is not from specification but mere assumption. Some Japanese 2064 // phones use this order. 2065 if (size > 3) { 2066 size = 3; 2067 } 2068 2069 if (elems.get(0).length() > 0) { 2070 boolean onlyFirstElemIsNonEmpty = true; 2071 for (int i = 1; i < size; i++) { 2072 if (elems.get(i).length() > 0) { 2073 onlyFirstElemIsNonEmpty = false; 2074 break; 2075 } 2076 } 2077 if (onlyFirstElemIsNonEmpty) { 2078 final String[] namesArray = elems.get(0).split(" "); 2079 final int nameArrayLength = namesArray.length; 2080 if (nameArrayLength == 3) { 2081 // Assume the string is "Family Middle Given". 2082 mNameData.mPhoneticFamily = namesArray[0]; 2083 mNameData.mPhoneticMiddle = namesArray[1]; 2084 mNameData.mPhoneticGiven = namesArray[2]; 2085 } else if (nameArrayLength == 2) { 2086 // Assume the string is "Family Given" based on the Japanese mobile 2087 // phones' preference. 2088 mNameData.mPhoneticFamily = namesArray[0]; 2089 mNameData.mPhoneticGiven = namesArray[1]; 2090 } else { 2091 mNameData.mPhoneticGiven = elems.get(0); 2092 } 2093 return; 2094 } 2095 } 2096 2097 switch (size) { 2098 // fallthrough 2099 case 3: 2100 mNameData.mPhoneticMiddle = elems.get(2); 2101 case 2: 2102 mNameData.mPhoneticGiven = elems.get(1); 2103 default: 2104 mNameData.mPhoneticFamily = elems.get(0); 2105 } 2106 } 2107 2108 public void addProperty(final VCardProperty property) { 2109 final String propertyName = property.getName(); 2110 final Map<String, Collection<String>> paramMap = property.getParameterMap(); 2111 final List<String> propertyValueList = property.getValueList(); 2112 byte[] propertyBytes = property.getByteValue(); 2113 2114 if ((propertyValueList == null || propertyValueList.size() == 0) 2115 && propertyBytes == null) { 2116 return; 2117 } 2118 final String propValue = (propertyValueList != null 2119 ? listToString(propertyValueList).trim() 2120 : null); 2121 2122 if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) { 2123 // vCard version. Ignore this. 2124 } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) { 2125 mNameData.mFormatted = propValue; 2126 } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) { 2127 // Only in vCard 3.0. Use this if FN doesn't exist though it is 2128 // required in vCard 3.0. 2129 if (TextUtils.isEmpty(mNameData.mFormatted)) { 2130 mNameData.mFormatted = propValue; 2131 } 2132 } else if (propertyName.equals(VCardConstants.PROPERTY_N)) { 2133 handleNProperty(propertyValueList, paramMap); 2134 } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) { 2135 mNameData.mSortString = propValue; 2136 } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME) 2137 || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { 2138 addNickName(propValue); 2139 } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) { 2140 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2141 if (typeCollection != null 2142 && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { 2143 // As of 2009-10-08, Parser side does not split a property value into separated 2144 // values using ';' (in other words, propValueList.size() == 1), 2145 // which is correct behavior from the view of vCard 2.1. 2146 // But we want it to be separated, so do the separation here. 2147 final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue, 2148 mVCardType); 2149 handlePhoneticNameFromSound(phoneticNameList); 2150 } else { 2151 // Ignore this field since Android cannot understand what it is. 2152 } 2153 } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) { 2154 boolean valuesAreAllEmpty = true; 2155 for (String value : propertyValueList) { 2156 if (!TextUtils.isEmpty(value)) { 2157 valuesAreAllEmpty = false; 2158 break; 2159 } 2160 } 2161 if (valuesAreAllEmpty) { 2162 return; 2163 } 2164 2165 int type = -1; 2166 String label = null; 2167 boolean isPrimary = false; 2168 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2169 if (typeCollection != null) { 2170 for (final String typeStringOrg : typeCollection) { 2171 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2172 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2173 isPrimary = true; 2174 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2175 type = StructuredPostal.TYPE_HOME; 2176 label = null; 2177 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK) 2178 || typeStringUpperCase 2179 .equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { 2180 // "COMPANY" seems emitted by Windows Mobile, which is not 2181 // specifically supported by vCard 2.1. We assume this is same 2182 // as "WORK". 2183 type = StructuredPostal.TYPE_WORK; 2184 label = null; 2185 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) 2186 || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM) 2187 || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { 2188 // We do not have any appropriate way to store this information. 2189 } else if (type < 0) { // If no other type is specified before. 2190 type = StructuredPostal.TYPE_CUSTOM; 2191 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2192 label = typeStringOrg.substring(2); 2193 } else { 2194 label = typeStringOrg; 2195 } 2196 } 2197 } 2198 } 2199 // We use "HOME" as default 2200 if (type < 0) { 2201 type = StructuredPostal.TYPE_HOME; 2202 } 2203 2204 addPostal(type, propertyValueList, label, isPrimary); 2205 } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) { 2206 int type = -1; 2207 String label = null; 2208 boolean isPrimary = false; 2209 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2210 if (typeCollection != null) { 2211 for (final String typeStringOrg : typeCollection) { 2212 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2213 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2214 isPrimary = true; 2215 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2216 type = Email.TYPE_HOME; 2217 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) { 2218 type = Email.TYPE_WORK; 2219 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) { 2220 type = Email.TYPE_MOBILE; 2221 } else if (type < 0) { // If no other type is specified before 2222 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2223 label = typeStringOrg.substring(2); 2224 } else { 2225 label = typeStringOrg; 2226 } 2227 type = Email.TYPE_CUSTOM; 2228 } 2229 } 2230 } 2231 if (type < 0) { 2232 type = Email.TYPE_OTHER; 2233 } 2234 addEmail(type, propValue, label, isPrimary); 2235 } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) { 2236 // vCard specification does not specify other types. 2237 final int type = Organization.TYPE_WORK; 2238 boolean isPrimary = false; 2239 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2240 if (typeCollection != null) { 2241 for (String typeString : typeCollection) { 2242 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 2243 isPrimary = true; 2244 } 2245 } 2246 } 2247 handleOrgValue(type, propertyValueList, paramMap, isPrimary); 2248 } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) { 2249 handleTitleValue(propValue); 2250 } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) { 2251 // This conflicts with TITLE. Ignore for now... 2252 // handleTitleValue(propValue); 2253 } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO) 2254 || propertyName.equals(VCardConstants.PROPERTY_LOGO)) { 2255 Collection<String> paramMapValue = paramMap.get("VALUE"); 2256 if (paramMapValue != null && paramMapValue.contains("URL")) { 2257 // Currently we do not have appropriate example for testing this case. 2258 } else { 2259 final Collection<String> typeCollection = paramMap.get("TYPE"); 2260 String formatName = null; 2261 boolean isPrimary = false; 2262 if (typeCollection != null) { 2263 for (String typeValue : typeCollection) { 2264 if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { 2265 isPrimary = true; 2266 } else if (formatName == null) { 2267 formatName = typeValue; 2268 } 2269 } 2270 } 2271 addPhotoBytes(formatName, propertyBytes, isPrimary); 2272 } 2273 } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) { 2274 String phoneNumber = null; 2275 boolean isSip = false; 2276 if (VCardConfig.isVersion40(mVCardType)) { 2277 // Given propValue is in URI format, not in phone number format used until 2278 // vCard 3.0. 2279 if (propValue.startsWith("sip:")) { 2280 isSip = true; 2281 } else if (propValue.startsWith("tel:")) { 2282 phoneNumber = propValue.substring(4); 2283 } else { 2284 // We don't know appropriate way to handle the other schemas. Also, 2285 // we may still have non-URI phone number. To keep given data as much as 2286 // we can, just save original value here. 2287 phoneNumber = propValue; 2288 } 2289 } else { 2290 phoneNumber = propValue; 2291 } 2292 2293 if (isSip) { 2294 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2295 handleSipCase(propValue, typeCollection); 2296 } else { 2297 if (propValue.length() == 0) { 2298 return; 2299 } 2300 2301 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2302 final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection, 2303 phoneNumber); 2304 final int type; 2305 final String label; 2306 if (typeObject instanceof Integer) { 2307 type = (Integer) typeObject; 2308 label = null; 2309 } else { 2310 type = Phone.TYPE_CUSTOM; 2311 label = typeObject.toString(); 2312 } 2313 2314 final boolean isPrimary; 2315 if (typeCollection != null && 2316 typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 2317 isPrimary = true; 2318 } else { 2319 isPrimary = false; 2320 } 2321 2322 addPhone(type, phoneNumber, label, isPrimary); 2323 } 2324 } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { 2325 // The phone number available via Skype. 2326 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2327 final int type = Phone.TYPE_OTHER; 2328 final boolean isPrimary; 2329 if (typeCollection != null 2330 && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 2331 isPrimary = true; 2332 } else { 2333 isPrimary = false; 2334 } 2335 addPhone(type, propValue, null, isPrimary); 2336 } else if (sImMap.containsKey(propertyName)) { 2337 final int protocol = sImMap.get(propertyName); 2338 boolean isPrimary = false; 2339 int type = -1; 2340 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2341 if (typeCollection != null) { 2342 for (String typeString : typeCollection) { 2343 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 2344 isPrimary = true; 2345 } else if (type < 0) { 2346 if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { 2347 type = Im.TYPE_HOME; 2348 } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { 2349 type = Im.TYPE_WORK; 2350 } 2351 } 2352 } 2353 } 2354 if (type < 0) { 2355 type = Im.TYPE_HOME; 2356 } 2357 addIm(protocol, null, propValue, type, isPrimary); 2358 } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) { 2359 addNote(propValue); 2360 } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) { 2361 if (mWebsiteList == null) { 2362 mWebsiteList = new ArrayList<WebsiteData>(1); 2363 } 2364 mWebsiteList.add(new WebsiteData(propValue)); 2365 } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) { 2366 mBirthday = new BirthdayData(propValue); 2367 } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) { 2368 mAnniversary = new AnniversaryData(propValue); 2369 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { 2370 mNameData.mPhoneticGiven = propValue; 2371 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { 2372 mNameData.mPhoneticMiddle = propValue; 2373 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { 2374 mNameData.mPhoneticFamily = propValue; 2375 } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) { 2376 // See also RFC 4770 (for vCard 3.0) 2377 if (propValue.startsWith("sip:")) { 2378 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2379 handleSipCase(propValue, typeCollection); 2380 } 2381 } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) { 2382 if (!TextUtils.isEmpty(propValue)) { 2383 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2384 handleSipCase(propValue, typeCollection); 2385 } 2386 } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { 2387 final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue, 2388 mVCardType); 2389 handleAndroidCustomProperty(customPropertyList); 2390 } else { 2391 } 2392 // Be careful when adding some logic here, as some blocks above may use "return". 2393 } 2394 2395 /** 2396 * @param propValue may contain "sip:" at the beginning. 2397 * @param typeCollection 2398 */ 2399 private void handleSipCase(String propValue, Collection<String> typeCollection) { 2400 if (TextUtils.isEmpty(propValue)) { 2401 return; 2402 } 2403 if (propValue.startsWith("sip:")) { 2404 propValue = propValue.substring(4); 2405 if (propValue.length() == 0) { 2406 return; 2407 } 2408 } 2409 2410 int type = -1; 2411 String label = null; 2412 boolean isPrimary = false; 2413 if (typeCollection != null) { 2414 for (final String typeStringOrg : typeCollection) { 2415 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2416 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2417 isPrimary = true; 2418 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2419 type = SipAddress.TYPE_HOME; 2420 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) { 2421 type = SipAddress.TYPE_WORK; 2422 } else if (type < 0) { // If no other type is specified before 2423 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2424 label = typeStringOrg.substring(2); 2425 } else { 2426 label = typeStringOrg; 2427 } 2428 type = SipAddress.TYPE_CUSTOM; 2429 } 2430 } 2431 } 2432 if (type < 0) { 2433 type = SipAddress.TYPE_OTHER; 2434 } 2435 addSip(propValue, type, label, isPrimary); 2436 } 2437 2438 public void addChild(VCardEntry child) { 2439 if (mChildren == null) { 2440 mChildren = new ArrayList<VCardEntry>(); 2441 } 2442 mChildren.add(child); 2443 } 2444 2445 private void handleAndroidCustomProperty(final List<String> customPropertyList) { 2446 if (mAndroidCustomDataList == null) { 2447 mAndroidCustomDataList = new ArrayList<AndroidCustomData>(); 2448 } 2449 mAndroidCustomDataList 2450 .add(AndroidCustomData.constructAndroidCustomData(customPropertyList)); 2451 } 2452 2453 /** 2454 * Construct the display name. The constructed data must not be null. 2455 */ 2456 private String constructDisplayName() { 2457 String displayName = null; 2458 // FullName (created via "FN" or "NAME" field) is prefered. 2459 if (!TextUtils.isEmpty(mNameData.mFormatted)) { 2460 displayName = mNameData.mFormatted; 2461 } else if (!mNameData.emptyStructuredName()) { 2462 displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily, 2463 mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix); 2464 } else if (!mNameData.emptyPhoneticStructuredName()) { 2465 displayName = VCardUtils.constructNameFromElements(mVCardType, 2466 mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven); 2467 } else if (mEmailList != null && mEmailList.size() > 0) { 2468 displayName = mEmailList.get(0).mAddress; 2469 } else if (mPhoneList != null && mPhoneList.size() > 0) { 2470 displayName = mPhoneList.get(0).mNumber; 2471 } else if (mPostalList != null && mPostalList.size() > 0) { 2472 displayName = mPostalList.get(0).getFormattedAddress(mVCardType); 2473 } else if (mOrganizationList != null && mOrganizationList.size() > 0) { 2474 displayName = mOrganizationList.get(0).getFormattedString(); 2475 } 2476 if (displayName == null) { 2477 displayName = ""; 2478 } 2479 return displayName; 2480 } 2481 2482 /** 2483 * Consolidate several fielsds (like mName) using name candidates, 2484 */ 2485 public void consolidateFields() { 2486 mNameData.displayName = constructDisplayName(); 2487 } 2488 2489 /** 2490 * @return true when this object has nothing meaningful for Android's 2491 * Contacts, and thus is "ignorable" for Android's Contacts. This 2492 * does not mean an original vCard is really empty. Even when the 2493 * original vCard has some fields, this may ignore it if those 2494 * fields cannot be transcoded into Android's Contacts 2495 * representation. 2496 */ 2497 public boolean isIgnorable() { 2498 IsIgnorableIterator iterator = new IsIgnorableIterator(); 2499 iterateAllData(iterator); 2500 return iterator.getResult(); 2501 } 2502 2503 /** 2504 * Constructs the list of insert operation for this object. When the 2505 * operationList argument is null, this method creates a new ArrayList and 2506 * return it. The returned object is filled with new insert operations for 2507 * this object. When operationList argument is not null, this method appends 2508 * those new operations into the object instead of creating a new ArrayList. 2509 * 2510 * @param resolver {@link ContentResolver} object to be used in this method. 2511 * @param operationList object to be filled. You can use this argument to 2512 * concatinate operation lists. If null, this method creates a 2513 * new array object. 2514 * @return If operationList argument is null, new object with new insert 2515 * operations. If it is not null, the operationList object with 2516 * operations inserted by this method. 2517 */ 2518 public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver, 2519 ArrayList<ContentProviderOperation> operationList) { 2520 if (operationList == null) { 2521 operationList = new ArrayList<ContentProviderOperation>(); 2522 } 2523 2524 if (isIgnorable()) { 2525 return operationList; 2526 } 2527 2528 final int backReferenceIndex = operationList.size(); 2529 2530 // After applying the batch the first result's Uri is returned so it is important that 2531 // the RawContact is the first operation that gets inserted into the list. 2532 ContentProviderOperation.Builder builder = ContentProviderOperation 2533 .newInsert(RawContacts.CONTENT_URI); 2534 if (mAccount != null) { 2535 builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); 2536 builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); 2537 } else { 2538 builder.withValue(RawContacts.ACCOUNT_NAME, null); 2539 builder.withValue(RawContacts.ACCOUNT_TYPE, null); 2540 } 2541 operationList.add(builder.build()); 2542 2543 int start = operationList.size(); 2544 iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex)); 2545 int end = operationList.size(); 2546 2547 return operationList; 2548 } 2549 2550 public static VCardEntry buildFromResolver(ContentResolver resolver) { 2551 return buildFromResolver(resolver, Contacts.CONTENT_URI); 2552 } 2553 2554 public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { 2555 return null; 2556 } 2557 2558 private String listToString(List<String> list) { 2559 final int size = list.size(); 2560 if (size > 1) { 2561 StringBuilder builder = new StringBuilder(); 2562 int i = 0; 2563 for (String type : list) { 2564 builder.append(type); 2565 if (i < size - 1) { 2566 builder.append(";"); 2567 } 2568 } 2569 return builder.toString(); 2570 } else if (size == 1) { 2571 return list.get(0); 2572 } else { 2573 return ""; 2574 } 2575 } 2576 2577 public final NameData getNameData() { 2578 return mNameData; 2579 } 2580 2581 public final List<NicknameData> getNickNameList() { 2582 return mNicknameList; 2583 } 2584 2585 public final String getBirthday() { 2586 return mBirthday != null ? mBirthday.mBirthday : null; 2587 } 2588 2589 public final List<NoteData> getNotes() { 2590 return mNoteList; 2591 } 2592 2593 public final List<PhoneData> getPhoneList() { 2594 return mPhoneList; 2595 } 2596 2597 public final List<EmailData> getEmailList() { 2598 return mEmailList; 2599 } 2600 2601 public final List<PostalData> getPostalList() { 2602 return mPostalList; 2603 } 2604 2605 public final List<OrganizationData> getOrganizationList() { 2606 return mOrganizationList; 2607 } 2608 2609 public final List<ImData> getImList() { 2610 return mImList; 2611 } 2612 2613 public final List<PhotoData> getPhotoList() { 2614 return mPhotoList; 2615 } 2616 2617 public final List<WebsiteData> getWebsiteList() { 2618 return mWebsiteList; 2619 } 2620 2621 /** 2622 * @hide this interface may be changed for better support of vCard 4.0 (UID) 2623 */ 2624 public final List<VCardEntry> getChildlen() { 2625 return mChildren; 2626 } 2627 2628 public String getDisplayName() { 2629 if (mNameData.displayName == null) { 2630 mNameData.displayName = constructDisplayName(); 2631 } 2632 return mNameData.displayName; 2633 } 2634} 2635