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