VCardEntry.java revision 9919ad2126c06dbf2eb54a11e6158f87f316bc22
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 */ 16package com.android.vcard; 17 18import com.android.vcard.VCardUtils.PhoneNumberUtilsPort; 19 20import android.accounts.Account; 21import android.content.ContentProviderOperation; 22import android.content.ContentProviderResult; 23import android.content.ContentResolver; 24import android.content.OperationApplicationException; 25import android.net.Uri; 26import android.os.RemoteException; 27import android.provider.ContactsContract; 28import android.provider.ContactsContract.CommonDataKinds.Email; 29import android.provider.ContactsContract.CommonDataKinds.Event; 30import android.provider.ContactsContract.CommonDataKinds.GroupMembership; 31import android.provider.ContactsContract.CommonDataKinds.Im; 32import android.provider.ContactsContract.CommonDataKinds.Nickname; 33import android.provider.ContactsContract.CommonDataKinds.Note; 34import android.provider.ContactsContract.CommonDataKinds.Organization; 35import android.provider.ContactsContract.CommonDataKinds.Phone; 36import android.provider.ContactsContract.CommonDataKinds.Photo; 37import android.provider.ContactsContract.CommonDataKinds.SipAddress; 38import android.provider.ContactsContract.CommonDataKinds.StructuredName; 39import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 40import android.provider.ContactsContract.CommonDataKinds.Website; 41import android.provider.ContactsContract.Contacts; 42import android.provider.ContactsContract.Data; 43import android.provider.ContactsContract.RawContacts; 44import android.text.TextUtils; 45import android.util.Log; 46 47import java.util.ArrayList; 48import java.util.Arrays; 49import java.util.Collection; 50import java.util.Collections; 51import java.util.HashMap; 52import java.util.HashSet; 53import java.util.LinkedHashSet; 54import java.util.List; 55import java.util.Map; 56import java.util.Set; 57 58/** 59 * This class bridges between data structure of Contact app and VCard data. 60 */ 61public class VCardEntry { 62 private static final String LOG_TAG = "VCardEntry"; 63 64 private final static int DEFAULT_ORGANIZATION_TYPE = Organization.TYPE_WORK; 65 66 private static final Map<String, Integer> sImMap = new HashMap<String, Integer>(); 67 68 static { 69 sImMap.put(VCardConstants.PROPERTY_X_AIM, Im.PROTOCOL_AIM); 70 sImMap.put(VCardConstants.PROPERTY_X_MSN, Im.PROTOCOL_MSN); 71 sImMap.put(VCardConstants.PROPERTY_X_YAHOO, Im.PROTOCOL_YAHOO); 72 sImMap.put(VCardConstants.PROPERTY_X_ICQ, Im.PROTOCOL_ICQ); 73 sImMap.put(VCardConstants.PROPERTY_X_JABBER, Im.PROTOCOL_JABBER); 74 sImMap.put(VCardConstants.PROPERTY_X_SKYPE_USERNAME, Im.PROTOCOL_SKYPE); 75 sImMap.put(VCardConstants.PROPERTY_X_GOOGLE_TALK, Im.PROTOCOL_GOOGLE_TALK); 76 sImMap.put(VCardConstants.ImportOnly.PROPERTY_X_GOOGLE_TALK_WITH_SPACE, 77 Im.PROTOCOL_GOOGLE_TALK); 78 } 79 80 public static class PhoneData { 81 public final int type; 82 public final String data; 83 public final String label; 84 // isPrimary is (not final but) changable, only when there's no appropriate one existing 85 // in the original VCard. 86 public boolean isPrimary; 87 public PhoneData(int type, String data, String label, boolean isPrimary) { 88 this.type = type; 89 this.data = data; 90 this.label = label; 91 this.isPrimary = isPrimary; 92 } 93 94 @Override 95 public boolean equals(Object obj) { 96 if (!(obj instanceof PhoneData)) { 97 return false; 98 } 99 PhoneData phoneData = (PhoneData)obj; 100 return (type == phoneData.type && data.equals(phoneData.data) && 101 label.equals(phoneData.label) && isPrimary == phoneData.isPrimary); 102 } 103 104 @Override 105 public String toString() { 106 return String.format("type: %d, data: %s, label: %s, isPrimary: %s", 107 type, data, label, isPrimary); 108 } 109 } 110 111 public static class EmailData { 112 public final int type; 113 public final String data; 114 // Used only when TYPE is TYPE_CUSTOM. 115 public final String label; 116 public boolean isPrimary; 117 public EmailData(int type, String data, String label, boolean isPrimary) { 118 this.type = type; 119 this.data = data; 120 this.label = label; 121 this.isPrimary = isPrimary; 122 } 123 124 @Override 125 public boolean equals(Object obj) { 126 if (!(obj instanceof EmailData)) { 127 return false; 128 } 129 EmailData emailData = (EmailData)obj; 130 return (type == emailData.type && data.equals(emailData.data) && 131 label.equals(emailData.label) && isPrimary == emailData.isPrimary); 132 } 133 134 @Override 135 public String toString() { 136 return String.format("type: %d, data: %s, label: %s, isPrimary: %s", 137 type, data, label, isPrimary); 138 } 139 } 140 141 public static class PostalData { 142 // Determined by vCard specification. 143 // - PO Box, Extended Addr, Street, Locality, Region, Postal Code, Country Name 144 public static final int ADDR_MAX_DATA_SIZE = 7; 145 private final String[] dataArray; 146 public final String pobox; 147 public final String extendedAddress; 148 public final String street; 149 public final String localty; 150 public final String region; 151 public final String postalCode; 152 public final String country; 153 public final int type; 154 public final String label; 155 public boolean isPrimary; 156 157 public PostalData(final int type, final List<String> propValueList, 158 final String label, boolean isPrimary) { 159 this.type = type; 160 dataArray = new String[ADDR_MAX_DATA_SIZE]; 161 162 int size = propValueList.size(); 163 if (size > ADDR_MAX_DATA_SIZE) { 164 size = ADDR_MAX_DATA_SIZE; 165 } 166 167 // adr-value = 0*6(text-value ";") text-value 168 // ; PO Box, Extended Address, Street, Locality, Region, Postal 169 // ; Code, Country Name 170 // 171 // Use Iterator assuming List may be LinkedList, though actually it is 172 // always ArrayList in the current implementation. 173 int i = 0; 174 for (String addressElement : propValueList) { 175 dataArray[i] = addressElement; 176 if (++i >= size) { 177 break; 178 } 179 } 180 while (i < ADDR_MAX_DATA_SIZE) { 181 dataArray[i++] = null; 182 } 183 184 this.pobox = dataArray[0]; 185 this.extendedAddress = dataArray[1]; 186 this.street = dataArray[2]; 187 this.localty = dataArray[3]; 188 this.region = dataArray[4]; 189 this.postalCode = dataArray[5]; 190 this.country = dataArray[6]; 191 this.label = label; 192 this.isPrimary = isPrimary; 193 } 194 195 @Override 196 public boolean equals(Object obj) { 197 if (!(obj instanceof PostalData)) { 198 return false; 199 } 200 final PostalData postalData = (PostalData)obj; 201 return (Arrays.equals(dataArray, postalData.dataArray) && 202 (type == postalData.type && 203 (type == StructuredPostal.TYPE_CUSTOM ? 204 (label == postalData.label) : true)) && 205 (isPrimary == postalData.isPrimary)); 206 } 207 208 public String getFormattedAddress(final int vcardType) { 209 StringBuilder builder = new StringBuilder(); 210 boolean empty = true; 211 if (VCardConfig.isJapaneseDevice(vcardType)) { 212 // In Japan, the order is reversed. 213 for (int i = ADDR_MAX_DATA_SIZE - 1; i >= 0; i--) { 214 String addressPart = dataArray[i]; 215 if (!TextUtils.isEmpty(addressPart)) { 216 if (!empty) { 217 builder.append(' '); 218 } else { 219 empty = false; 220 } 221 builder.append(addressPart); 222 } 223 } 224 } else { 225 for (int i = 0; i < ADDR_MAX_DATA_SIZE; i++) { 226 String addressPart = dataArray[i]; 227 if (!TextUtils.isEmpty(addressPart)) { 228 if (!empty) { 229 builder.append(' '); 230 } else { 231 empty = false; 232 } 233 builder.append(addressPart); 234 } 235 } 236 } 237 238 return builder.toString().trim(); 239 } 240 241 @Override 242 public String toString() { 243 return String.format("type: %d, label: %s, isPrimary: %s", 244 type, label, isPrimary); 245 } 246 } 247 248 public static class OrganizationData { 249 public final int type; 250 // non-final is Intentional: we may change the values since this info is separated into 251 // two parts in vCard: "ORG" + "TITLE", and we have to cope with each field in 252 // different timing. 253 public String companyName; 254 public String departmentName; 255 public String titleName; 256 public final String phoneticName; // We won't have this in "TITLE" property. 257 public boolean isPrimary; 258 259 public OrganizationData(int type, 260 final String companyName, 261 final String departmentName, 262 final String titleName, 263 final String phoneticName, 264 final boolean isPrimary) { 265 this.type = type; 266 this.companyName = companyName; 267 this.departmentName = departmentName; 268 this.titleName = titleName; 269 this.phoneticName = phoneticName; 270 this.isPrimary = isPrimary; 271 } 272 273 @Override 274 public boolean equals(Object obj) { 275 if (!(obj instanceof OrganizationData)) { 276 return false; 277 } 278 OrganizationData organization = (OrganizationData)obj; 279 return (type == organization.type && 280 TextUtils.equals(companyName, organization.companyName) && 281 TextUtils.equals(departmentName, organization.departmentName) && 282 TextUtils.equals(titleName, organization.titleName) && 283 isPrimary == organization.isPrimary); 284 } 285 286 public String getFormattedString() { 287 final StringBuilder builder = new StringBuilder(); 288 if (!TextUtils.isEmpty(companyName)) { 289 builder.append(companyName); 290 } 291 292 if (!TextUtils.isEmpty(departmentName)) { 293 if (builder.length() > 0) { 294 builder.append(", "); 295 } 296 builder.append(departmentName); 297 } 298 299 if (!TextUtils.isEmpty(titleName)) { 300 if (builder.length() > 0) { 301 builder.append(", "); 302 } 303 builder.append(titleName); 304 } 305 306 return builder.toString(); 307 } 308 309 @Override 310 public String toString() { 311 return String.format( 312 "type: %d, company: %s, department: %s, title: %s, isPrimary: %s", 313 type, companyName, departmentName, titleName, isPrimary); 314 } 315 } 316 317 public static class ImData { 318 public final int protocol; 319 public final String customProtocol; 320 public final int type; 321 public final String data; 322 public final boolean isPrimary; 323 324 public ImData(final int protocol, final String customProtocol, final int type, 325 final String data, final boolean isPrimary) { 326 this.protocol = protocol; 327 this.customProtocol = customProtocol; 328 this.type = type; 329 this.data = data; 330 this.isPrimary = isPrimary; 331 } 332 333 @Override 334 public boolean equals(Object obj) { 335 if (!(obj instanceof ImData)) { 336 return false; 337 } 338 ImData imData = (ImData)obj; 339 return (type == imData.type && protocol == imData.protocol 340 && (customProtocol != null ? customProtocol.equals(imData.customProtocol) : 341 (imData.customProtocol == null)) 342 && (data != null ? data.equals(imData.data) : (imData.data == null)) 343 && isPrimary == imData.isPrimary); 344 } 345 346 @Override 347 public String toString() { 348 return String.format( 349 "type: %d, protocol: %d, custom_protcol: %s, data: %s, isPrimary: %s", 350 type, protocol, customProtocol, data, isPrimary); 351 } 352 } 353 354 public static class PhotoData { 355 public static final String FORMAT_FLASH = "SWF"; 356 public final int type; 357 public final String formatName; // used when type is not defined in ContactsContract. 358 public final byte[] photoBytes; 359 public final boolean isPrimary; 360 361 public PhotoData(int type, String formatName, byte[] photoBytes, boolean isPrimary) { 362 this.type = type; 363 this.formatName = formatName; 364 this.photoBytes = photoBytes; 365 this.isPrimary = isPrimary; 366 } 367 368 @Override 369 public boolean equals(Object obj) { 370 if (!(obj instanceof PhotoData)) { 371 return false; 372 } 373 PhotoData photoData = (PhotoData)obj; 374 return (type == photoData.type && 375 (formatName == null ? (photoData.formatName == null) : 376 formatName.equals(photoData.formatName)) && 377 (Arrays.equals(photoBytes, photoData.photoBytes)) && 378 (isPrimary == photoData.isPrimary)); 379 } 380 381 @Override 382 public String toString() { 383 return String.format("type: %d, format: %s: size: %d, isPrimary: %s", 384 type, formatName, photoBytes.length, isPrimary); 385 } 386 } 387 388 /* package */ static class Property { 389 private String mPropertyName; 390 private Map<String, Collection<String>> mParameterMap = 391 new HashMap<String, Collection<String>>(); 392 private List<String> mPropertyValueList = new ArrayList<String>(); 393 private byte[] mPropertyBytes; 394 395 public void setPropertyName(final String propertyName) { 396 mPropertyName = propertyName; 397 } 398 399 public void addParameter(final String paramName, final String paramValue) { 400 Collection<String> values; 401 if (!mParameterMap.containsKey(paramName)) { 402 if (paramName.equals("TYPE")) { 403 values = new HashSet<String>(); 404 } else { 405 values = new ArrayList<String>(); 406 } 407 mParameterMap.put(paramName, values); 408 } else { 409 values = mParameterMap.get(paramName); 410 } 411 values.add(paramValue); 412 } 413 414 public void addToPropertyValueList(final String propertyValue) { 415 mPropertyValueList.add(propertyValue); 416 } 417 418 public void setPropertyBytes(final byte[] propertyBytes) { 419 mPropertyBytes = propertyBytes; 420 } 421 422 public final Collection<String> getParameters(String type) { 423 return mParameterMap.get(type); 424 } 425 426 public final List<String> getPropertyValueList() { 427 return mPropertyValueList; 428 } 429 430 public void clear() { 431 mPropertyName = null; 432 mParameterMap.clear(); 433 mPropertyValueList.clear(); 434 mPropertyBytes = null; 435 } 436 } 437 438 // TODO(dmiyakawa): vCard 4.0 logically has multiple formatted names and we need to 439 // select the most preferable one using PREF parameter. 440 // 441 // e.g. (based on rev.13) 442 // FN;PREF=1:John M. Doe 443 // FN;PREF=2:John Doe 444 // FN;PREF=3;John 445 446 private String mFamilyName; 447 private String mGivenName; 448 private String mMiddleName; 449 private String mPrefix; 450 private String mSuffix; 451 452 // Used only when no family nor given name is found. 453 private String mFormattedName; 454 455 private String mPhoneticFamilyName; 456 private String mPhoneticGivenName; 457 private String mPhoneticMiddleName; 458 459 private String mPhoneticFullName; 460 461 private List<String> mNickNameList; 462 463 private String mDisplayName; 464 465 private String mBirthday; 466 private String mAnniversary; 467 468 private List<String> mNoteList; 469 private List<PhoneData> mPhoneList; 470 private List<EmailData> mEmailList; 471 private List<PostalData> mPostalList; 472 private List<OrganizationData> mOrganizationList; 473 private List<ImData> mImList; 474 private List<PhotoData> mPhotoList; 475 private List<String> mWebsiteList; 476 private Set<String> mSipSet; 477 private List<List<String>> mAndroidCustomPropertyList; 478 479 private final int mVCardType; 480 private final Account mAccount; 481 482 public VCardEntry() { 483 this(VCardConfig.VCARD_TYPE_V21_GENERIC); 484 } 485 486 public VCardEntry(int vcardType) { 487 this(vcardType, null); 488 } 489 490 public VCardEntry(int vcardType, Account account) { 491 mVCardType = vcardType; 492 mAccount = account; 493 } 494 495 private void addPhone(int type, String data, String label, boolean isPrimary) { 496 if (mPhoneList == null) { 497 mPhoneList = new ArrayList<PhoneData>(); 498 } 499 final StringBuilder builder = new StringBuilder(); 500 final String trimed = data.trim(); 501 final String formattedNumber; 502 if (type == Phone.TYPE_PAGER || VCardConfig.refrainPhoneNumberFormatting(mVCardType)) { 503 formattedNumber = trimed; 504 } else { 505 final int length = trimed.length(); 506 for (int i = 0; i < length; i++) { 507 char ch = trimed.charAt(i); 508 if (('0' <= ch && ch <= '9') || (i == 0 && ch == '+')) { 509 builder.append(ch); 510 } 511 } 512 513 final int formattingType = VCardUtils.getPhoneNumberFormat(mVCardType); 514 formattedNumber = PhoneNumberUtilsPort.formatNumber(builder.toString(), formattingType); 515 } 516 PhoneData phoneData = new PhoneData(type, formattedNumber, label, isPrimary); 517 mPhoneList.add(phoneData); 518 } 519 520 private void addNickName(final String nickName) { 521 if (mNickNameList == null) { 522 mNickNameList = new ArrayList<String>(); 523 } 524 mNickNameList.add(nickName); 525 } 526 527 private void addEmail(int type, String data, String label, boolean isPrimary){ 528 if (mEmailList == null) { 529 mEmailList = new ArrayList<EmailData>(); 530 } 531 mEmailList.add(new EmailData(type, data, label, isPrimary)); 532 } 533 534 private void addPostal(int type, List<String> propValueList, String label, boolean isPrimary){ 535 if (mPostalList == null) { 536 mPostalList = new ArrayList<PostalData>(0); 537 } 538 mPostalList.add(new PostalData(type, propValueList, label, isPrimary)); 539 } 540 541 /** 542 * Should be called via {@link #handleOrgValue(int, List, Map, boolean) or 543 * {@link #handleTitleValue(String)}. 544 */ 545 private void addNewOrganization(int type, final String companyName, 546 final String departmentName, 547 final String titleName, 548 final String phoneticName, 549 final boolean isPrimary) { 550 if (mOrganizationList == null) { 551 mOrganizationList = new ArrayList<OrganizationData>(); 552 } 553 mOrganizationList.add(new OrganizationData(type, companyName, 554 departmentName, titleName, phoneticName, isPrimary)); 555 } 556 557 private static final List<String> sEmptyList = 558 Collections.unmodifiableList(new ArrayList<String>(0)); 559 560 private String buildSinglePhoneticNameFromSortAsParam(Map<String, Collection<String>> paramMap) { 561 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 562 if (sortAsCollection != null && sortAsCollection.size() != 0) { 563 if (sortAsCollection.size() > 1) { 564 Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + 565 Arrays.toString(sortAsCollection.toArray())); 566 } 567 final List<String> sortNames = 568 VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), 569 mVCardType); 570 final StringBuilder builder = new StringBuilder(); 571 for (final String elem : sortNames) { 572 builder.append(elem); 573 } 574 return builder.toString(); 575 } else { 576 return null; 577 } 578 } 579 580 /** 581 * Set "ORG" related values to the appropriate data. If there's more than one 582 * {@link OrganizationData} objects, this input data are attached to the last one which 583 * does not have valid values (not including empty but only null). If there's no 584 * {@link OrganizationData} object, a new {@link OrganizationData} is created, 585 * whose title is set to null. 586 */ 587 private void handleOrgValue(final int type, List<String> orgList, 588 Map<String, Collection<String>> paramMap, boolean isPrimary) { 589 final String phoneticName = buildSinglePhoneticNameFromSortAsParam(paramMap); 590 if (orgList == null) { 591 orgList = sEmptyList; 592 } 593 final String companyName; 594 final String departmentName; 595 final int size = orgList.size(); 596 switch (size) { 597 case 0: { 598 companyName = ""; 599 departmentName = null; 600 break; 601 } 602 case 1: { 603 companyName = orgList.get(0); 604 departmentName = null; 605 break; 606 } 607 default: { // More than 1. 608 companyName = orgList.get(0); 609 // We're not sure which is the correct string for department. 610 // In order to keep all the data, concatinate the rest of elements. 611 StringBuilder builder = new StringBuilder(); 612 for (int i = 1; i < size; i++) { 613 if (i > 1) { 614 builder.append(' '); 615 } 616 builder.append(orgList.get(i)); 617 } 618 departmentName = builder.toString(); 619 } 620 } 621 if (mOrganizationList == null) { 622 // Create new first organization entry, with "null" title which may be 623 // added via handleTitleValue(). 624 addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); 625 return; 626 } 627 for (OrganizationData organizationData : mOrganizationList) { 628 // Not use TextUtils.isEmpty() since ORG was set but the elements might be empty. 629 // e.g. "ORG;PREF:;" -> Both companyName and departmentName become empty but not null. 630 if (organizationData.companyName == null && 631 organizationData.departmentName == null) { 632 // Probably the "TITLE" property comes before the "ORG" property via 633 // handleTitleLine(). 634 organizationData.companyName = companyName; 635 organizationData.departmentName = departmentName; 636 organizationData.isPrimary = isPrimary; 637 return; 638 } 639 } 640 // No OrganizatioData is available. Create another one, with "null" title, which may be 641 // added via handleTitleValue(). 642 addNewOrganization(type, companyName, departmentName, null, phoneticName, isPrimary); 643 } 644 645 /** 646 * Set "title" value to the appropriate data. If there's more than one 647 * OrganizationData objects, this input is attached to the last one which does not 648 * have valid title value (not including empty but only null). If there's no 649 * OrganizationData object, a new OrganizationData is created, whose company name is 650 * set to null. 651 */ 652 private void handleTitleValue(final String title) { 653 if (mOrganizationList == null) { 654 // Create new first organization entry, with "null" other info, which may be 655 // added via handleOrgValue(). 656 addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); 657 return; 658 } 659 for (OrganizationData organizationData : mOrganizationList) { 660 if (organizationData.titleName == null) { 661 organizationData.titleName = title; 662 return; 663 } 664 } 665 // No Organization is available. Create another one, with "null" other info, which may be 666 // added via handleOrgValue(). 667 addNewOrganization(DEFAULT_ORGANIZATION_TYPE, null, null, title, null, false); 668 } 669 670 private void addIm(int protocol, String customProtocol, int type, 671 String propValue, boolean isPrimary) { 672 if (mImList == null) { 673 mImList = new ArrayList<ImData>(); 674 } 675 mImList.add(new ImData(protocol, customProtocol, type, propValue, isPrimary)); 676 } 677 678 private void addNote(final String note) { 679 if (mNoteList == null) { 680 mNoteList = new ArrayList<String>(1); 681 } 682 mNoteList.add(note); 683 } 684 685 private void addPhotoBytes(String formatName, byte[] photoBytes, boolean isPrimary) { 686 if (mPhotoList == null) { 687 mPhotoList = new ArrayList<PhotoData>(1); 688 } 689 final PhotoData photoData = new PhotoData(0, null, photoBytes, isPrimary); 690 mPhotoList.add(photoData); 691 } 692 693 /** 694 * Tries to extract paramMap, constructs SORT-AS parameter values, and store them in 695 * appropriate phonetic name variables. 696 * 697 * This method does not care the vCard version. Even when we have SORT-AS parameters in 698 * invalid versions (i.e. 2.1 and 3.0), we scilently accept them so that we won't drop 699 * meaningful information. If we had this parameter in the N field of vCard 3.0, and 700 * the contact data also have SORT-STRING, we will prefer SORT-STRING, since it is 701 * regitimate property to be understood. 702 */ 703 private void tryHandleSortAsName(final Map<String, Collection<String>> paramMap) { 704 if (VCardConfig.isVersion30(mVCardType) && 705 !(TextUtils.isEmpty(mPhoneticFamilyName) && 706 TextUtils.isEmpty(mPhoneticMiddleName) && 707 TextUtils.isEmpty(mPhoneticGivenName))) { 708 return; 709 } 710 711 final Collection<String> sortAsCollection = paramMap.get(VCardConstants.PARAM_SORT_AS); 712 if (sortAsCollection != null && sortAsCollection.size() != 0) { 713 if (sortAsCollection.size() > 1) { 714 Log.w(LOG_TAG, "Incorrect multiple SORT_AS parameters detected: " + 715 Arrays.toString(sortAsCollection.toArray())); 716 } 717 final List<String> sortNames = 718 VCardUtils.constructListFromValue(sortAsCollection.iterator().next(), 719 mVCardType); 720 int size = sortNames.size(); 721 if (size > 3) { 722 size = 3; 723 } 724 switch (size) { 725 case 3: mPhoneticMiddleName = sortNames.get(2); //$FALL-THROUGH$ 726 case 2: mPhoneticGivenName = sortNames.get(1); //$FALL-THROUGH$ 727 default: mPhoneticFamilyName = sortNames.get(0); break; 728 } 729 } 730 } 731 732 @SuppressWarnings("fallthrough") 733 private void handleNProperty(final List<String> paramValues, 734 Map<String, Collection<String>> paramMap) { 735 // in vCard 4.0, SORT-AS parameter is available. 736 tryHandleSortAsName(paramMap); 737 738 // Family, Given, Middle, Prefix, Suffix. (1 - 5) 739 int size; 740 if (paramValues == null || (size = paramValues.size()) < 1) { 741 return; 742 } 743 if (size > 5) { 744 size = 5; 745 } 746 747 switch (size) { 748 // Fall-through. 749 case 5: mSuffix = paramValues.get(4); 750 case 4: mPrefix = paramValues.get(3); 751 case 3: mMiddleName = paramValues.get(2); 752 case 2: mGivenName = paramValues.get(1); 753 default: mFamilyName = paramValues.get(0); 754 } 755 } 756 757 /** 758 * Note: Some Japanese mobile phones use this field for phonetic name, 759 * since vCard 2.1 does not have "SORT-STRING" type. 760 * Also, in some cases, the field has some ';'s in it. 761 * Assume the ';' means the same meaning in N property 762 */ 763 @SuppressWarnings("fallthrough") 764 private void handlePhoneticNameFromSound(List<String> elems) { 765 if (!(TextUtils.isEmpty(mPhoneticFamilyName) && 766 TextUtils.isEmpty(mPhoneticMiddleName) && 767 TextUtils.isEmpty(mPhoneticGivenName))) { 768 // This means the other properties like "X-PHONETIC-FIRST-NAME" was already found. 769 // Ignore "SOUND;X-IRMC-N". 770 return; 771 } 772 773 int size; 774 if (elems == null || (size = elems.size()) < 1) { 775 return; 776 } 777 778 // Assume that the order is "Family, Given, Middle". 779 // This is not from specification but mere assumption. Some Japanese phones use this order. 780 if (size > 3) { 781 size = 3; 782 } 783 784 if (elems.get(0).length() > 0) { 785 boolean onlyFirstElemIsNonEmpty = true; 786 for (int i = 1; i < size; i++) { 787 if (elems.get(i).length() > 0) { 788 onlyFirstElemIsNonEmpty = false; 789 break; 790 } 791 } 792 if (onlyFirstElemIsNonEmpty) { 793 final String[] namesArray = elems.get(0).split(" "); 794 final int nameArrayLength = namesArray.length; 795 if (nameArrayLength == 3) { 796 // Assume the string is "Family Middle Given". 797 mPhoneticFamilyName = namesArray[0]; 798 mPhoneticMiddleName = namesArray[1]; 799 mPhoneticGivenName = namesArray[2]; 800 } else if (nameArrayLength == 2) { 801 // Assume the string is "Family Given" based on the Japanese mobile 802 // phones' preference. 803 mPhoneticFamilyName = namesArray[0]; 804 mPhoneticGivenName = namesArray[1]; 805 } else { 806 mPhoneticFullName = elems.get(0); 807 } 808 return; 809 } 810 } 811 812 switch (size) { 813 // fallthrough 814 case 3: mPhoneticMiddleName = elems.get(2); 815 case 2: mPhoneticGivenName = elems.get(1); 816 default: mPhoneticFamilyName = elems.get(0); 817 } 818 } 819 820 public void addProperty(final Property property) { 821 final String propName = property.mPropertyName; 822 final Map<String, Collection<String>> paramMap = property.mParameterMap; 823 final List<String> propValueList = property.mPropertyValueList; 824 byte[] propBytes = property.mPropertyBytes; 825 826 if (propValueList.size() == 0) { 827 return; 828 } 829 final String propValue = listToString(propValueList).trim(); 830 831 if (propName.equals(VCardConstants.PROPERTY_VERSION)) { 832 // vCard version. Ignore this. 833 } else if (propName.equals(VCardConstants.PROPERTY_FN)) { 834 mFormattedName = propValue; 835 } else if (propName.equals(VCardConstants.PROPERTY_NAME) && mFormattedName == null) { 836 // Only in vCard 3.0. Use this if FN, which must exist in vCard 3.0 but may not 837 // actually exist in the real vCard data, does not exist. 838 mFormattedName = propValue; 839 } else if (propName.equals(VCardConstants.PROPERTY_N)) { 840 handleNProperty(propValueList, paramMap); 841 } else if (propName.equals(VCardConstants.PROPERTY_SORT_STRING)) { 842 mPhoneticFullName = propValue; 843 } else if (propName.equals(VCardConstants.PROPERTY_NICKNAME) || 844 propName.equals(VCardConstants.ImportOnly.PROPERTY_X_NICKNAME)) { 845 addNickName(propValue); 846 } else if (propName.equals(VCardConstants.PROPERTY_SOUND)) { 847 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 848 if (typeCollection != null 849 && typeCollection.contains(VCardConstants.PARAM_TYPE_X_IRMC_N)) { 850 // As of 2009-10-08, Parser side does not split a property value into separated 851 // values using ';' (in other words, propValueList.size() == 1), 852 // which is correct behavior from the view of vCard 2.1. 853 // But we want it to be separated, so do the separation here. 854 final List<String> phoneticNameList = 855 VCardUtils.constructListFromValue(propValue, mVCardType); 856 handlePhoneticNameFromSound(phoneticNameList); 857 } else { 858 // Ignore this field since Android cannot understand what it is. 859 } 860 } else if (propName.equals(VCardConstants.PROPERTY_ADR)) { 861 boolean valuesAreAllEmpty = true; 862 for (String value : propValueList) { 863 if (value.length() > 0) { 864 valuesAreAllEmpty = false; 865 break; 866 } 867 } 868 if (valuesAreAllEmpty) { 869 return; 870 } 871 872 int type = -1; 873 String label = ""; 874 boolean isPrimary = false; 875 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 876 if (typeCollection != null) { 877 for (final String typeStringOrg : typeCollection) { 878 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 879 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 880 isPrimary = true; 881 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 882 type = StructuredPostal.TYPE_HOME; 883 label = ""; 884 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK) || 885 typeStringUpperCase.equalsIgnoreCase( 886 VCardConstants.PARAM_EXTRA_TYPE_COMPANY)) { 887 // "COMPANY" seems emitted by Windows Mobile, which is not 888 // specifically supported by vCard 2.1. We assume this is same 889 // as "WORK". 890 type = StructuredPostal.TYPE_WORK; 891 label = ""; 892 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_PARCEL) || 893 typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_DOM) || 894 typeStringUpperCase.equals(VCardConstants.PARAM_ADR_TYPE_INTL)) { 895 // We do not have any appropriate way to store this information. 896 } else if (type < 0) { // If no other type is specified before 897 type = StructuredPostal.TYPE_CUSTOM; 898 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 899 label = typeStringOrg.substring(2); 900 } else { 901 label = typeStringOrg; 902 } 903 } 904 } 905 } 906 // We use "HOME" as default 907 if (type < 0) { 908 type = StructuredPostal.TYPE_HOME; 909 } 910 911 addPostal(type, propValueList, label, isPrimary); 912 } else if (propName.equals(VCardConstants.PROPERTY_EMAIL)) { 913 int type = -1; 914 String label = null; 915 boolean isPrimary = false; 916 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 917 if (typeCollection != null) { 918 for (final String typeStringOrg : typeCollection) { 919 final String typeStringUpperCase = typeStringOrg.toUpperCase(); 920 if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_PREF)) { 921 isPrimary = true; 922 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_HOME)) { 923 type = Email.TYPE_HOME; 924 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_WORK)) { 925 type = Email.TYPE_WORK; 926 } else if (typeStringUpperCase.equals(VCardConstants.PARAM_TYPE_CELL)) { 927 type = Email.TYPE_MOBILE; 928 } else if (type < 0) { // If no other type is specified before 929 if (typeStringUpperCase.startsWith("X-")) { // If X- or x- 930 label = typeStringOrg.substring(2); 931 } else { 932 label = typeStringOrg; 933 } 934 type = Email.TYPE_CUSTOM; 935 } 936 } 937 } 938 if (type < 0) { 939 type = Email.TYPE_OTHER; 940 } 941 addEmail(type, propValue, label, isPrimary); 942 } else if (propName.equals(VCardConstants.PROPERTY_ORG)) { 943 // vCard specification does not specify other types. 944 final int type = Organization.TYPE_WORK; 945 boolean isPrimary = false; 946 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 947 if (typeCollection != null) { 948 for (String typeString : typeCollection) { 949 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 950 isPrimary = true; 951 } 952 } 953 } 954 handleOrgValue(type, propValueList, paramMap, isPrimary); 955 } else if (propName.equals(VCardConstants.PROPERTY_TITLE)) { 956 handleTitleValue(propValue); 957 } else if (propName.equals(VCardConstants.PROPERTY_ROLE)) { 958 // This conflicts with TITLE. Ignore for now... 959 // handleTitleValue(propValue); 960 } else if (propName.equals(VCardConstants.PROPERTY_PHOTO) || 961 propName.equals(VCardConstants.PROPERTY_LOGO)) { 962 Collection<String> paramMapValue = paramMap.get("VALUE"); 963 if (paramMapValue != null && paramMapValue.contains("URL")) { 964 // Currently we do not have appropriate example for testing this case. 965 } else { 966 final Collection<String> typeCollection = paramMap.get("TYPE"); 967 String formatName = null; 968 boolean isPrimary = false; 969 if (typeCollection != null) { 970 for (String typeValue : typeCollection) { 971 if (VCardConstants.PARAM_TYPE_PREF.equals(typeValue)) { 972 isPrimary = true; 973 } else if (formatName == null){ 974 formatName = typeValue; 975 } 976 } 977 } 978 addPhotoBytes(formatName, propBytes, isPrimary); 979 } 980 } else if (propName.equals(VCardConstants.PROPERTY_TEL)) { 981 final String phoneNumber; 982 if (VCardConfig.isVersion40(mVCardType)) { 983 // Given propValue is in URI format, not in phone number format used until 984 // vCard 3.0. 985 if (propValue.startsWith("sip:") ) { 986 if (propValue.length() > 4) { 987 if (mSipSet == null) { 988 mSipSet = new LinkedHashSet<String>(); 989 } 990 mSipSet.add(propValue.substring(4)); 991 } 992 return; 993 } else if (propValue.startsWith("tel:")) { 994 phoneNumber = propValue.substring(4); 995 } else { 996 // We don't know appropriate way to handle the other schemas. Also, 997 // we may still have non-URI phone number. To keep given data as much as 998 // we can, just save original value here. 999 phoneNumber = propValue; 1000 } 1001 } else { 1002 phoneNumber = propValue; 1003 } 1004 1005 if (propValue.length() == 0) { 1006 return; 1007 } 1008 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 1009 final Object typeObject = 1010 VCardUtils.getPhoneTypeFromStrings(typeCollection, phoneNumber); 1011 final int type; 1012 final String label; 1013 if (typeObject instanceof Integer) { 1014 type = (Integer)typeObject; 1015 label = null; 1016 } else { 1017 type = Phone.TYPE_CUSTOM; 1018 label = typeObject.toString(); 1019 } 1020 1021 final boolean isPrimary; 1022 if (typeCollection != null && 1023 typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 1024 isPrimary = true; 1025 } else { 1026 isPrimary = false; 1027 } 1028 addPhone(type, phoneNumber, label, isPrimary); 1029 } else if (propName.equals(VCardConstants.PROPERTY_X_SKYPE_PSTNNUMBER)) { 1030 // The phone number available via Skype. 1031 Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 1032 final int type = Phone.TYPE_OTHER; 1033 final boolean isPrimary; 1034 if (typeCollection != null && typeCollection.contains(VCardConstants.PARAM_TYPE_PREF)) { 1035 isPrimary = true; 1036 } else { 1037 isPrimary = false; 1038 } 1039 addPhone(type, propValue, null, isPrimary); 1040 } else if (sImMap.containsKey(propName)) { 1041 final int protocol = sImMap.get(propName); 1042 boolean isPrimary = false; 1043 int type = -1; 1044 final Collection<String> typeCollection = paramMap.get(VCardConstants.PARAM_TYPE); 1045 if (typeCollection != null) { 1046 for (String typeString : typeCollection) { 1047 if (typeString.equals(VCardConstants.PARAM_TYPE_PREF)) { 1048 isPrimary = true; 1049 } else if (type < 0) { 1050 if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_HOME)) { 1051 type = Im.TYPE_HOME; 1052 } else if (typeString.equalsIgnoreCase(VCardConstants.PARAM_TYPE_WORK)) { 1053 type = Im.TYPE_WORK; 1054 } 1055 } 1056 } 1057 } 1058 if (type < 0) { 1059 type = Im.TYPE_HOME; 1060 } 1061 addIm(protocol, null, type, propValue, isPrimary); 1062 } else if (propName.equals(VCardConstants.PROPERTY_NOTE)) { 1063 addNote(propValue); 1064 } else if (propName.equals(VCardConstants.PROPERTY_URL)) { 1065 if (mWebsiteList == null) { 1066 mWebsiteList = new ArrayList<String>(1); 1067 } 1068 mWebsiteList.add(propValue); 1069 } else if (propName.equals(VCardConstants.PROPERTY_BDAY)) { 1070 mBirthday = propValue; 1071 } else if (propName.equals(VCardConstants.PROPERTY_ANNIVERSARY)) { 1072 mAnniversary = propValue; 1073 } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_FIRST_NAME)) { 1074 mPhoneticGivenName = propValue; 1075 } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_MIDDLE_NAME)) { 1076 mPhoneticMiddleName = propValue; 1077 } else if (propName.equals(VCardConstants.PROPERTY_X_PHONETIC_LAST_NAME)) { 1078 mPhoneticFamilyName = propValue; 1079 } else if (propName.equals(VCardConstants.PROPERTY_IMPP)) { 1080 // See also RFC 4770 (for vCard 3.0) 1081 if (propValue.startsWith("sip:") && propValue.length() > 4) { 1082 if (mSipSet == null) { 1083 mSipSet = new LinkedHashSet<String>(); 1084 } 1085 mSipSet.add(propValue.substring(4)); 1086 } 1087 } else if (propName.equals(VCardConstants.PROPERTY_X_SIP)) { 1088 if (!TextUtils.isEmpty(propValue)) { 1089 if (mSipSet == null) { 1090 mSipSet = new LinkedHashSet<String>(); 1091 } 1092 1093 if (propValue.startsWith("sip:")) { 1094 if (propValue.length() > 4) { 1095 mSipSet.add(propValue.substring(4)); 1096 } else { 1097 // Empty sip value. Ignore. 1098 } 1099 } else { 1100 mSipSet.add(propValue); 1101 } 1102 } 1103 } else if (propName.equals(VCardConstants.PROPERTY_X_ANDROID_CUSTOM)) { 1104 final List<String> customPropertyList = 1105 VCardUtils.constructListFromValue(propValue, mVCardType); 1106 handleAndroidCustomProperty(customPropertyList); 1107 } else { 1108 } 1109 // Be careful when adding some logic here, as some blocks above may use "return". 1110 } 1111 1112 private void handleAndroidCustomProperty(final List<String> customPropertyList) { 1113 if (mAndroidCustomPropertyList == null) { 1114 mAndroidCustomPropertyList = new ArrayList<List<String>>(); 1115 } 1116 mAndroidCustomPropertyList.add(customPropertyList); 1117 } 1118 1119 /** 1120 * Construct the display name. The constructed data must not be null. 1121 */ 1122 private void constructDisplayName() { 1123 // FullName (created via "FN" or "NAME" field) is prefered. 1124 if (!TextUtils.isEmpty(mFormattedName)) { 1125 mDisplayName = mFormattedName; 1126 } else if (!(TextUtils.isEmpty(mFamilyName) && TextUtils.isEmpty(mGivenName))) { 1127 mDisplayName = VCardUtils.constructNameFromElements(mVCardType, 1128 mFamilyName, mMiddleName, mGivenName, mPrefix, mSuffix); 1129 } else if (!(TextUtils.isEmpty(mPhoneticFamilyName) && 1130 TextUtils.isEmpty(mPhoneticGivenName))) { 1131 mDisplayName = VCardUtils.constructNameFromElements(mVCardType, 1132 mPhoneticFamilyName, mPhoneticMiddleName, mPhoneticGivenName); 1133 } else if (mEmailList != null && mEmailList.size() > 0) { 1134 mDisplayName = mEmailList.get(0).data; 1135 } else if (mPhoneList != null && mPhoneList.size() > 0) { 1136 mDisplayName = mPhoneList.get(0).data; 1137 } else if (mPostalList != null && mPostalList.size() > 0) { 1138 mDisplayName = mPostalList.get(0).getFormattedAddress(mVCardType); 1139 } else if (mOrganizationList != null && mOrganizationList.size() > 0) { 1140 mDisplayName = mOrganizationList.get(0).getFormattedString(); 1141 } 1142 1143 if (mDisplayName == null) { 1144 mDisplayName = ""; 1145 } 1146 } 1147 1148 /** 1149 * Consolidate several fielsds (like mName) using name candidates, 1150 */ 1151 public void consolidateFields() { 1152 constructDisplayName(); 1153 1154 if (mPhoneticFullName != null) { 1155 mPhoneticFullName = mPhoneticFullName.trim(); 1156 } 1157 } 1158 1159 public Uri pushIntoContentResolver(ContentResolver resolver) { 1160 ArrayList<ContentProviderOperation> operationList = 1161 new ArrayList<ContentProviderOperation>(); 1162 // After applying the batch the first result's Uri is returned so it is important that 1163 // the RawContact is the first operation that gets inserted into the list 1164 ContentProviderOperation.Builder builder = 1165 ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); 1166 String myGroupsId = null; 1167 if (mAccount != null) { 1168 builder.withValue(RawContacts.ACCOUNT_NAME, mAccount.name); 1169 builder.withValue(RawContacts.ACCOUNT_TYPE, mAccount.type); 1170 } else { 1171 builder.withValue(RawContacts.ACCOUNT_NAME, null); 1172 builder.withValue(RawContacts.ACCOUNT_TYPE, null); 1173 } 1174 operationList.add(builder.build()); 1175 1176 if (!nameFieldsAreEmpty()) { 1177 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1178 builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0); 1179 builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE); 1180 1181 builder.withValue(StructuredName.GIVEN_NAME, mGivenName); 1182 builder.withValue(StructuredName.FAMILY_NAME, mFamilyName); 1183 builder.withValue(StructuredName.MIDDLE_NAME, mMiddleName); 1184 builder.withValue(StructuredName.PREFIX, mPrefix); 1185 builder.withValue(StructuredName.SUFFIX, mSuffix); 1186 1187 if (!(TextUtils.isEmpty(mPhoneticGivenName) 1188 && TextUtils.isEmpty(mPhoneticFamilyName) 1189 && TextUtils.isEmpty(mPhoneticMiddleName))) { 1190 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticGivenName); 1191 builder.withValue(StructuredName.PHONETIC_FAMILY_NAME, mPhoneticFamilyName); 1192 builder.withValue(StructuredName.PHONETIC_MIDDLE_NAME, mPhoneticMiddleName); 1193 } else if (!TextUtils.isEmpty(mPhoneticFullName)) { 1194 builder.withValue(StructuredName.PHONETIC_GIVEN_NAME, mPhoneticFullName); 1195 } 1196 1197 builder.withValue(StructuredName.DISPLAY_NAME, getDisplayName()); 1198 operationList.add(builder.build()); 1199 } 1200 1201 if (mNickNameList != null && mNickNameList.size() > 0) { 1202 for (String nickName : mNickNameList) { 1203 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1204 builder.withValueBackReference(Nickname.RAW_CONTACT_ID, 0); 1205 builder.withValue(Data.MIMETYPE, Nickname.CONTENT_ITEM_TYPE); 1206 builder.withValue(Nickname.TYPE, Nickname.TYPE_DEFAULT); 1207 builder.withValue(Nickname.NAME, nickName); 1208 operationList.add(builder.build()); 1209 } 1210 } 1211 1212 if (mPhoneList != null) { 1213 for (PhoneData phoneData : mPhoneList) { 1214 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1215 builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0); 1216 builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE); 1217 1218 builder.withValue(Phone.TYPE, phoneData.type); 1219 if (phoneData.type == Phone.TYPE_CUSTOM) { 1220 builder.withValue(Phone.LABEL, phoneData.label); 1221 } 1222 builder.withValue(Phone.NUMBER, phoneData.data); 1223 if (phoneData.isPrimary) { 1224 builder.withValue(Phone.IS_PRIMARY, 1); 1225 } 1226 operationList.add(builder.build()); 1227 } 1228 } 1229 1230 if (mOrganizationList != null) { 1231 for (OrganizationData organizationData : mOrganizationList) { 1232 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1233 builder.withValueBackReference(Organization.RAW_CONTACT_ID, 0); 1234 builder.withValue(Data.MIMETYPE, Organization.CONTENT_ITEM_TYPE); 1235 builder.withValue(Organization.TYPE, organizationData.type); 1236 if (organizationData.companyName != null) { 1237 builder.withValue(Organization.COMPANY, organizationData.companyName); 1238 } 1239 if (organizationData.departmentName != null) { 1240 builder.withValue(Organization.DEPARTMENT, organizationData.departmentName); 1241 } 1242 if (organizationData.titleName != null) { 1243 builder.withValue(Organization.TITLE, organizationData.titleName); 1244 } 1245 if (organizationData.phoneticName != null) { 1246 builder.withValue(Organization.PHONETIC_NAME, organizationData.phoneticName); 1247 } 1248 if (organizationData.isPrimary) { 1249 builder.withValue(Organization.IS_PRIMARY, 1); 1250 } 1251 operationList.add(builder.build()); 1252 } 1253 } 1254 1255 if (mEmailList != null) { 1256 for (EmailData emailData : mEmailList) { 1257 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1258 builder.withValueBackReference(Email.RAW_CONTACT_ID, 0); 1259 builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE); 1260 1261 builder.withValue(Email.TYPE, emailData.type); 1262 if (emailData.type == Email.TYPE_CUSTOM) { 1263 builder.withValue(Email.LABEL, emailData.label); 1264 } 1265 builder.withValue(Email.DATA, emailData.data); 1266 if (emailData.isPrimary) { 1267 builder.withValue(Data.IS_PRIMARY, 1); 1268 } 1269 operationList.add(builder.build()); 1270 } 1271 } 1272 1273 if (mPostalList != null) { 1274 for (PostalData postalData : mPostalList) { 1275 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1276 builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); 1277 VCardUtils.insertStructuredPostalDataUsingContactsStruct( 1278 mVCardType, builder, postalData); 1279 operationList.add(builder.build()); 1280 } 1281 } 1282 1283 if (mImList != null) { 1284 for (ImData imData : mImList) { 1285 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1286 builder.withValueBackReference(Im.RAW_CONTACT_ID, 0); 1287 builder.withValue(Data.MIMETYPE, Im.CONTENT_ITEM_TYPE); 1288 builder.withValue(Im.TYPE, imData.type); 1289 builder.withValue(Im.PROTOCOL, imData.protocol); 1290 builder.withValue(Im.DATA, imData.data); 1291 if (imData.protocol == Im.PROTOCOL_CUSTOM) { 1292 builder.withValue(Im.CUSTOM_PROTOCOL, imData.customProtocol); 1293 } 1294 if (imData.isPrimary) { 1295 builder.withValue(Data.IS_PRIMARY, 1); 1296 } 1297 operationList.add(builder.build()); 1298 } 1299 } 1300 1301 if (mNoteList != null) { 1302 for (String note : mNoteList) { 1303 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1304 builder.withValueBackReference(Note.RAW_CONTACT_ID, 0); 1305 builder.withValue(Data.MIMETYPE, Note.CONTENT_ITEM_TYPE); 1306 builder.withValue(Note.NOTE, note); 1307 operationList.add(builder.build()); 1308 } 1309 } 1310 1311 if (mPhotoList != null) { 1312 for (PhotoData photoData : mPhotoList) { 1313 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1314 builder.withValueBackReference(Photo.RAW_CONTACT_ID, 0); 1315 builder.withValue(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE); 1316 builder.withValue(Photo.PHOTO, photoData.photoBytes); 1317 if (photoData.isPrimary) { 1318 builder.withValue(Photo.IS_PRIMARY, 1); 1319 } 1320 operationList.add(builder.build()); 1321 } 1322 } 1323 1324 if (mWebsiteList != null) { 1325 for (String website : mWebsiteList) { 1326 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1327 builder.withValueBackReference(Website.RAW_CONTACT_ID, 0); 1328 builder.withValue(Data.MIMETYPE, Website.CONTENT_ITEM_TYPE); 1329 builder.withValue(Website.URL, website); 1330 // There's no information about the type of URL in vCard. 1331 // We use TYPE_HOMEPAGE for safety. 1332 builder.withValue(Website.TYPE, Website.TYPE_HOMEPAGE); 1333 operationList.add(builder.build()); 1334 } 1335 } 1336 1337 if (!TextUtils.isEmpty(mBirthday)) { 1338 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1339 builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); 1340 builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); 1341 builder.withValue(Event.START_DATE, mBirthday); 1342 builder.withValue(Event.TYPE, Event.TYPE_BIRTHDAY); 1343 operationList.add(builder.build()); 1344 } 1345 1346 if (!TextUtils.isEmpty(mAnniversary)) { 1347 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1348 builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); 1349 builder.withValue(Data.MIMETYPE, Event.CONTENT_ITEM_TYPE); 1350 builder.withValue(Event.START_DATE, mAnniversary); 1351 builder.withValue(Event.TYPE, Event.TYPE_ANNIVERSARY); 1352 operationList.add(builder.build()); 1353 } 1354 1355 if (mSipSet != null && !mSipSet.isEmpty()) { 1356 for (String sipAddress : mSipSet) { 1357 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1358 builder.withValueBackReference(Event.RAW_CONTACT_ID, 0); 1359 builder.withValue(Data.MIMETYPE, SipAddress.CONTENT_ITEM_TYPE); 1360 builder.withValue(SipAddress.SIP_ADDRESS, sipAddress); 1361 operationList.add(builder.build()); 1362 } 1363 } 1364 1365 if (mAndroidCustomPropertyList != null) { 1366 for (List<String> customPropertyList : mAndroidCustomPropertyList) { 1367 int size = customPropertyList.size(); 1368 if (size < 2 || TextUtils.isEmpty(customPropertyList.get(0))) { 1369 continue; 1370 } else if (size > VCardConstants.MAX_DATA_COLUMN + 1) { 1371 size = VCardConstants.MAX_DATA_COLUMN + 1; 1372 customPropertyList = 1373 customPropertyList.subList(0, VCardConstants.MAX_DATA_COLUMN + 2); 1374 } 1375 1376 int i = 0; 1377 for (final String customPropertyValue : customPropertyList) { 1378 if (i == 0) { 1379 final String mimeType = customPropertyValue; 1380 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1381 builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); 1382 builder.withValue(Data.MIMETYPE, mimeType); 1383 } else { // 1 <= i && i <= MAX_DATA_COLUMNS 1384 if (!TextUtils.isEmpty(customPropertyValue)) { 1385 builder.withValue("data" + i, customPropertyValue); 1386 } 1387 } 1388 1389 i++; 1390 } 1391 operationList.add(builder.build()); 1392 } 1393 } 1394 1395 if (myGroupsId != null) { 1396 builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); 1397 builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0); 1398 builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE); 1399 builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId); 1400 operationList.add(builder.build()); 1401 } 1402 1403 try { 1404 ContentProviderResult[] results = resolver.applyBatch( 1405 ContactsContract.AUTHORITY, operationList); 1406 // the first result is always the raw_contact. return it's uri so 1407 // that it can be found later. do null checking for badly behaving 1408 // ContentResolvers 1409 return (results == null || results.length == 0 || results[0] == null) 1410 ? null 1411 : results[0].uri; 1412 } catch (RemoteException e) { 1413 Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); 1414 return null; 1415 } catch (OperationApplicationException e) { 1416 Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage())); 1417 return null; 1418 } 1419 } 1420 1421 public static VCardEntry buildFromResolver(ContentResolver resolver) { 1422 return buildFromResolver(resolver, Contacts.CONTENT_URI); 1423 } 1424 1425 public static VCardEntry buildFromResolver(ContentResolver resolver, Uri uri) { 1426 1427 return null; 1428 } 1429 1430 private boolean nameFieldsAreEmpty() { 1431 return (TextUtils.isEmpty(mFamilyName) 1432 && TextUtils.isEmpty(mMiddleName) 1433 && TextUtils.isEmpty(mGivenName) 1434 && TextUtils.isEmpty(mPrefix) 1435 && TextUtils.isEmpty(mSuffix) 1436 && TextUtils.isEmpty(mFormattedName) 1437 && TextUtils.isEmpty(mPhoneticFamilyName) 1438 && TextUtils.isEmpty(mPhoneticMiddleName) 1439 && TextUtils.isEmpty(mPhoneticGivenName) 1440 && TextUtils.isEmpty(mPhoneticFullName)); 1441 } 1442 1443 public boolean isIgnorable() { 1444 return getDisplayName().length() == 0; 1445 } 1446 1447 private String listToString(List<String> list){ 1448 final int size = list.size(); 1449 if (size > 1) { 1450 StringBuilder builder = new StringBuilder(); 1451 int i = 0; 1452 for (String type : list) { 1453 builder.append(type); 1454 if (i < size - 1) { 1455 builder.append(";"); 1456 } 1457 } 1458 return builder.toString(); 1459 } else if (size == 1) { 1460 return list.get(0); 1461 } else { 1462 return ""; 1463 } 1464 } 1465 1466 // All getter methods should be used carefully, since they may change 1467 // in the future as of 2009-10-05, on which I cannot be sure this structure 1468 // is completely consolidated. 1469 // 1470 // Also note that these getter methods should be used only after 1471 // all properties being pushed into this object. If not, incorrect 1472 // value will "be stored in the local cache and" be returned to you. 1473 1474 public String getFamilyName() { 1475 return mFamilyName; 1476 } 1477 1478 public String getGivenName() { 1479 return mGivenName; 1480 } 1481 1482 public String getMiddleName() { 1483 return mMiddleName; 1484 } 1485 1486 public String getPrefix() { 1487 return mPrefix; 1488 } 1489 1490 public String getSuffix() { 1491 return mSuffix; 1492 } 1493 1494 public String getFullName() { 1495 return mFormattedName; 1496 } 1497 1498 public String getPhoneticFamilyName() { 1499 return mPhoneticFamilyName; 1500 } 1501 1502 public String getPhoneticGivenName() { 1503 return mPhoneticGivenName; 1504 } 1505 1506 public String getPhoneticMiddleName() { 1507 return mPhoneticMiddleName; 1508 } 1509 1510 public String getPhoneticFullName() { 1511 return mPhoneticFullName; 1512 } 1513 1514 public final List<String> getNickNameList() { 1515 return mNickNameList; 1516 } 1517 1518 public String getBirthday() { 1519 return mBirthday; 1520 } 1521 1522 public final List<String> getNotes() { 1523 return mNoteList; 1524 } 1525 1526 public final List<PhoneData> getPhoneList() { 1527 return mPhoneList; 1528 } 1529 1530 public final List<EmailData> getEmailList() { 1531 return mEmailList; 1532 } 1533 1534 public final List<PostalData> getPostalList() { 1535 return mPostalList; 1536 } 1537 1538 public final List<OrganizationData> getOrganizationList() { 1539 return mOrganizationList; 1540 } 1541 1542 public final List<ImData> getImList() { 1543 return mImList; 1544 } 1545 1546 public final List<PhotoData> getPhotoList() { 1547 return mPhotoList; 1548 } 1549 1550 public final List<String> getWebsiteList() { 1551 return mWebsiteList; 1552 } 1553 1554 public String getDisplayName() { 1555 if (mDisplayName == null) { 1556 constructDisplayName(); 1557 } 1558 return mDisplayName; 1559 } 1560} 1561