VCardComposer.java revision 49c0decf46d4f7082a17e595fba2c501a8369452
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16package android.pim.vcard; 17 18import android.content.ContentResolver; 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.Entity; 22import android.content.EntityIterator; 23import android.content.Entity.NamedContentValues; 24import android.database.Cursor; 25import android.database.sqlite.SQLiteException; 26import android.net.Uri; 27import android.os.RemoteException; 28import android.provider.CallLog; 29import android.provider.CallLog.Calls; 30import android.provider.ContactsContract.Contacts; 31import android.provider.ContactsContract.Data; 32import android.provider.ContactsContract.RawContacts; 33import android.provider.ContactsContract.CommonDataKinds.Email; 34import android.provider.ContactsContract.CommonDataKinds.Event; 35import android.provider.ContactsContract.CommonDataKinds.Im; 36import android.provider.ContactsContract.CommonDataKinds.Nickname; 37import android.provider.ContactsContract.CommonDataKinds.Note; 38import android.provider.ContactsContract.CommonDataKinds.Organization; 39import android.provider.ContactsContract.CommonDataKinds.Phone; 40import android.provider.ContactsContract.CommonDataKinds.Photo; 41import android.provider.ContactsContract.CommonDataKinds.Relation; 42import android.provider.ContactsContract.CommonDataKinds.StructuredName; 43import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 44import android.provider.ContactsContract.CommonDataKinds.Website; 45import android.text.TextUtils; 46import android.text.format.Time; 47import android.util.CharsetUtils; 48import android.util.Log; 49 50import java.io.BufferedWriter; 51import java.io.FileOutputStream; 52import java.io.IOException; 53import java.io.OutputStream; 54import java.io.OutputStreamWriter; 55import java.io.UnsupportedEncodingException; 56import java.io.Writer; 57import java.nio.charset.UnsupportedCharsetException; 58import java.util.ArrayList; 59import java.util.Arrays; 60import java.util.HashMap; 61import java.util.HashSet; 62import java.util.List; 63import java.util.Map; 64import java.util.Set; 65 66/** 67 * <p> 68 * The class for composing VCard from Contacts information. Note that this is 69 * completely differnt implementation from 70 * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. 71 * </p> 72 * 73 * <p> 74 * Usually, this class should be used like this. 75 * </p> 76 * 77 * <pre class="prettyprint">VCardComposer composer = null; 78 * try { 79 * composer = new VCardComposer(context); 80 * composer.addHandler( 81 * composer.new HandlerForOutputStream(outputStream)); 82 * if (!composer.init()) { 83 * // Do something handling the situation. 84 * return; 85 * } 86 * while (!composer.isAfterLast()) { 87 * if (mCanceled) { 88 * // Assume a user may cancel this operation during the export. 89 * return; 90 * } 91 * if (!composer.createOneEntry()) { 92 * // Do something handling the error situation. 93 * return; 94 * } 95 * } 96 * } finally { 97 * if (composer != null) { 98 * composer.terminate(); 99 * } 100 * } </pre> 101 */ 102public class VCardComposer { 103 private static final String LOG_TAG = "VCardComposer"; 104 105 public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; 106 public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; 107 public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; 108 109 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 110 "Failed to get database information"; 111 112 public static final String FAILURE_REASON_NO_ENTRY = 113 "There's no exportable in the database"; 114 115 public static final String FAILURE_REASON_NOT_INITIALIZED = 116 "The vCard composer object is not correctly initialized"; 117 118 /** Should be visible only from developers... (no need to translate, hopefully) */ 119 public static final String FAILURE_REASON_UNSUPPORTED_URI = 120 "The Uri vCard composer received is not supported by the composer."; 121 122 public static final String NO_ERROR = "No error"; 123 124 public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; 125 126 // Property for call log entry 127 private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME"; 128 private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING"; 129 private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING"; 130 private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED"; 131 132 private static final String SHIFT_JIS = "SHIFT_JIS"; 133 private static final String UTF_8 = "UTF-8"; 134 135 /** 136 * Special URI for testing. 137 */ 138 public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; 139 public static final Uri VCARD_TEST_AUTHORITY_URI = 140 Uri.parse("content://" + VCARD_TEST_AUTHORITY); 141 public static final Uri CONTACTS_TEST_CONTENT_URI = 142 Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); 143 144 private static final Uri sDataRequestUri; 145 private static final Map<Integer, String> sImMap; 146 147 static { 148 Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon(); 149 builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1"); 150 sDataRequestUri = builder.build(); 151 sImMap = new HashMap<Integer, String>(); 152 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); 153 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); 154 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); 155 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); 156 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); 157 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 158 // Google talk is a special case. 159 } 160 161 public static interface OneEntryHandler { 162 public boolean onInit(Context context); 163 public boolean onEntryCreated(String vcard); 164 public void onTerminate(); 165 } 166 167 /** 168 * <p> 169 * An useful example handler, which emits VCard String to outputstream one by one. 170 * </p> 171 * <p> 172 * The input OutputStream object is closed() on {@link #onTerminate()}. 173 * Must not close the stream outside. 174 * </p> 175 */ 176 public class HandlerForOutputStream implements OneEntryHandler { 177 @SuppressWarnings("hiding") 178 private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; 179 180 final private OutputStream mOutputStream; // mWriter will close this. 181 private Writer mWriter; 182 183 private boolean mOnTerminateIsCalled = false; 184 185 /** 186 * Input stream will be closed on the detruction of this object. 187 */ 188 public HandlerForOutputStream(OutputStream outputStream) { 189 mOutputStream = outputStream; 190 } 191 192 public boolean onInit(Context context) { 193 try { 194 mWriter = new BufferedWriter(new OutputStreamWriter( 195 mOutputStream, mCharsetString)); 196 } catch (UnsupportedEncodingException e1) { 197 Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); 198 mErrorReason = "Encoding is not supported (usually this does not happen!): " 199 + mCharsetString; 200 return false; 201 } 202 203 if (mIsDoCoMo) { 204 try { 205 // Create one empty entry. 206 mWriter.write(createOneEntryInternal("-1")); 207 } catch (IOException e) { 208 Log.e(LOG_TAG, 209 "IOException occurred during exportOneContactData: " 210 + e.getMessage()); 211 mErrorReason = "IOException occurred: " + e.getMessage(); 212 return false; 213 } 214 } 215 return true; 216 } 217 218 public boolean onEntryCreated(String vcard) { 219 try { 220 mWriter.write(vcard); 221 } catch (IOException e) { 222 Log.e(LOG_TAG, 223 "IOException occurred during exportOneContactData: " 224 + e.getMessage()); 225 mErrorReason = "IOException occurred: " + e.getMessage(); 226 return false; 227 } 228 return true; 229 } 230 231 public void onTerminate() { 232 mOnTerminateIsCalled = true; 233 if (mWriter != null) { 234 try { 235 // Flush and sync the data so that a user is able to pull 236 // the SDCard just after 237 // the export. 238 mWriter.flush(); 239 if (mOutputStream != null 240 && mOutputStream instanceof FileOutputStream) { 241 ((FileOutputStream) mOutputStream).getFD().sync(); 242 } 243 } catch (IOException e) { 244 Log.d(LOG_TAG, 245 "IOException during closing the output stream: " 246 + e.getMessage()); 247 } finally { 248 try { 249 mWriter.close(); 250 } catch (IOException e) { 251 } 252 } 253 } 254 } 255 256 @Override 257 public void finalize() { 258 if (!mOnTerminateIsCalled) { 259 onTerminate(); 260 } 261 } 262 } 263 264 private final Context mContext; 265 private final int mVCardType; 266 private final boolean mCareHandlerErrors; 267 private final ContentResolver mContentResolver; 268 269 private final boolean mIsDoCoMo; 270 private final boolean mUsesShiftJis; 271 private Cursor mCursor; 272 private int mIdColumn; 273 274 private final String mCharsetString; 275 private boolean mTerminateIsCalled; 276 final private List<OneEntryHandler> mHandlerList; 277 278 private String mErrorReason = NO_ERROR; 279 280 private boolean mIsCallLogComposer; 281 282 private static final String[] sContactsProjection = new String[] { 283 Contacts._ID, 284 }; 285 286 /** The projection to use when querying the call log table */ 287 private static final String[] sCallLogProjection = new String[] { 288 Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE, 289 Calls.CACHED_NUMBER_LABEL 290 }; 291 private static final int NUMBER_COLUMN_INDEX = 0; 292 private static final int DATE_COLUMN_INDEX = 1; 293 private static final int CALL_TYPE_COLUMN_INDEX = 2; 294 private static final int CALLER_NAME_COLUMN_INDEX = 3; 295 private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4; 296 private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5; 297 298 private static final String FLAG_TIMEZONE_UTC = "Z"; 299 300 public VCardComposer(Context context) { 301 this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); 302 } 303 304 public VCardComposer(Context context, int vcardType) { 305 this(context, vcardType, true); 306 } 307 308 public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { 309 this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); 310 } 311 312 /** 313 * Construct for supporting call log entry vCard composing. 314 */ 315 public VCardComposer(final Context context, final int vcardType, 316 final boolean careHandlerErrors) { 317 mContext = context; 318 mVCardType = vcardType; 319 mCareHandlerErrors = careHandlerErrors; 320 mContentResolver = context.getContentResolver(); 321 322 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 323 mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); 324 mHandlerList = new ArrayList<OneEntryHandler>(); 325 326 if (mIsDoCoMo) { 327 String charset; 328 try { 329 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); 330 } catch (UnsupportedCharsetException e) { 331 Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 332 charset = SHIFT_JIS; 333 } 334 mCharsetString = charset; 335 } else if (mUsesShiftJis) { 336 String charset; 337 try { 338 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); 339 } catch (UnsupportedCharsetException e) { 340 Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 341 charset = SHIFT_JIS; 342 } 343 mCharsetString = charset; 344 } else { 345 mCharsetString = UTF_8; 346 } 347 } 348 349 /** 350 * Must be called before {@link #init()}. 351 */ 352 public void addHandler(OneEntryHandler handler) { 353 if (handler != null) { 354 mHandlerList.add(handler); 355 } 356 } 357 358 /** 359 * @return Returns true when initialization is successful and all the other 360 * methods are available. Returns false otherwise. 361 */ 362 public boolean init() { 363 return init(null, null); 364 } 365 366 public boolean init(final String selection, final String[] selectionArgs) { 367 return init(Contacts.CONTENT_URI, selection, selectionArgs, null); 368 } 369 370 /** 371 * Note that this is unstable interface, may be deleted in the future. 372 */ 373 public boolean init(final Uri contentUri, final String selection, 374 final String[] selectionArgs, final String sortOrder) { 375 if (contentUri == null) { 376 return false; 377 } 378 if (mCareHandlerErrors) { 379 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 380 mHandlerList.size()); 381 for (OneEntryHandler handler : mHandlerList) { 382 if (!handler.onInit(mContext)) { 383 for (OneEntryHandler finished : finishedList) { 384 finished.onTerminate(); 385 } 386 return false; 387 } 388 } 389 } else { 390 // Just ignore the false returned from onInit(). 391 for (OneEntryHandler handler : mHandlerList) { 392 handler.onInit(mContext); 393 } 394 } 395 396 final String[] projection; 397 if (CallLog.Calls.CONTENT_URI.equals(contentUri)) { 398 projection = sCallLogProjection; 399 mIsCallLogComposer = true; 400 } else if (Contacts.CONTENT_URI.equals(contentUri) || 401 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { 402 projection = sContactsProjection; 403 } else { 404 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 405 return false; 406 } 407 mCursor = mContentResolver.query( 408 contentUri, projection, selection, selectionArgs, sortOrder); 409 410 if (mCursor == null) { 411 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 412 return false; 413 } 414 415 if (getCount() == 0 || !mCursor.moveToFirst()) { 416 try { 417 mCursor.close(); 418 } catch (SQLiteException e) { 419 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 420 } finally { 421 mCursor = null; 422 mErrorReason = FAILURE_REASON_NO_ENTRY; 423 } 424 return false; 425 } 426 427 if (mIsCallLogComposer) { 428 mIdColumn = -1; 429 } else { 430 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 431 } 432 433 return true; 434 } 435 436 public boolean createOneEntry() { 437 if (mCursor == null || mCursor.isAfterLast()) { 438 mErrorReason = FAILURE_REASON_NOT_INITIALIZED; 439 return false; 440 } 441 String name = null; 442 String vcard; 443 try { 444 if (mIsCallLogComposer) { 445 vcard = createOneCallLogEntryInternal(); 446 } else { 447 if (mIdColumn >= 0) { 448 vcard = createOneEntryInternal(mCursor.getString(mIdColumn)); 449 } else { 450 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); 451 return true; 452 } 453 } 454 } catch (OutOfMemoryError error) { 455 // Maybe some data (e.g. photo) is too big to have in memory. But it 456 // should be rare. 457 Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + name); 458 System.gc(); 459 // TODO: should tell users what happened? 460 return true; 461 } finally { 462 mCursor.moveToNext(); 463 } 464 465 // This function does not care the OutOfMemoryError on the handler side 466 // :-P 467 if (mCareHandlerErrors) { 468 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 469 mHandlerList.size()); 470 for (OneEntryHandler handler : mHandlerList) { 471 if (!handler.onEntryCreated(vcard)) { 472 return false; 473 } 474 } 475 } else { 476 for (OneEntryHandler handler : mHandlerList) { 477 handler.onEntryCreated(vcard); 478 } 479 } 480 481 return true; 482 } 483 484 private String createOneEntryInternal(final String contactId) { 485 final Map<String, List<ContentValues>> contentValuesListMap = 486 new HashMap<String, List<ContentValues>>(); 487 final String selection = Data.CONTACT_ID + "=?"; 488 final String[] selectionArgs = new String[] {contactId}; 489 // The resolver may return the entity iterator with no data. It is possiible. 490 // e.g. If all the data in the contact of the given contact id are not exportable ones, 491 // they are hidden from the view of this method, though contact id itself exists. 492 boolean dataExists = false; 493 EntityIterator entityIterator = null; 494 try { 495 entityIterator = mContentResolver.queryEntities( 496 sDataRequestUri, selection, selectionArgs, null); 497 dataExists = entityIterator.hasNext(); 498 while (entityIterator.hasNext()) { 499 Entity entity = entityIterator.next(); 500 for (NamedContentValues namedContentValues : entity.getSubValues()) { 501 ContentValues contentValues = namedContentValues.values; 502 String key = contentValues.getAsString(Data.MIMETYPE); 503 if (key != null) { 504 List<ContentValues> contentValuesList = 505 contentValuesListMap.get(key); 506 if (contentValuesList == null) { 507 contentValuesList = new ArrayList<ContentValues>(); 508 contentValuesListMap.put(key, contentValuesList); 509 } 510 contentValuesList.add(contentValues); 511 } 512 } 513 } 514 } catch (RemoteException e) { 515 Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)", 516 contactId, e.getMessage())); 517 return ""; 518 } finally { 519 if (entityIterator != null) { 520 entityIterator.close(); 521 } 522 } 523 524 if (!dataExists) { 525 return ""; 526 } 527 528 final VCardBuilder builder = new VCardBuilder(mVCardType); 529 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) 530 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) 531 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) 532 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) 533 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) 534 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) 535 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)) 536 .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)) 537 .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) 538 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) 539 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) 540 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); 541 return builder.toString(); 542 } 543 544 public void terminate() { 545 for (OneEntryHandler handler : mHandlerList) { 546 handler.onTerminate(); 547 } 548 549 if (mCursor != null) { 550 try { 551 mCursor.close(); 552 } catch (SQLiteException e) { 553 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 554 } 555 mCursor = null; 556 } 557 558 mTerminateIsCalled = true; 559 } 560 561 @Override 562 public void finalize() { 563 if (!mTerminateIsCalled) { 564 terminate(); 565 } 566 } 567 568 public int getCount() { 569 if (mCursor == null) { 570 return 0; 571 } 572 return mCursor.getCount(); 573 } 574 575 public boolean isAfterLast() { 576 if (mCursor == null) { 577 return false; 578 } 579 return mCursor.isAfterLast(); 580 } 581 582 /** 583 * @return Return the error reason if possible. 584 */ 585 public String getErrorReason() { 586 return mErrorReason; 587 } 588 589 /** 590 * This static function is to compose vCard for phone own number 591 */ 592 public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName, 593 String phoneNumber, boolean vcardVer21) { 594 final int vcardType = (vcardVer21 ? 595 VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 : 596 VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8); 597 final VCardBuilder builder = new VCardBuilder(vcardType); 598 boolean needCharset = false; 599 if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) { 600 needCharset = true; 601 } 602 builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false); 603 builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false); 604 605 if (!TextUtils.isEmpty(phoneNumber)) { 606 String label = Integer.toString(phonetype); 607 builder.appendTelLine(phonetype, label, phoneNumber, false); 608 } 609 610 return builder.toString(); 611 } 612 613 /** 614 * Format according to RFC 2445 DATETIME type. 615 * The format is: ("%Y%m%dT%H%M%SZ"). 616 */ 617 private final String toRfc2455Format(final long millSecs) { 618 Time startDate = new Time(); 619 startDate.set(millSecs); 620 String date = startDate.format2445(); 621 return date + FLAG_TIMEZONE_UTC; 622 } 623 624 /** 625 * Try to append the property line for a call history time stamp field if possible. 626 * Do nothing if the call log type gotton from the database is invalid. 627 */ 628 private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) { 629 // Extension for call history as defined in 630 // in the Specification for Ic Mobile Communcation - ver 1.1, 631 // Oct 2000. This is used to send the details of the call 632 // history - missed, incoming, outgoing along with date and time 633 // to the requesting device (For example, transferring phone book 634 // when connected over bluetooth) 635 // 636 // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z" 637 final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX); 638 final String callLogTypeStr; 639 switch (callLogType) { 640 case Calls.INCOMING_TYPE: { 641 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING; 642 break; 643 } 644 case Calls.OUTGOING_TYPE: { 645 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING; 646 break; 647 } 648 case Calls.MISSED_TYPE: { 649 callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED; 650 break; 651 } 652 default: { 653 Log.w(LOG_TAG, "Call log type not correct."); 654 return; 655 } 656 } 657 658 final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX); 659 builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP, 660 Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong)); 661 } 662 663 private String createOneCallLogEntryInternal() { 664 final VCardBuilder builder = new VCardBuilder(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8); 665 String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX); 666 if (TextUtils.isEmpty(name)) { 667 name = mCursor.getString(NUMBER_COLUMN_INDEX); 668 } 669 final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name)); 670 builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false); 671 builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false); 672 673 final String number = mCursor.getString(NUMBER_COLUMN_INDEX); 674 final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX); 675 String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX); 676 if (TextUtils.isEmpty(label)) { 677 label = Integer.toString(type); 678 } 679 builder.appendTelLine(type, label, number, false); 680 tryAppendCallHistoryTimeStampField(builder); 681 return builder.toString(); 682 } 683} 684