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 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Collection; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 54 /** 55 * Represents one vCard entry, which should start with "BEGIN:VCARD" and end 56 * with "END:VCARD". This class is for bridging between real vCard data and 57 * Android's {@link ContactsContract}, which means some aspects of vCard are 58 * dropped before this object being constructed. Raw vCard data should be first 59 * supplied with {@link #addProperty(VCardProperty)}. After supplying all data, 60 * user should call {@link #consolidateFields()} to prepare some additional 61 * information which is constructable from supplied raw data. TODO: preserve raw 62 * data using {@link VCardProperty}. If it may just waste memory, this at least 63 * should contain them when it cannot convert vCard as a string to Android's 64 * Contacts representation. Those raw properties should _not_ be used for 65 * {@link #isIgnorable()}. 66 */ 67 public class VCardEntry { 68 private static final String LOG_TAG = VCardConstants.LOG_TAG; 69 70 private static final int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; 71 72 private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); 73 74 static { 75 sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); 76 sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); 77 sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); 78 sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); 79 sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); 80 sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); 81 sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); 82 sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, 83 Im.PROTOCOL_GOOGLE_TALK); 84 } 85 86 public enum EntryLabel { 87 NAME, 88 PHONE, 89 EMAIL, 90 POSTAL_ADDRESS, 91 ORGANIZATION, 92 IM, 93 PHOTO, 94 WEBSITE, 95 SIP, 96 NICKNAME, 97 NOTE, 98 BIRTHDAY, 99 ANNIVERSARY, 100 ANDROID_CUSTOM 101 } 102 103 public static interface EntryElement { 104 // Also need to inherit toString(), equals(). 105 public EntryLabel getEntryLabel(); 106 107 public void constructInsertOperation(List<ContentProviderOperation> operationList, 108 int backReferenceIndex); 109 110 public boolean isEmpty(); 111 } 112 113 // TODO: vCard 4.0 logically has multiple formatted names and we need to 114 // select the most preferable one using PREF parameter. 115 // 116 // e.g. (based on rev.13) 117 // FN;PREF=1:John M. Doe 118 // FN;PREF=2:John Doe 119 // FN;PREF=3;John 120 public static class NameData implements EntryElement { 121 private String mFamily; 122 private String mGiven; 123 private String mMiddle; 124 private String mPrefix; 125 private String mSuffix; 126 127 // Used only when no family nor given name is found. 128 private String mFormatted; 129 130 private String mPhoneticFamily; 131 private String mPhoneticGiven; 132 private String mPhoneticMiddle; 133 134 // For "SORT-STRING" in vCard 3.0. 135 private String mSortString; 136 137 /** 138 * Not in vCard but for {@link StructuredName#DISPLAY_NAME}. This field 139 * is constructed by VCardEntry on demand. Consider using 140 * {@link VCardEntry#getDisplayName()}. 141 */ 142 // This field should reflect the other Elem fields like Email, 143 // PostalAddress, etc., while 144 // This is static class which cannot see other data. Thus we ask 145 // VCardEntry to populate it. 146 public String displayName; 147 148 public boolean emptyStructuredName() { 149 return TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mGiven) 150 && TextUtils.isEmpty(mMiddle) && TextUtils.isEmpty(mPrefix) 151 && TextUtils.isEmpty(mSuffix); 152 } 153 154 public boolean emptyPhoneticStructuredName() { 155 return TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticGiven) 156 && TextUtils.isEmpty(mPhoneticMiddle); 157 } 158 159 @Override 160 public void constructInsertOperation(List<ContentProviderOperation> operationList, 161 int backReferenceIndex) { 162 final ContentProviderOperation.Builder builder = ContentProviderOperation 163 .newInsert(Data.CONTENT_URI); 164 builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, backReferenceIndex); 165 builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 166 167 if (!TextUtils.isEmpty(mGiven)) { 168 builder.withValue(StructuredName.GIVEN_NAME, mGiven); 169 } 170 if (!TextUtils.isEmpty(mFamily)) { 171 builder.withValue(StructuredName.FAMILY_NAME, mFamily); 172 } 173 if (!TextUtils.isEmpty(mMiddle)) { 174 builder.withValue(StructuredName.MIDDLE_NAME, mMiddle); 175 } 176 if (!TextUtils.isEmpty(mPrefix)) { 177 builder.withValue(StructuredName.PREFIX, mPrefix); 178 } 179 if (!TextUtils.isEmpty(mSuffix)) { 180 builder.withValue(StructuredName.SUFFIX, mSuffix); 181 } 182 183 boolean phoneticNameSpecified = false; 184 185 if (!TextUtils.isEmpty(mPhoneticGiven)) { 186 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGiven); 187 phoneticNameSpecified = true; 188 } 189 if (!TextUtils.isEmpty(mPhoneticFamily)) { 190 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamily); 191 phoneticNameSpecified = true; 192 } 193 if (!TextUtils.isEmpty(mPhoneticMiddle)) { 194 builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddle); 195 phoneticNameSpecified = true; 196 } 197 198 // SORT-STRING is used only when phonetic names aren't specified in 199 // the original vCard. 200 if (!phoneticNameSpecified) { 201 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mSortString); 202 } 203 204 builder.withValue(StructuredName.DISPLAY_NAME, displayName); 205 operationList.add(builder.build()); 206 } 207 208 @Override 209 public boolean isEmpty() { 210 return (TextUtils.isEmpty(mFamily) && TextUtils.isEmpty(mMiddle) 211 && TextUtils.isEmpty(mGiven) && TextUtils.isEmpty(mPrefix) 212 && TextUtils.isEmpty(mSuffix) && TextUtils.isEmpty(mFormatted) 213 && TextUtils.isEmpty(mPhoneticFamily) && TextUtils.isEmpty(mPhoneticMiddle) 214 && TextUtils.isEmpty(mPhoneticGiven) && TextUtils.isEmpty(mSortString)); 215 } 216 217 @Override 218 public boolean equals(Object obj) { 219 if (this == obj) { 220 return true; 221 } 222 if (!(obj instanceof NameData)) { 223 return false; 224 } 225 NameData nameData = (NameData) obj; 226 227 return (TextUtils.equals(mFamily, nameData.mFamily) 228 && TextUtils.equals(mMiddle, nameData.mMiddle) 229 && TextUtils.equals(mGiven, nameData.mGiven) 230 && TextUtils.equals(mPrefix, nameData.mPrefix) 231 && TextUtils.equals(mSuffix, nameData.mSuffix) 232 && TextUtils.equals(mFormatted, nameData.mFormatted) 233 && TextUtils.equals(mPhoneticFamily, nameData.mPhoneticFamily) 234 && TextUtils.equals(mPhoneticMiddle, nameData.mPhoneticMiddle) 235 && TextUtils.equals(mPhoneticGiven, nameData.mPhoneticGiven) 236 && TextUtils.equals(mSortString, nameData.mSortString)); 237 } 238 239 @Override 240 public int hashCode() { 241 final String[] hashTargets = new String[] {mFamily, mMiddle, mGiven, mPrefix, mSuffix, 242 mFormatted, mPhoneticFamily, mPhoneticMiddle, 243 mPhoneticGiven, mSortString}; 244 int hash = 0; 245 for (String hashTarget : hashTargets) { 246 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0); 247 } 248 return hash; 249 } 250 251 @Override 252 public String toString() { 253 return String.format("family: %s, given: %s, middle: %s, prefix: %s, suffix: %s", 254 mFamily, mGiven, mMiddle, mPrefix, mSuffix); 255 } 256 257 @Override 258 public final EntryLabel getEntryLabel() { 259 return EntryLabel.NAME; 260 } 261 262 public String getFamily() { 263 return mFamily; 264 } 265 266 public String getMiddle() { 267 return mMiddle; 268 } 269 270 public String getGiven() { 271 return mGiven; 272 } 273 274 public String getPrefix() { 275 return mPrefix; 276 } 277 278 public String getSuffix() { 279 return mSuffix; 280 } 281 282 public String getFormatted() { 283 return mFormatted; 284 } 285 286 public String getSortString() { 287 return mSortString; 288 } 289 290 /** @hide Just for testing. */ 291 public void setFamily(String family) { mFamily = family; } 292 /** @hide Just for testing. */ 293 public void setMiddle(String middle) { mMiddle = middle; } 294 /** @hide Just for testing. */ 295 public void setGiven(String given) { mGiven = given; } 296 /** @hide Just for testing. */ 297 public void setPrefix(String prefix) { mPrefix = prefix; } 298 /** @hide Just for testing. */ 299 public void setSuffix(String suffix) { mSuffix = suffix; } 300 } 301 302 public static class PhoneData implements EntryElement { 303 private final String mNumber; 304 private final int mType; 305 private final String mLabel; 306 307 // isPrimary is (not final but) changable, only when there's no 308 // appropriate one existing 309 // in the original VCard. 310 private boolean mIsPrimary; 311 312 public PhoneData(String data, int type, String label, boolean isPrimary) { 313 mNumber = data; 314 mType = type; 315 mLabel = label; 316 mIsPrimary = isPrimary; 317 } 318 319 @Override 320 public void constructInsertOperation(List<ContentProviderOperation> operationList, 321 int backReferenceIndex) { 322 final ContentProviderOperation.Builder builder = ContentProviderOperation 323 .newInsert(Data.CONTENT_URI); 324 builder.withValueBackReference(Phone.RAW_CONTACT_ID, backReferenceIndex); 325 builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 326 327 builder.withValue(Phone.TYPE, mType); 328 if (mType == Phone.TYPE_CUSTOM) { 329 builder.withValue(Phone.LABEL, mLabel); 330 } 331 builder.withValue(Phone.NUMBER, mNumber); 332 if (mIsPrimary) { 333 builder.withValue(Phone.IS_PRIMARY, 1); 334 } 335 operationList.add(builder.build()); 336 } 337 338 @Override 339 public boolean isEmpty() { 340 return TextUtils.isEmpty(mNumber); 341 } 342 343 @Override 344 public boolean equals(Object obj) { 345 if (this == obj) { 346 return true; 347 } 348 if (!(obj instanceof PhoneData)) { 349 return false; 350 } 351 PhoneData phoneData = (PhoneData) obj; 352 return (mType == phoneData.mType 353 && TextUtils.equals(mNumber, phoneData.mNumber) 354 && TextUtils.equals(mLabel, phoneData.mLabel) 355 && (mIsPrimary == phoneData.mIsPrimary)); 356 } 357 358 @Override 359 public int hashCode() { 360 int hash = mType; 361 hash = hash * 31 + (mNumber != null ? mNumber.hashCode() : 0); 362 hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0); 363 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 364 return hash; 365 } 366 367 @Override 368 public String toString() { 369 return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mNumber, 370 mLabel, mIsPrimary); 371 } 372 373 @Override 374 public final EntryLabel getEntryLabel() { 375 return EntryLabel.PHONE; 376 } 377 378 public String getNumber() { 379 return mNumber; 380 } 381 382 public int getType() { 383 return mType; 384 } 385 386 public String getLabel() { 387 return mLabel; 388 } 389 390 public boolean isPrimary() { 391 return mIsPrimary; 392 } 393 } 394 395 public static class EmailData implements EntryElement { 396 private final String mAddress; 397 private final int mType; 398 // Used only when TYPE is TYPE_CUSTOM. 399 private final String mLabel; 400 private final boolean mIsPrimary; 401 402 public EmailData(String data, int type, String label, boolean isPrimary) { 403 mType = type; 404 mAddress = data; 405 mLabel = label; 406 mIsPrimary = isPrimary; 407 } 408 409 @Override 410 public void constructInsertOperation(List<ContentProviderOperation> operationList, 411 int backReferenceIndex) { 412 final ContentProviderOperation.Builder builder = ContentProviderOperation 413 .newInsert(Data.CONTENT_URI); 414 builder.withValueBackReference(Email.RAW_CONTACT_ID, backReferenceIndex); 415 builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 416 417 builder.withValue(Email.TYPE, mType); 418 if (mType == Email.TYPE_CUSTOM) { 419 builder.withValue(Email.LABEL, mLabel); 420 } 421 builder.withValue(Email.DATA, mAddress); 422 if (mIsPrimary) { 423 builder.withValue(Data.IS_PRIMARY, 1); 424 } 425 operationList.add(builder.build()); 426 } 427 428 @Override 429 public boolean isEmpty() { 430 return TextUtils.isEmpty(mAddress); 431 } 432 433 @Override 434 public boolean equals(Object obj) { 435 if (this == obj) { 436 return true; 437 } 438 if (!(obj instanceof EmailData)) { 439 return false; 440 } 441 EmailData emailData = (EmailData) obj; 442 return (mType == emailData.mType 443 && TextUtils.equals(mAddress, emailData.mAddress) 444 && TextUtils.equals(mLabel, emailData.mLabel) 445 && (mIsPrimary == emailData.mIsPrimary)); 446 } 447 448 @Override 449 public int hashCode() { 450 int hash = mType; 451 hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0); 452 hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0); 453 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 454 return hash; 455 } 456 457 @Override 458 public String toString() { 459 return String.format("type: %d, data: %s, label: %s, isPrimary: %s", mType, mAddress, 460 mLabel, mIsPrimary); 461 } 462 463 @Override 464 public final EntryLabel getEntryLabel() { 465 return EntryLabel.EMAIL; 466 } 467 468 public String getAddress() { 469 return mAddress; 470 } 471 472 public int getType() { 473 return mType; 474 } 475 476 public String getLabel() { 477 return mLabel; 478 } 479 480 public boolean isPrimary() { 481 return mIsPrimary; 482 } 483 } 484 485 public static class PostalData implements EntryElement { 486 // Determined by vCard specification. 487 // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name 488 private static final int ADDR_MAX_DATA_SIZE = 7; 489 private final String mPobox; 490 private final String mExtendedAddress; 491 private final String mStreet; 492 private final String mLocalty; 493 private final String mRegion; 494 private final String mPostalCode; 495 private final String mCountry; 496 private final int mType; 497 private final String mLabel; 498 private boolean mIsPrimary; 499 500 /** We keep this for {@link StructuredPostal#FORMATTED_ADDRESS} */ 501 // TODO: need better way to construct formatted address. 502 private int mVCardType; 503 504 public PostalData(String pobox, String extendedAddress, String street, String localty, 505 String region, String postalCode, String country, int type, String label, 506 boolean isPrimary, int vcardType) { 507 mType = type; 508 mPobox = pobox; 509 mExtendedAddress = extendedAddress; 510 mStreet = street; 511 mLocalty = localty; 512 mRegion = region; 513 mPostalCode = postalCode; 514 mCountry = country; 515 mLabel = label; 516 mIsPrimary = isPrimary; 517 mVCardType = vcardType; 518 } 519 520 /** 521 * Accepts raw propertyValueList in vCard and constructs PostalData. 522 */ 523 public static PostalData constructPostalData(final List<String> propValueList, 524 final int type, final String label, boolean isPrimary, int vcardType) { 525 final String[] dataArray = new String[ADDR_MAX_DATA_SIZE]; 526 527 int size = propValueList.size(); 528 if (size > ADDR_MAX_DATA_SIZE) { 529 size = ADDR_MAX_DATA_SIZE; 530 } 531 532 // adr-value = 0*6(text-value ";") text-value 533 // ; PO Box, Extended Address, Street, Locality, Region, Postal Code, Country Name 534 // 535 // Use Iterator assuming List may be LinkedList, though actually it is 536 // always ArrayList in the current implementation. 537 int i = 0; 538 for (String addressElement : propValueList) { 539 dataArray[i] = addressElement; 540 if (++i >= size) { 541 break; 542 } 543 } 544 while (i < ADDR_MAX_DATA_SIZE) { 545 dataArray[i++] = null; 546 } 547 548 return new PostalData(dataArray[0], dataArray[1], dataArray[2], dataArray[3], 549 dataArray[4], dataArray[5], dataArray[6], type, label, isPrimary, vcardType); 550 } 551 552 @Override 553 public void constructInsertOperation(List<ContentProviderOperation> operationList, 554 int backReferenceIndex) { 555 final ContentProviderOperation.Builder builder = ContentProviderOperation 556 .newInsert(Data.CONTENT_URI); 557 builder.withValueBackReference(StructuredPostal.RAW_CONTACT_ID, backReferenceIndex); 558 builder.withValue(Data.MIMETYPE, StructuredPostal.CONTENT_ITEM_TYPE); 559 560 builder.withValue(StructuredPostal.TYPE, mType); 561 if (mType == StructuredPostal.TYPE_CUSTOM) { 562 builder.withValue(StructuredPostal.LABEL, mLabel); 563 } 564 565 final String streetString; 566 if (TextUtils.isEmpty(mStreet)) { 567 if (TextUtils.isEmpty(mExtendedAddress)) { 568 streetString = null; 569 } else { 570 streetString = mExtendedAddress; 571 } 572 } else { 573 if (TextUtils.isEmpty(mExtendedAddress)) { 574 streetString = mStreet; 575 } else { 576 streetString = mStreet + " " + mExtendedAddress; 577 } 578 } 579 builder.withValue(StructuredPostal.POBOX, mPobox); 580 builder.withValue(StructuredPostal.STREET, streetString); 581 builder.withValue(StructuredPostal.CITY, mLocalty); 582 builder.withValue(StructuredPostal.REGION, mRegion); 583 builder.withValue(StructuredPostal.POSTCODE, mPostalCode); 584 builder.withValue(StructuredPostal.COUNTRY, mCountry); 585 586 builder.withValue(StructuredPostal.FORMATTED_ADDRESS, getFormattedAddress(mVCardType)); 587 if (mIsPrimary) { 588 builder.withValue(Data.IS_PRIMARY, 1); 589 } 590 operationList.add(builder.build()); 591 } 592 593 public String getFormattedAddress(final int vcardType) { 594 StringBuilder builder = new StringBuilder(); 595 boolean empty = true; 596 final String[] dataArray = new String[] { 597 mPobox, mExtendedAddress, mStreet, mLocalty, mRegion, mPostalCode, mCountry 598 }; 599 if (VCardConfig.isJapaneseDevice(vcardType)) { 600 // In Japan, the order is reversed. 601 for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { 602 String addressPart = dataArray[i]; 603 if (!TextUtils.isEmpty(addressPart)) { 604 if (!empty) { 605 builder.append(' '); 606 } else { 607 empty = false; 608 } 609 builder.append(addressPart); 610 } 611 } 612 } else { 613 for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { 614 String addressPart = dataArray[i]; 615 if (!TextUtils.isEmpty(addressPart)) { 616 if (!empty) { 617 builder.append(' '); 618 } else { 619 empty = false; 620 } 621 builder.append(addressPart); 622 } 623 } 624 } 625 626 return builder.toString().trim(); 627 } 628 629 @Override 630 public boolean isEmpty() { 631 return (TextUtils.isEmpty(mPobox) 632 && TextUtils.isEmpty(mExtendedAddress) 633 && TextUtils.isEmpty(mStreet) 634 && TextUtils.isEmpty(mLocalty) 635 && TextUtils.isEmpty(mRegion) 636 && TextUtils.isEmpty(mPostalCode) 637 && TextUtils.isEmpty(mCountry)); 638 } 639 640 @Override 641 public boolean equals(Object obj) { 642 if (this == obj) { 643 return true; 644 } 645 if (!(obj instanceof PostalData)) { 646 return false; 647 } 648 final PostalData postalData = (PostalData) obj; 649 return (mType == postalData.mType) 650 && (mType == StructuredPostal.TYPE_CUSTOM ? TextUtils.equals(mLabel, 651 postalData.mLabel) : true) 652 && (mIsPrimary == postalData.mIsPrimary) 653 && TextUtils.equals(mPobox, postalData.mPobox) 654 && TextUtils.equals(mExtendedAddress, postalData.mExtendedAddress) 655 && TextUtils.equals(mStreet, postalData.mStreet) 656 && TextUtils.equals(mLocalty, postalData.mLocalty) 657 && TextUtils.equals(mRegion, postalData.mRegion) 658 && TextUtils.equals(mPostalCode, postalData.mPostalCode) 659 && TextUtils.equals(mCountry, postalData.mCountry); 660 } 661 662 @Override 663 public int hashCode() { 664 int hash = mType; 665 hash = hash * 31 + (mLabel != null ? mLabel.hashCode() : 0); 666 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 667 668 final String[] hashTargets = new String[] {mPobox, mExtendedAddress, mStreet, 669 mLocalty, mRegion, mPostalCode, mCountry}; 670 for (String hashTarget : hashTargets) { 671 hash = hash * 31 + (hashTarget != null ? hashTarget.hashCode() : 0); 672 } 673 return hash; 674 } 675 676 @Override 677 public String toString() { 678 return String.format("type: %d, label: %s, isPrimary: %s, pobox: %s, " 679 + "extendedAddress: %s, street: %s, localty: %s, region: %s, postalCode %s, " 680 + "country: %s", mType, mLabel, mIsPrimary, mPobox, mExtendedAddress, mStreet, 681 mLocalty, mRegion, mPostalCode, mCountry); 682 } 683 684 @Override 685 public final EntryLabel getEntryLabel() { 686 return EntryLabel.POSTAL_ADDRESS; 687 } 688 689 public String getPobox() { 690 return mPobox; 691 } 692 693 public String getExtendedAddress() { 694 return mExtendedAddress; 695 } 696 697 public String getStreet() { 698 return mStreet; 699 } 700 701 public String getLocalty() { 702 return mLocalty; 703 } 704 705 public String getRegion() { 706 return mRegion; 707 } 708 709 public String getPostalCode() { 710 return mPostalCode; 711 } 712 713 public String getCountry() { 714 return mCountry; 715 } 716 717 public int getType() { 718 return mType; 719 } 720 721 public String getLabel() { 722 return mLabel; 723 } 724 725 public boolean isPrimary() { 726 return mIsPrimary; 727 } 728 } 729 730 public static class OrganizationData implements EntryElement { 731 // non-final is Intentional: we may change the values since this info is separated into 732 // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in different 733 // timing. 734 private String mOrganizationName; 735 private String mDepartmentName; 736 private String mTitle; 737 private final String mPhoneticName; // We won't have this in "TITLE" property. 738 private final int mType; 739 private boolean mIsPrimary; 740 741 public OrganizationData(final String organizationName, final String departmentName, 742 final String titleName, final String phoneticName, int type, 743 final boolean isPrimary) { 744 mType = type; 745 mOrganizationName = organizationName; 746 mDepartmentName = departmentName; 747 mTitle = titleName; 748 mPhoneticName = phoneticName; 749 mIsPrimary = isPrimary; 750 } 751 752 public String getFormattedString() { 753 final StringBuilder builder = new StringBuilder(); 754 if (!TextUtils.isEmpty(mOrganizationName)) { 755 builder.append(mOrganizationName); 756 } 757 758 if (!TextUtils.isEmpty(mDepartmentName)) { 759 if (builder.length() > 0) { 760 builder.append(", "); 761 } 762 builder.append(mDepartmentName); 763 } 764 765 if (!TextUtils.isEmpty(mTitle)) { 766 if (builder.length() > 0) { 767 builder.append(", "); 768 } 769 builder.append(mTitle); 770 } 771 772 return builder.toString(); 773 } 774 775 @Override 776 public void constructInsertOperation(List<ContentProviderOperation> operationList, 777 int backReferenceIndex) { 778 final ContentProviderOperation.Builder builder = ContentProviderOperation 779 .newInsert(Data.CONTENT_URI); 780 builder.withValueBackReference(Organization.RAW_CONTACT_ID, backReferenceIndex); 781 builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); 782 builder.withValue(Organization.TYPE, mType); 783 if (mOrganizationName != null) { 784 builder.withValue(Organization.COMPANY, mOrganizationName); 785 } 786 if (mDepartmentName != null) { 787 builder.withValue(Organization.DEPARTMENT, mDepartmentName); 788 } 789 if (mTitle != null) { 790 builder.withValue(Organization.TITLE, mTitle); 791 } 792 if (mPhoneticName != null) { 793 builder.withValue(Organization.PHONETIC_NAME, mPhoneticName); 794 } 795 if (mIsPrimary) { 796 builder.withValue(Organization.IS_PRIMARY, 1); 797 } 798 operationList.add(builder.build()); 799 } 800 801 @Override 802 public boolean isEmpty() { 803 return TextUtils.isEmpty(mOrganizationName) && TextUtils.isEmpty(mDepartmentName) 804 && TextUtils.isEmpty(mTitle) && TextUtils.isEmpty(mPhoneticName); 805 } 806 807 @Override 808 public boolean equals(Object obj) { 809 if (this == obj) { 810 return true; 811 } 812 if (!(obj instanceof OrganizationData)) { 813 return false; 814 } 815 OrganizationData organization = (OrganizationData) obj; 816 return (mType == organization.mType 817 && TextUtils.equals(mOrganizationName, organization.mOrganizationName) 818 && TextUtils.equals(mDepartmentName, organization.mDepartmentName) 819 && TextUtils.equals(mTitle, organization.mTitle) 820 && (mIsPrimary == organization.mIsPrimary)); 821 } 822 823 @Override 824 public int hashCode() { 825 int hash = mType; 826 hash = hash * 31 + (mOrganizationName != null ? mOrganizationName.hashCode() : 0); 827 hash = hash * 31 + (mDepartmentName != null ? mDepartmentName.hashCode() : 0); 828 hash = hash * 31 + (mTitle != null ? mTitle.hashCode() : 0); 829 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 830 return hash; 831 } 832 833 @Override 834 public String toString() { 835 return String.format( 836 "type: %d, organization: %s, department: %s, title: %s, isPrimary: %s", mType, 837 mOrganizationName, mDepartmentName, mTitle, mIsPrimary); 838 } 839 840 @Override 841 public final EntryLabel getEntryLabel() { 842 return EntryLabel.ORGANIZATION; 843 } 844 845 public String getOrganizationName() { 846 return mOrganizationName; 847 } 848 849 public String getDepartmentName() { 850 return mDepartmentName; 851 } 852 853 public String getTitle() { 854 return mTitle; 855 } 856 857 public String getPhoneticName() { 858 return mPhoneticName; 859 } 860 861 public int getType() { 862 return mType; 863 } 864 865 public boolean isPrimary() { 866 return mIsPrimary; 867 } 868 } 869 870 public static class ImData implements EntryElement { 871 private final String mAddress; 872 private final int mProtocol; 873 private final String mCustomProtocol; 874 private final int mType; 875 private final boolean mIsPrimary; 876 877 public ImData(final int protocol, final String customProtocol, final String address, 878 final int type, final boolean isPrimary) { 879 mProtocol = protocol; 880 mCustomProtocol = customProtocol; 881 mType = type; 882 mAddress = address; 883 mIsPrimary = isPrimary; 884 } 885 886 @Override 887 public void constructInsertOperation(List<ContentProviderOperation> operationList, 888 int backReferenceIndex) { 889 final ContentProviderOperation.Builder builder = ContentProviderOperation 890 .newInsert(Data.CONTENT_URI); 891 builder.withValueBackReference(Im.RAW_CONTACT_ID, backReferenceIndex); 892 builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); 893 builder.withValue(Im.TYPE, mType); 894 builder.withValue(Im.PROTOCOL, mProtocol); 895 builder.withValue(Im.DATA, mAddress); 896 if (mProtocol == Im.PROTOCOL_CUSTOM) { 897 builder.withValue(Im.CUSTOM_PROTOCOL, mCustomProtocol); 898 } 899 if (mIsPrimary) { 900 builder.withValue(Data.IS_PRIMARY, 1); 901 } 902 operationList.add(builder.build()); 903 } 904 905 @Override 906 public boolean isEmpty() { 907 return TextUtils.isEmpty(mAddress); 908 } 909 910 @Override 911 public boolean equals(Object obj) { 912 if (this == obj) { 913 return true; 914 } 915 if (!(obj instanceof ImData)) { 916 return false; 917 } 918 ImData imData = (ImData) obj; 919 return (mType == imData.mType 920 && mProtocol == imData.mProtocol 921 && TextUtils.equals(mCustomProtocol, imData.mCustomProtocol) 922 && TextUtils.equals(mAddress, imData.mAddress) 923 && (mIsPrimary == imData.mIsPrimary)); 924 } 925 926 @Override 927 public int hashCode() { 928 int hash = mType; 929 hash = hash * 31 + mProtocol; 930 hash = hash * 31 + (mCustomProtocol != null ? mCustomProtocol.hashCode() : 0); 931 hash = hash * 31 + (mAddress != null ? mAddress.hashCode() : 0); 932 hash = hash * 31 + (mIsPrimary ? 1231 : 1237); 933 return hash; 934 } 935 936 @Override 937 public String toString() { 938 return String.format( 939 "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", mType, 940 mProtocol, mCustomProtocol, mAddress, mIsPrimary); 941 } 942 943 @Override 944 public final EntryLabel getEntryLabel() { 945 return EntryLabel.IM; 946 } 947 948 public String getAddress() { 949 return mAddress; 950 } 951 952 /** 953 * One of the value available for {@link Im#PROTOCOL}. e.g. 954 * {@link Im#PROTOCOL_GOOGLE_TALK} 955 */ 956 public int getProtocol() { 957 return mProtocol; 958 } 959 960 public String getCustomProtocol() { 961 return mCustomProtocol; 962 } 963 964 public int getType() { 965 return mType; 966 } 967 968 public boolean isPrimary() { 969 return mIsPrimary; 970 } 971 } 972 973 public static class PhotoData implements EntryElement { 974 // private static final String FORMAT_FLASH = "SWF"; 975 976 // used when type is not defined in ContactsContract. 977 private final String mFormat; 978 private final boolean mIsPrimary; 979 980 private final byte[] mBytes; 981 982 private Integer mHashCode = null; 983 984 public PhotoData(String format, byte[] photoBytes, boolean isPrimary) { 985 mFormat = format; 986 mBytes = 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, mBytes); 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 mBytes == null || mBytes.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(mBytes, photoData.mBytes) 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 (mBytes != null) { 1032 for (byte b : mBytes) { 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, mBytes.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 mBytes; 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 trimmed = data.trim(); 1771 final String formattedNumber; 1772 if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { 1773 formattedNumber = trimmed; 1774 } else { 1775 // TODO: from the view of vCard spec these auto conversions should be removed. 1776 // Note that some other codes (like the phone number formatter) or modules expect this 1777 // auto conversion (bug 5178723), so just omitting this code won't be preferable enough 1778 // (bug 4177894) 1779 boolean hasPauseOrWait = false; 1780 final int length = trimmed.length(); 1781 for (int i = 0; i < length; i++) { 1782 char ch = trimmed.charAt(i); 1783 // See RFC 3601 and docs for PhoneNumberUtils for more info. 1784 if (ch == 'p' || ch == 'P') { 1785 builder.append(PhoneNumberUtils.PAUSE); 1786 hasPauseOrWait = true; 1787 } else if (ch == 'w' || ch == 'W') { 1788 builder.append(PhoneNumberUtils.WAIT); 1789 hasPauseOrWait = true; 1790 } else if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { 1791 builder.append(ch); 1792 } 1793 } 1794 if (!hasPauseOrWait) { 1795 final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); 1796 formattedNumber = PhoneNumberUtilsPort.formatNumber( 1797 builder.toString(), formattingType); 1798 } else { 1799 formattedNumber = builder.toString(); 1800 } 1801 } 1802 PhoneData phoneData = new PhoneData(formattedNumber, type, label, isPrimary); 1803 mPhoneList.add(phoneData); 1804 } 1805 1806 private void addSip(String sipData, int type, String label, boolean isPrimary) { 1807 if (mSipList == null) { 1808 mSipList = new ArrayList<SipData>(); 1809 } 1810 mSipList.add(new SipData(sipData, type, label, isPrimary)); 1811 } 1812 1813 private void addNickName(final String nickName) { 1814 if (mNicknameList == null) { 1815 mNicknameList = new ArrayList<NicknameData>(); 1816 } 1817 mNicknameList.add(new NicknameData(nickName)); 1818 } 1819 1820 private void addEmail(int type, String data, String label, boolean isPrimary) { 1821 if (mEmailList == null) { 1822 mEmailList = new ArrayList<EmailData>(); 1823 } 1824 mEmailList.add(new EmailData(data, type, label, isPrimary)); 1825 } 1826 1827 private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary) { 1828 if (mPostalList == null) { 1829 mPostalList = new ArrayList<PostalData>(0); 1830 } 1831 mPostalList.add(PostalData.constructPostalData(propValueList, type, label, isPrimary, 1832 mVCardType)); 1833 } 1834 1835 /** 1836 * Should be called via {@link #handleOrgValue(int, List, Map, boolean)} or 1837 * {@link #handleTitleValue(String)}. 1838 */ 1839 private void addNewOrganization(final String organizationName, final String departmentName, 1840 final String titleName, final String phoneticName, int type, final boolean isPrimary) { 1841 if (mOrganizationList == null) { 1842 mOrganizationList = new ArrayList<OrganizationData>(); 1843 } 1844 mOrganizationList.add(new OrganizationData(organizationName, departmentName, titleName, 1845 phoneticName, type, isPrimary)); 1846 } 1847 1848 private static final List<String> sEmptyList = Collections 1849 .unmodifiableList(new ArrayList<String>(0)); 1850 1851 private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) { 1852 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 1853 if (sortAsCollection != null && sortAsCollection.size() != 0) { 1854 if (sortAsCollection.size() > 1) { 1855 Log.w(LOG_TAG, 1856 "Incorrect multiple SORT_AS parameters detected: " 1857 + Arrays.toString(sortAsCollection.toArray())); 1858 } 1859 final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection 1860 .iterator().next(), mVCardType); 1861 final StringBuilder builder = new StringBuilder(); 1862 for (final String elem : sortNames) { 1863 builder.append(elem); 1864 } 1865 return builder.toString(); 1866 } else { 1867 return null; 1868 } 1869 } 1870 1871 /** 1872 * Set "ORG" related values to the appropriate data. If there's more than 1873 * one {@link OrganizationData} objects, this input data are attached to the 1874 * last one which does not have valid values (not including empty but only 1875 * null). If there's no {@link OrganizationData} object, a new 1876 * {@link OrganizationData} is created, whose title is set to null. 1877 */ 1878 private void handleOrgValue(final int type, List<String> orgList, 1879 Map<String, Collection<String>> paramMap, boolean isPrimary) { 1880 final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap); 1881 if (orgList == null) { 1882 orgList = sEmptyList; 1883 } 1884 final String organizationName; 1885 final String departmentName; 1886 final int size = orgList.size(); 1887 switch (size) { 1888 case 0: { 1889 organizationName = ""; 1890 departmentName = null; 1891 break; 1892 } 1893 case 1: { 1894 organizationName = orgList.get(0); 1895 departmentName = null; 1896 break; 1897 } 1898 default: { // More than 1. 1899 organizationName = orgList.get(0); 1900 // We're not sure which is the correct string for department. 1901 // In order to keep all the data, concatinate the rest of elements. 1902 StringBuilder builder = new StringBuilder(); 1903 for (int i = 1; i < size; i++) { 1904 if (i > 1) { 1905 builder.append(' '); 1906 } 1907 builder.append(orgList.get(i)); 1908 } 1909 departmentName = builder.toString(); 1910 } 1911 } 1912 if (mOrganizationList == null) { 1913 // Create new first organization entry, with "null" title which may be 1914 // added via handleTitleValue(). 1915 addNewOrganization(organizationName, departmentName, null, phoneticName, type, 1916 isPrimary); 1917 return; 1918 } 1919 for (OrganizationData organizationData : mOrganizationList) { 1920 // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. 1921 // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. 1922 if (organizationData.mOrganizationName == null 1923 && organizationData.mDepartmentName == null) { 1924 // Probably the "TITLE" property comes before the "ORG" property via 1925 // handleTitleLine(). 1926 organizationData.mOrganizationName = organizationName; 1927 organizationData.mDepartmentName = departmentName; 1928 organizationData.mIsPrimary = isPrimary; 1929 return; 1930 } 1931 } 1932 // No OrganizatioData is available. Create another one, with "null" title, which may be 1933 // added via handleTitleValue(). 1934 addNewOrganization(organizationName, departmentName, null, phoneticName, type, isPrimary); 1935 } 1936 1937 /** 1938 * Set "title" value to the appropriate data. If there's more than one 1939 * OrganizationData objects, this input is attached to the last one which 1940 * does not have valid title value (not including empty but only null). If 1941 * there's no OrganizationData object, a new OrganizationData is created, 1942 * whose company name is set to null. 1943 */ 1944 private void handleTitleValue(final String title) { 1945 if (mOrganizationList == null) { 1946 // Create new first organization entry, with "null" other info, which may be 1947 // added via handleOrgValue(). 1948 addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false); 1949 return; 1950 } 1951 for (OrganizationData organizationData : mOrganizationList) { 1952 if (organizationData.mTitle == null) { 1953 organizationData.mTitle = title; 1954 return; 1955 } 1956 } 1957 // No Organization is available. Create another one, with "null" other info, which may be 1958 // added via handleOrgValue(). 1959 addNewOrganization(null, null, title, null, DEFAULT_ORGANIZATION_TYPE, false); 1960 } 1961 1962 private void addIm(int protocol, String customProtocol, String propValue, int type, 1963 boolean isPrimary) { 1964 if (mImList == null) { 1965 mImList = new ArrayList<ImData>(); 1966 } 1967 mImList.add(new ImData(protocol, customProtocol, propValue, type, isPrimary)); 1968 } 1969 1970 private void addNote(final String note) { 1971 if (mNoteList == null) { 1972 mNoteList = new ArrayList<NoteData>(1); 1973 } 1974 mNoteList.add(new NoteData(note)); 1975 } 1976 1977 private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { 1978 if (mPhotoList == null) { 1979 mPhotoList = new ArrayList<PhotoData>(1); 1980 } 1981 final PhotoData photoData = new PhotoData(formatName, photoBytes, isPrimary); 1982 mPhotoList.add(photoData); 1983 } 1984 1985 /** 1986 * Tries to extract paramMap, constructs SORT-AS parameter values, and store 1987 * them in appropriate phonetic name variables. This method does not care 1988 * the vCard version. Even when we have SORT-AS parameters in invalid 1989 * versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't 1990 * drop meaningful information. If we had this parameter in the N field of 1991 * vCard 3.0, and the contact data also have SORT-STRING, we will prefer 1992 * SORT-STRING, since it is regitimate property to be understood. 1993 */ 1994 private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) { 1995 if (VCardConfig.isVersion30(mVCardType) 1996 && !(TextUtils.isEmpty(mNameData.mPhoneticFamily) 1997 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils 1998 .isEmpty(mNameData.mPhoneticGiven))) { 1999 return; 2000 } 2001 2002 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 2003 if (sortAsCollection != null && sortAsCollection.size() != 0) { 2004 if (sortAsCollection.size() > 1) { 2005 Log.w(LOG_TAG, 2006 "Incorrect multiple SORT_AS parameters detected: " 2007 + Arrays.toString(sortAsCollection.toArray())); 2008 } 2009 final List<String> sortNames = VCardUtils.constructListFromValue(sortAsCollection 2010 .iterator().next(), mVCardType); 2011 int size = sortNames.size(); 2012 if (size > 3) { 2013 size = 3; 2014 } 2015 switch (size) { 2016 case 3: 2017 mNameData.mPhoneticMiddle = sortNames.get(2); //$FALL-THROUGH$ 2018 case 2: 2019 mNameData.mPhoneticGiven = sortNames.get(1); //$FALL-THROUGH$ 2020 default: 2021 mNameData.mPhoneticFamily = sortNames.get(0); 2022 break; 2023 } 2024 } 2025 } 2026 2027 @SuppressWarnings("fallthrough") 2028 private void handleNProperty(final List<String> paramValues, 2029 Map<String, Collection<String>> paramMap) { 2030 // in vCard 4.0, SORT-AS parameter is available. 2031 tryHandleSortAsName(paramMap); 2032 2033 // Family, Given, Middle, Prefix, Suffix. (1 - 5) 2034 int size; 2035 if (paramValues == null || (size = paramValues.size()) < 1) { 2036 return; 2037 } 2038 if (size > 5) { 2039 size = 5; 2040 } 2041 2042 switch (size) { 2043 // Fall-through. 2044 case 5: 2045 mNameData.mSuffix = paramValues.get(4); 2046 case 4: 2047 mNameData.mPrefix = paramValues.get(3); 2048 case 3: 2049 mNameData.mMiddle = paramValues.get(2); 2050 case 2: 2051 mNameData.mGiven = paramValues.get(1); 2052 default: 2053 mNameData.mFamily = paramValues.get(0); 2054 } 2055 } 2056 2057 /** 2058 * Note: Some Japanese mobile phones use this field for phonetic name, since 2059 * vCard 2.1 does not have "SORT-STRING" type. Also, in some cases, the 2060 * field has some ';'s in it. Assume the ';' means the same meaning in N 2061 * property 2062 */ 2063 @SuppressWarnings("fallthrough") 2064 private void handlePhoneticNameFromSound(List<String> elems) { 2065 if (!(TextUtils.isEmpty(mNameData.mPhoneticFamily) 2066 && TextUtils.isEmpty(mNameData.mPhoneticMiddle) && TextUtils 2067 .isEmpty(mNameData.mPhoneticGiven))) { 2068 // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. 2069 // Ignore "SOUND;X-IRMC-N". 2070 return; 2071 } 2072 2073 int size; 2074 if (elems == null || (size = elems.size()) < 1) { 2075 return; 2076 } 2077 2078 // Assume that the order is "Family, Given, Middle". 2079 // This is not from specification but mere assumption. Some Japanese 2080 // phones use this order. 2081 if (size > 3) { 2082 size = 3; 2083 } 2084 2085 if (elems.get(0).length() > 0) { 2086 boolean onlyFirstElemIsNonEmpty = true; 2087 for (int i = 1; i < size; i++) { 2088 if (elems.get(i).length() > 0) { 2089 onlyFirstElemIsNonEmpty = false; 2090 break; 2091 } 2092 } 2093 if (onlyFirstElemIsNonEmpty) { 2094 final String[] namesArray = elems.get(0).split(" "); 2095 final int nameArrayLength = namesArray.length; 2096 if (nameArrayLength == 3) { 2097 // Assume the string is "Family Middle Given". 2098 mNameData.mPhoneticFamily = namesArray[0]; 2099 mNameData.mPhoneticMiddle = namesArray[1]; 2100 mNameData.mPhoneticGiven = namesArray[2]; 2101 } else if (nameArrayLength == 2) { 2102 // Assume the string is "Family Given" based on the Japanese mobile 2103 // phones' preference. 2104 mNameData.mPhoneticFamily = namesArray[0]; 2105 mNameData.mPhoneticGiven = namesArray[1]; 2106 } else { 2107 mNameData.mPhoneticGiven = elems.get(0); 2108 } 2109 return; 2110 } 2111 } 2112 2113 switch (size) { 2114 // fallthrough 2115 case 3: 2116 mNameData.mPhoneticMiddle = elems.get(2); 2117 case 2: 2118 mNameData.mPhoneticGiven = elems.get(1); 2119 default: 2120 mNameData.mPhoneticFamily = elems.get(0); 2121 } 2122 } 2123 2124 public void addProperty(final VCardProperty property) { 2125 final String propertyName = property.getName(); 2126 final Map<String, Collection<String>> paramMap = property.getParameterMap(); 2127 final List<String> propertyValueList = property.getValueList(); 2128 byte[] propertyBytes = property.getByteValue(); 2129 2130 if ((propertyValueList == null || propertyValueList.size() == 0) 2131 && propertyBytes == null) { 2132 return; 2133 } 2134 final String propValue = (propertyValueList != null 2135 ? listToString(propertyValueList).trim() 2136 : null); 2137 2138 if (propertyName.equals(VCardConstants.PROPERTY_VERSION)) { 2139 // vCard version. Ignore this. 2140 } else if (propertyName.equals(VCardConstants.PROPERTY_FN)) { 2141 mNameData.mFormatted = propValue; 2142 } else if (propertyName.equals(VCardConstants.PROPERTY_NAME)) { 2143 // Only in vCard 3.0. Use this if FN doesn't exist though it is 2144 // required in vCard 3.0. 2145 if (TextUtils.isEmpty(mNameData.mFormatted)) { 2146 mNameData.mFormatted = propValue; 2147 } 2148 } else if (propertyName.equals(VCardConstants.PROPERTY_N)) { 2149 handleNProperty(propertyValueList, paramMap); 2150 } else if (propertyName.equals(VCardConstants.PROPERTY_SORT_STRING)) { 2151 mNameData.mSortString = propValue; 2152 } else if (propertyName.equals(VCardConstants.PROPERTY_NICKNAME) 2153 || propertyName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { 2154 addNickName(propValue); 2155 } else if (propertyName.equals(VCardConstants.PROPERTY_SOUND)) { 2156 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2157 if (typeCollection != null 2158 && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { 2159 // As of 2009-10-08, Parser side does not split a property value into separated 2160 // values using ';' (in other words, propValueList.size() == 1), 2161 // which is correct behavior from the view of vCard 2.1. 2162 // But we want it to be separated, so do the separation here. 2163 final List<String> phoneticNameList = VCardUtils.constructListFromValue(propValue, 2164 mVCardType); 2165 handlePhoneticNameFromSound(phoneticNameList); 2166 } else { 2167 // Ignore this field since Android cannot understand what it is. 2168 } 2169 } else if (propertyName.equals(VCardConstants.PROPERTY_ADR)) { 2170 boolean valuesAreAllEmpty = true; 2171 for (String value : propertyValueList) { 2172 if (!TextUtils.isEmpty(value)) { 2173 valuesAreAllEmpty = false; 2174 break; 2175 } 2176 } 2177 if (valuesAreAllEmpty) { 2178 return; 2179 } 2180 2181 int type = -1; 2182 String label = null; 2183 boolean isPrimary = false; 2184 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2185 if (typeCollection != null) { 2186 for (final String typeStringOrg : typeCollection) { 2187 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2188 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2189 isPrimary = true; 2190 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2191 type = StructuredPostal.TYPE_HOME; 2192 label = null; 2193 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK) 2194 || typeStringUpperCase 2195 .equalsIgnoreCase(VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { 2196 // "COMPANY" seems emitted by Windows Mobile, which is not 2197 // specifically supported by vCard 2.1. We assume this is same 2198 // as "WORK". 2199 type = StructuredPostal.TYPE_WORK; 2200 label = null; 2201 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) 2202 || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM) 2203 || typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { 2204 // We do not have any appropriate way to store this information. 2205 } else if (type < 0) { // If no other type is specified before. 2206 type = StructuredPostal.TYPE_CUSTOM; 2207 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2208 label = typeStringOrg.substring(2); 2209 } else { 2210 label = typeStringOrg; 2211 } 2212 } 2213 } 2214 } 2215 // We use "HOME" as default 2216 if (type < 0) { 2217 type = StructuredPostal.TYPE_HOME; 2218 } 2219 2220 addPostal(type, propertyValueList, label, isPrimary); 2221 } else if (propertyName.equals(VCardConstants.PROPERTY_EMAIL)) { 2222 int type = -1; 2223 String label = null; 2224 boolean isPrimary = false; 2225 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2226 if (typeCollection != null) { 2227 for (final String typeStringOrg : typeCollection) { 2228 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2229 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2230 isPrimary = true; 2231 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2232 type = Email.TYPE_HOME; 2233 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) { 2234 type = Email.TYPE_WORK; 2235 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) { 2236 type = Email.TYPE_MOBILE; 2237 } else if (type < 0) { // If no other type is specified before 2238 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2239 label = typeStringOrg.substring(2); 2240 } else { 2241 label = typeStringOrg; 2242 } 2243 type = Email.TYPE_CUSTOM; 2244 } 2245 } 2246 } 2247 if (type < 0) { 2248 type = Email.TYPE_OTHER; 2249 } 2250 addEmail(type, propValue, label, isPrimary); 2251 } else if (propertyName.equals(VCardConstants.PROPERTY_ORG)) { 2252 // vCard specification does not specify other types. 2253 final int type = Organization.TYPE_WORK; 2254 boolean isPrimary = false; 2255 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2256 if (typeCollection != null) { 2257 for (String typeString : typeCollection) { 2258 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 2259 isPrimary = true; 2260 } 2261 } 2262 } 2263 handleOrgValue(type, propertyValueList, paramMap, isPrimary); 2264 } else if (propertyName.equals(VCardConstants.PROPERTY_TITLE)) { 2265 handleTitleValue(propValue); 2266 } else if (propertyName.equals(VCardConstants.PROPERTY_ROLE)) { 2267 // This conflicts with TITLE. Ignore for now... 2268 // handleTitleValue(propValue); 2269 } else if (propertyName.equals(VCardConstants.PROPERTY_PHOTO) 2270 || propertyName.equals(VCardConstants.PROPERTY_LOGO)) { 2271 Collection<String> paramMapValue = paramMap.get("VALUE"); 2272 if (paramMapValue != null && paramMapValue.contains("URL")) { 2273 // Currently we do not have appropriate example for testing this case. 2274 } else { 2275 final Collection<String> typeCollection = paramMap.get("TYPE"); 2276 String formatName = null; 2277 boolean isPrimary = false; 2278 if (typeCollection != null) { 2279 for (String typeValue : typeCollection) { 2280 if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { 2281 isPrimary = true; 2282 } else if (formatName == null) { 2283 formatName = typeValue; 2284 } 2285 } 2286 } 2287 addPhotoBytes(formatName, propertyBytes, isPrimary); 2288 } 2289 } else if (propertyName.equals(VCardConstants.PROPERTY_TEL)) { 2290 String phoneNumber = null; 2291 boolean isSip = false; 2292 if (VCardConfig.isVersion40(mVCardType)) { 2293 // Given propValue is in URI format, not in phone number format used until 2294 // vCard 3.0. 2295 if (propValue.startsWith("sip:")) { 2296 isSip = true; 2297 } else if (propValue.startsWith("tel:")) { 2298 phoneNumber = propValue.substring(4); 2299 } else { 2300 // We don't know appropriate way to handle the other schemas. Also, 2301 // we may still have non-URI phone number. To keep given data as much as 2302 // we can, just save original value here. 2303 phoneNumber = propValue; 2304 } 2305 } else { 2306 phoneNumber = propValue; 2307 } 2308 2309 if (isSip) { 2310 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2311 handleSipCase(propValue, typeCollection); 2312 } else { 2313 if (propValue.length() == 0) { 2314 return; 2315 } 2316 2317 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2318 final Object typeObject = VCardUtils.getPhoneTypeFromStrings(typeCollection, 2319 phoneNumber); 2320 final int type; 2321 final String label; 2322 if (typeObject instanceof Integer) { 2323 type = (Integer) typeObject; 2324 label = null; 2325 } else { 2326 type = Phone.TYPE_CUSTOM; 2327 label = typeObject.toString(); 2328 } 2329 2330 final boolean isPrimary; 2331 if (typeCollection != null && 2332 typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 2333 isPrimary = true; 2334 } else { 2335 isPrimary = false; 2336 } 2337 2338 addPhone(type, phoneNumber, label, isPrimary); 2339 } 2340 } else if (propertyName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { 2341 // The phone number available via Skype. 2342 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2343 final int type = Phone.TYPE_OTHER; 2344 final boolean isPrimary; 2345 if (typeCollection != null 2346 && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 2347 isPrimary = true; 2348 } else { 2349 isPrimary = false; 2350 } 2351 addPhone(type, propValue, null, isPrimary); 2352 } else if (sImMap.containsKey(propertyName)) { 2353 final int protocol = sImMap.get(propertyName); 2354 boolean isPrimary = false; 2355 int type = -1; 2356 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2357 if (typeCollection != null) { 2358 for (String typeString : typeCollection) { 2359 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 2360 isPrimary = true; 2361 } else if (type < 0) { 2362 if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { 2363 type = Im.TYPE_HOME; 2364 } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { 2365 type = Im.TYPE_WORK; 2366 } 2367 } 2368 } 2369 } 2370 if (type < 0) { 2371 type = Im.TYPE_HOME; 2372 } 2373 addIm(protocol, null, propValue, type, isPrimary); 2374 } else if (propertyName.equals(VCardConstants.PROPERTY_NOTE)) { 2375 addNote(propValue); 2376 } else if (propertyName.equals(VCardConstants.PROPERTY_URL)) { 2377 if (mWebsiteList == null) { 2378 mWebsiteList = new ArrayList<WebsiteData>(1); 2379 } 2380 mWebsiteList.add(new WebsiteData(propValue)); 2381 } else if (propertyName.equals(VCardConstants.PROPERTY_BDAY)) { 2382 mBirthday = new BirthdayData(propValue); 2383 } else if (propertyName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) { 2384 mAnniversary = new AnniversaryData(propValue); 2385 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { 2386 mNameData.mPhoneticGiven = propValue; 2387 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { 2388 mNameData.mPhoneticMiddle = propValue; 2389 } else if (propertyName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { 2390 mNameData.mPhoneticFamily = propValue; 2391 } else if (propertyName.equals(VCardConstants.PROPERTY_IMPP)) { 2392 // See also RFC 4770 (for vCard 3.0) 2393 if (propValue.startsWith("sip:")) { 2394 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2395 handleSipCase(propValue, typeCollection); 2396 } 2397 } else if (propertyName.equals(VCardConstants.PROPERTY_X_SIP)) { 2398 if (!TextUtils.isEmpty(propValue)) { 2399 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 2400 handleSipCase(propValue, typeCollection); 2401 } 2402 } else if (propertyName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { 2403 final List<String> customPropertyList = VCardUtils.constructListFromValue(propValue, 2404 mVCardType); 2405 handleAndroidCustomProperty(customPropertyList); 2406 } else { 2407 } 2408 // Be careful when adding some logic here, as some blocks above may use "return". 2409 } 2410 2411 /** 2412 * @param propValue may contain "sip:" at the beginning. 2413 * @param typeCollection 2414 */ 2415 private void handleSipCase(String propValue, Collection<String> typeCollection) { 2416 if (TextUtils.isEmpty(propValue)) { 2417 return; 2418 } 2419 if (propValue.startsWith("sip:")) { 2420 propValue = propValue.substring(4); 2421 if (propValue.length() == 0) { 2422 return; 2423 } 2424 } 2425 2426 int type = -1; 2427 String label = null; 2428 boolean isPrimary = false; 2429 if (typeCollection != null) { 2430 for (final String typeStringOrg : typeCollection) { 2431 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 2432 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 2433 isPrimary = true; 2434 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 2435 type = SipAddress.TYPE_HOME; 2436 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) { 2437 type = SipAddress.TYPE_WORK; 2438 } else if (type < 0) { // If no other type is specified before 2439 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 2440 label = typeStringOrg.substring(2); 2441 } else { 2442 label = typeStringOrg; 2443 } 2444 type = SipAddress.TYPE_CUSTOM; 2445 } 2446 } 2447 } 2448 if (type < 0) { 2449 type = SipAddress.TYPE_OTHER; 2450 } 2451 addSip(propValue, type, label, isPrimary); 2452 } 2453 2454 public void addChild(VCardEntry child) { 2455 if (mChildren == null) { 2456 mChildren = new ArrayList<VCardEntry>(); 2457 } 2458 mChildren.add(child); 2459 } 2460 2461 private void handleAndroidCustomProperty(final List<String> customPropertyList) { 2462 if (mAndroidCustomDataList == null) { 2463 mAndroidCustomDataList = new ArrayList<AndroidCustomData>(); 2464 } 2465 mAndroidCustomDataList 2466 .add(AndroidCustomData.constructAndroidCustomData(customPropertyList)); 2467 } 2468 2469 /** 2470 * Construct the display name. The constructed data must not be null. 2471 */ 2472 private String constructDisplayName() { 2473 String displayName = null; 2474 // FullName (created via "FN" or "NAME" field) is prefered. 2475 if (!TextUtils.isEmpty(mNameData.mFormatted)) { 2476 displayName = mNameData.mFormatted; 2477 } else if (!mNameData.emptyStructuredName()) { 2478 displayName = VCardUtils.constructNameFromElements(mVCardType, mNameData.mFamily, 2479 mNameData.mMiddle, mNameData.mGiven, mNameData.mPrefix, mNameData.mSuffix); 2480 } else if (!mNameData.emptyPhoneticStructuredName()) { 2481 displayName = VCardUtils.constructNameFromElements(mVCardType, 2482 mNameData.mPhoneticFamily, mNameData.mPhoneticMiddle, mNameData.mPhoneticGiven); 2483 } else if (mEmailList != null && mEmailList.size() > 0) { 2484 displayName = mEmailList.get(0).mAddress; 2485 } else if (mPhoneList != null && mPhoneList.size() > 0) { 2486 displayName = mPhoneList.get(0).mNumber; 2487 } else if (mPostalList != null && mPostalList.size() > 0) { 2488 displayName = mPostalList.get(0).getFormattedAddress(mVCardType); 2489 } else if (mOrganizationList != null && mOrganizationList.size() > 0) { 2490 displayName = mOrganizationList.get(0).getFormattedString(); 2491 } 2492 if (displayName == null) { 2493 displayName = ""; 2494 } 2495 return displayName; 2496 } 2497 2498 /** 2499 * Consolidate several fielsds (like mName) using name candidates, 2500 */ 2501 public void consolidateFields() { 2502 mNameData.displayName = constructDisplayName(); 2503 } 2504 2505 /** 2506 * @return true when this object has nothing meaningful for Android's 2507 * Contacts, and thus is "ignorable" for Android's Contacts. This 2508 * does not mean an original vCard is really empty. Even when the 2509 * original vCard has some fields, this may ignore it if those 2510 * fields cannot be transcoded into Android's Contacts 2511 * representation. 2512 */ 2513 public boolean isIgnorable() { 2514 IsIgnorableIterator iterator = new IsIgnorableIterator(); 2515 iterateAllData(iterator); 2516 return iterator.getResult(); 2517 } 2518 2519 /** 2520 * Constructs the list of insert operation for this object. When the 2521 * operationList argument is null, this method creates a new ArrayList and 2522 * return it. The returned object is filled with new insert operations for 2523 * this object. When operationList argument is not null, this method appends 2524 * those new operations into the object instead of creating a new ArrayList. 2525 * 2526 * @param resolver {@link ContentResolver} object to be used in this method. 2527 * @param operationList object to be filled. You can use this argument to 2528 * concatinate operation lists. If null, this method creates a 2529 * new array object. 2530 * @return If operationList argument is null, new object with new insert 2531 * operations. If it is not null, the operationList object with 2532 * operations inserted by this method. 2533 */ 2534 public ArrayList<ContentProviderOperation> constructInsertOperations(ContentResolver resolver, 2535 ArrayList<ContentProviderOperation> operationList) { 2536 if (operationList == null) { 2537 operationList = new ArrayList<ContentProviderOperation>(); 2538 } 2539 2540 if (isIgnorable()) { 2541 return operationList; 2542 } 2543 2544 final int backReferenceIndex = operationList.size(); 2545 2546 // After applying the batch the first result's Uri is returned so it is important that 2547 // the RawContact is the first operation that gets inserted into the list. 2548 ContentProviderOperation.Builder builder = ContentProviderOperation 2549 .newInsert(RawContacts.CONTENT_URI); 2550 if (mAccount != null) { 2551 builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); 2552 builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); 2553 } else { 2554 builder.withValue(RawContacts.ACCOUNT_NAME, null); 2555 builder.withValue(RawContacts.ACCOUNT_TYPE, null); 2556 } 2557 operationList.add(builder.build()); 2558 2559 int start = operationList.size(); 2560 iterateAllData(new InsertOperationConstrutor(operationList, backReferenceIndex)); 2561 int end = operationList.size(); 2562 2563 return operationList; 2564 } 2565 2566 public static VCardEntry buildFromResolver(ContentResolver resolver) { 2567 return buildFromResolver(resolver, Contacts.CONTENT_URI); 2568 } 2569 2570 public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { 2571 return null; 2572 } 2573 2574 private String listToString(List<String> list) { 2575 final int size = list.size(); 2576 if (size > 1) { 2577 StringBuilder builder = new StringBuilder(); 2578 int i = 0; 2579 for (String type : list) { 2580 builder.append(type); 2581 if (i < size - 1) { 2582 builder.append(";"); 2583 } 2584 } 2585 return builder.toString(); 2586 } else if (size == 1) { 2587 return list.get(0); 2588 } else { 2589 return ""; 2590 } 2591 } 2592 2593 public final NameData getNameData() { 2594 return mNameData; 2595 } 2596 2597 public final List<NicknameData> getNickNameList() { 2598 return mNicknameList; 2599 } 2600 2601 public final String getBirthday() { 2602 return mBirthday != null ? mBirthday.mBirthday : null; 2603 } 2604 2605 public final List<NoteData> getNotes() { 2606 return mNoteList; 2607 } 2608 2609 public final List<PhoneData> getPhoneList() { 2610 return mPhoneList; 2611 } 2612 2613 public final List<EmailData> getEmailList() { 2614 return mEmailList; 2615 } 2616 2617 public final List<PostalData> getPostalList() { 2618 return mPostalList; 2619 } 2620 2621 public final List<OrganizationData> getOrganizationList() { 2622 return mOrganizationList; 2623 } 2624 2625 public final List<ImData> getImList() { 2626 return mImList; 2627 } 2628 2629 public final List<PhotoData> getPhotoList() { 2630 return mPhotoList; 2631 } 2632 2633 public final List<WebsiteData> getWebsiteList() { 2634 return mWebsiteList; 2635 } 2636 2637 /** 2638 * @hide this interface may be changed for better support of vCard 4.0 (UID) 2639 */ 2640 public final List<VCardEntry> getChildlen() { 2641 return mChildren; 2642 } 2643 2644 public String getDisplayName() { 2645 if (mNameData.displayName == null) { 2646 mNameData.displayName = constructDisplayName(); 2647 } 2648 return mNameData.displayName; 2649 } 2650 } 2651