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 */ 16 package com.android.vcard; 17 18 import android.content.ContentResolver; 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.Entity; 22 import android.content.Entity.NamedContentValues; 23 import android.content.EntityIterator; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteException; 26 import android.net.Uri; 27 import android.provider.ContactsContract.CommonDataKinds.Email; 28 import android.provider.ContactsContract.CommonDataKinds.Event; 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.Relation; 36 import android.provider.ContactsContract.CommonDataKinds.SipAddress; 37 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 38 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 39 import android.provider.ContactsContract.CommonDataKinds.Website; 40 import android.provider.ContactsContract.Contacts; 41 import android.provider.ContactsContract.Data; 42 import android.provider.ContactsContract.RawContacts; 43 import android.provider.ContactsContract.RawContactsEntity; 44 import android.provider.ContactsContract; 45 import android.text.TextUtils; 46 import android.util.Log; 47 48 import java.lang.reflect.InvocationTargetException; 49 import java.lang.reflect.Method; 50 import java.util.ArrayList; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Map; 54 55 /** 56 * <p> 57 * The class for composing vCard from Contacts information. 58 * </p> 59 * <p> 60 * Usually, this class should be used like this. 61 * </p> 62 * <pre class="prettyprint">VCardComposer composer = null; 63 * try { 64 * composer = new VCardComposer(context); 65 * composer.addHandler( 66 * composer.new HandlerForOutputStream(outputStream)); 67 * if (!composer.init()) { 68 * // Do something handling the situation. 69 * return; 70 * } 71 * while (!composer.isAfterLast()) { 72 * if (mCanceled) { 73 * // Assume a user may cancel this operation during the export. 74 * return; 75 * } 76 * if (!composer.createOneEntry()) { 77 * // Do something handling the error situation. 78 * return; 79 * } 80 * } 81 * } finally { 82 * if (composer != null) { 83 * composer.terminate(); 84 * } 85 * }</pre> 86 * <p> 87 * Users have to manually take care of memory efficiency. Even one vCard may contain 88 * image of non-trivial size for mobile devices. 89 * </p> 90 * <p> 91 * {@link VCardBuilder} is used to build each vCard. 92 * </p> 93 */ 94 public class VCardComposer { 95 private static final String LOG_TAG = "VCardComposer"; 96 private static final boolean DEBUG = false; 97 98 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 99 "Failed to get database information"; 100 101 public static final String FAILURE_REASON_NO_ENTRY = 102 "There's no exportable in the database"; 103 104 public static final String FAILURE_REASON_NOT_INITIALIZED = 105 "The vCard composer object is not correctly initialized"; 106 107 /** Should be visible only from developers... (no need to translate, hopefully) */ 108 public static final String FAILURE_REASON_UNSUPPORTED_URI = 109 "The Uri vCard composer received is not supported by the composer."; 110 111 public static final String NO_ERROR = "No error"; 112 113 // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here, 114 // since usual vCard devices for Japanese devices already use it. 115 private static final String SHIFT_JIS = "SHIFT_JIS"; 116 private static final String UTF_8 = "UTF-8"; 117 118 private static final Map<Integer, String> sImMap; 119 120 static { 121 sImMap = new HashMap<Integer, String>(); sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM)122 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN)123 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO)124 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ)125 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER)126 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME)127 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 128 // We don't add Google talk here since it has to be handled separately. 129 } 130 131 private final int mVCardType; 132 private final ContentResolver mContentResolver; 133 134 private final boolean mIsDoCoMo; 135 /** 136 * Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo 137 * vCard is emitted. 138 */ 139 private boolean mFirstVCardEmittedInDoCoMoCase; 140 141 private Cursor mCursor; 142 private boolean mCursorSuppliedFromOutside; 143 private int mIdColumn; 144 private Uri mContentUriForRawContactsEntity; 145 146 private final String mCharset; 147 148 private boolean mInitDone; 149 private String mErrorReason = NO_ERROR; 150 151 /** 152 * Set to false when one of {@link #init()} variants is called, and set to true when 153 * {@link #terminate()} is called. Initially set to true. 154 */ 155 private boolean mTerminateCalled = true; 156 157 private RawContactEntitlesInfoCallback mRawContactEntitlesInfoCallback; 158 159 private static final String[] sContactsProjection = new String[] { 160 Contacts._ID, 161 }; 162 VCardComposer(Context context)163 public VCardComposer(Context context) { 164 this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true); 165 } 166 167 /** 168 * The variant which sets charset to null and sets careHandlerErrors to true. 169 */ VCardComposer(Context context, int vcardType)170 public VCardComposer(Context context, int vcardType) { 171 this(context, vcardType, null, true); 172 } 173 VCardComposer(Context context, int vcardType, String charset)174 public VCardComposer(Context context, int vcardType, String charset) { 175 this(context, vcardType, charset, true); 176 } 177 178 /** 179 * The variant which sets charset to null. 180 */ VCardComposer(final Context context, final int vcardType, final boolean careHandlerErrors)181 public VCardComposer(final Context context, final int vcardType, 182 final boolean careHandlerErrors) { 183 this(context, vcardType, null, careHandlerErrors); 184 } 185 186 /** 187 * Constructs for supporting call log entry vCard composing. 188 * 189 * @param context Context to be used during the composition. 190 * @param vcardType The type of vCard, typically available via {@link VCardConfig}. 191 * @param charset The charset to be used. Use null when you don't need the charset. 192 * @param careHandlerErrors If true, This object returns false everytime 193 */ VCardComposer(final Context context, final int vcardType, String charset, final boolean careHandlerErrors)194 public VCardComposer(final Context context, final int vcardType, String charset, 195 final boolean careHandlerErrors) { 196 this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors); 197 } 198 199 /** 200 * Just for testing for now. 201 * @param resolver {@link ContentResolver} which used by this object. 202 * @hide 203 */ VCardComposer(final Context context, ContentResolver resolver, final int vcardType, String charset, final boolean careHandlerErrors)204 public VCardComposer(final Context context, ContentResolver resolver, 205 final int vcardType, String charset, final boolean careHandlerErrors) { 206 // Not used right now 207 // mContext = context; 208 mVCardType = vcardType; 209 mContentResolver = resolver; 210 211 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 212 213 charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset); 214 final boolean shouldAppendCharsetParam = !( 215 VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset)); 216 217 if (mIsDoCoMo || shouldAppendCharsetParam) { 218 if (SHIFT_JIS.equalsIgnoreCase(charset)) { 219 mCharset = charset; 220 } else { 221 /* Log.w(LOG_TAG, 222 "The charset \"" + charset + "\" is used while " 223 + SHIFT_JIS + " is needed to be used."); */ 224 if (TextUtils.isEmpty(charset)) { 225 mCharset = SHIFT_JIS; 226 } else { 227 mCharset = charset; 228 } 229 } 230 } else { 231 if (TextUtils.isEmpty(charset)) { 232 mCharset = UTF_8; 233 } else { 234 mCharset = charset; 235 } 236 } 237 238 Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\""); 239 } 240 241 /** 242 * Initializes this object using default {@link Contacts#CONTENT_URI}. 243 * 244 * You can call this method or a variant of this method just once. In other words, you cannot 245 * reuse this object. 246 * 247 * @return Returns true when initialization is successful and all the other 248 * methods are available. Returns false otherwise. 249 */ init()250 public boolean init() { 251 return init(null, null); 252 } 253 254 /** 255 * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from 256 * {@link ContentResolver} with {@link Contacts#_ID}. 257 * <code> 258 * String selection = Data.CONTACT_ID + "=?"; 259 * String[] selectionArgs = new String[] {contactId}; 260 * Cursor cursor = mContentResolver.query( 261 * contentUriForRawContactsEntity, null, selection, selectionArgs, null) 262 * </code> 263 * 264 * You can call this method or a variant of this method just once. In other words, you cannot 265 * reuse this object. 266 * 267 * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really 268 * need to change the default Uri. 269 */ 270 @Deprecated initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity)271 public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) { 272 return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null, 273 contentUriForRawContactsEntity); 274 } 275 276 /** 277 * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection 278 * arguments. 279 */ init(final String selection, final String[] selectionArgs)280 public boolean init(final String selection, final String[] selectionArgs) { 281 return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs, 282 null, null); 283 } 284 285 /** 286 * Note that this is unstable interface, may be deleted in the future. 287 */ init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder)288 public boolean init(final Uri contentUri, final String selection, 289 final String[] selectionArgs, final String sortOrder) { 290 return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null); 291 } 292 293 /** 294 * @param contentUri Uri for obtaining the list of contactId. Used with 295 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 296 * @param selection selection used with 297 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 298 * @param selectionArgs selectionArgs used with 299 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 300 * @param sortOrder sortOrder used with 301 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 302 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each 303 * contactId. 304 * Note that this is an unstable interface, may be deleted in the future. 305 */ init(final Uri contentUri, final String selection, final String[] selectionArgs, final String sortOrder, final Uri contentUriForRawContactsEntity)306 public boolean init(final Uri contentUri, final String selection, 307 final String[] selectionArgs, final String sortOrder, 308 final Uri contentUriForRawContactsEntity) { 309 return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, 310 contentUriForRawContactsEntity); 311 } 312 313 /** 314 * A variant of init(). Currently just for testing. Use other variants for init(). 315 * 316 * First we'll create {@link Cursor} for the list of contactId. 317 * 318 * <code> 319 * Cursor cursorForId = mContentResolver.query( 320 * contentUri, projection, selection, selectionArgs, sortOrder); 321 * </code> 322 * 323 * After that, we'll obtain data for each contactId in the list. 324 * 325 * <code> 326 * Cursor cursorForContent = mContentResolver.query( 327 * contentUriForRawContactsEntity, null, 328 * Data.CONTACT_ID + "=?", new String[] {contactId}, null) 329 * </code> 330 * 331 * {@link #createOneEntry()} or its variants let the caller obtain each entry from 332 * <code>cursorForContent</code> above. 333 * 334 * @param contentUri Uri for obtaining the list of contactId. Used with 335 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 336 * @param projection projection used with 337 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 338 * @param selection selection used with 339 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 340 * @param selectionArgs selectionArgs used with 341 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 342 * @param sortOrder sortOrder used with 343 * {@link ContentResolver#query(Uri, String[], String, String[], String)} 344 * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each 345 * contactId. 346 * @return true when successful 347 * 348 * @hide 349 */ init(final Uri contentUri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder, Uri contentUriForRawContactsEntity)350 public boolean init(final Uri contentUri, final String[] projection, 351 final String selection, final String[] selectionArgs, 352 final String sortOrder, Uri contentUriForRawContactsEntity) { 353 if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) { 354 if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri); 355 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 356 return false; 357 } 358 359 if (!initInterFirstPart(contentUriForRawContactsEntity)) { 360 return false; 361 } 362 if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs, 363 sortOrder)) { 364 return false; 365 } 366 if (!initInterMainPart()) { 367 return false; 368 } 369 return initInterLastPart(); 370 } 371 372 /** 373 * Just for testing for now. Do not use. 374 * @hide 375 */ init(Cursor cursor)376 public boolean init(Cursor cursor) { 377 return initWithCallback(cursor, null); 378 } 379 380 /** 381 * @param cursor Cursor that used to get contact id 382 * @param rawContactEntitlesInfoCallback Callback that return RawContactEntitlesInfo 383 * Note that this is an unstable interface, may be deleted in the future. 384 * 385 * @return true when successful 386 */ initWithCallback(Cursor cursor, RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback)387 public boolean initWithCallback(Cursor cursor, 388 RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback) { 389 if (!initInterFirstPart(null)) { 390 return false; 391 } 392 mCursorSuppliedFromOutside = true; 393 mCursor = cursor; 394 mRawContactEntitlesInfoCallback = rawContactEntitlesInfoCallback; 395 if (!initInterMainPart()) { 396 return false; 397 } 398 return initInterLastPart(); 399 } 400 initInterFirstPart(Uri contentUriForRawContactsEntity)401 private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) { 402 mContentUriForRawContactsEntity = 403 (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity : 404 RawContactsEntity.CONTENT_URI); 405 if (mInitDone) { 406 Log.e(LOG_TAG, "init() is already called"); 407 return false; 408 } 409 return true; 410 } 411 initInterCursorCreationPart( final Uri contentUri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder)412 private boolean initInterCursorCreationPart( 413 final Uri contentUri, final String[] projection, 414 final String selection, final String[] selectionArgs, final String sortOrder) { 415 mCursorSuppliedFromOutside = false; 416 mCursor = mContentResolver.query( 417 contentUri, projection, selection, selectionArgs, sortOrder); 418 if (mCursor == null) { 419 Log.e(LOG_TAG, String.format("Cursor became null unexpectedly")); 420 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 421 return false; 422 } 423 return true; 424 } 425 initInterMainPart()426 private boolean initInterMainPart() { 427 if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) { 428 if (DEBUG) { 429 Log.d(LOG_TAG, 430 String.format("mCursor has an error (getCount: %d): ", mCursor.getCount())); 431 } 432 closeCursorIfAppropriate(); 433 return false; 434 } 435 mIdColumn = mCursor.getColumnIndex(Data.CONTACT_ID); 436 if (mIdColumn < 0) { 437 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 438 } 439 return mIdColumn >= 0; 440 } 441 initInterLastPart()442 private boolean initInterLastPart() { 443 mInitDone = true; 444 mTerminateCalled = false; 445 return true; 446 } 447 448 /** 449 * @return a vCard string. 450 */ createOneEntry()451 public String createOneEntry() { 452 return createOneEntry(null); 453 } 454 455 /** 456 * @hide 457 */ createOneEntry(Method getEntityIteratorMethod)458 public String createOneEntry(Method getEntityIteratorMethod) { 459 if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) { 460 mFirstVCardEmittedInDoCoMoCase = true; 461 // Previously we needed to emit empty data for this specific case, but actually 462 // this doesn't work now, as resolver doesn't return any data with "-1" contactId. 463 // TODO: re-introduce or remove this logic. Needs to modify unit test when we 464 // re-introduce the logic. 465 // return createOneEntryInternal("-1", getEntityIteratorMethod); 466 } 467 468 final String vcard = createOneEntryInternal(mCursor.getLong(mIdColumn), 469 getEntityIteratorMethod); 470 if (!mCursor.moveToNext()) { 471 Log.e(LOG_TAG, "Cursor#moveToNext() returned false"); 472 } 473 return vcard; 474 } 475 476 /** 477 * Class that store rawContactEntitlesUri and contactId 478 */ 479 public static class RawContactEntitlesInfo { 480 public final Uri rawContactEntitlesUri; 481 public final long contactId; RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId)482 public RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId) { 483 this.rawContactEntitlesUri = rawContactEntitlesUri; 484 this.contactId = contactId; 485 } 486 } 487 488 /** 489 * Listener for getting raw contact entitles info 490 */ 491 public interface RawContactEntitlesInfoCallback { 492 /** 493 * Callback to get RawContactEntitlesInfo from contact id 494 * 495 * @param contactId Contact id that you want to process. 496 * @return RawContactEntitlesInfo that ready to process. 497 */ getRawContactEntitlesInfo(long contactId)498 RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId); 499 } 500 createOneEntryInternal(long contactId, final Method getEntityIteratorMethod)501 private String createOneEntryInternal(long contactId, 502 final Method getEntityIteratorMethod) { 503 final Map<String, List<ContentValues>> contentValuesListMap = 504 new HashMap<String, List<ContentValues>>(); 505 // The resolver may return the entity iterator with no data. It is possible. 506 // e.g. If all the data in the contact of the given contact id are not exportable ones, 507 // they are hidden from the view of this method, though contact id itself exists. 508 EntityIterator entityIterator = null; 509 try { 510 Uri uri = mContentUriForRawContactsEntity; 511 if (mRawContactEntitlesInfoCallback != null) { 512 RawContactEntitlesInfo rawContactEntitlesInfo = 513 mRawContactEntitlesInfoCallback.getRawContactEntitlesInfo(contactId); 514 uri = rawContactEntitlesInfo.rawContactEntitlesUri; 515 contactId = rawContactEntitlesInfo.contactId; 516 } 517 final String selection = Data.CONTACT_ID + "=?"; 518 final String[] selectionArgs = new String[] {String.valueOf(contactId)}; 519 if (getEntityIteratorMethod != null) { 520 // Please note that this branch is executed by unit tests only 521 try { 522 entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, 523 mContentResolver, uri, selection, selectionArgs, null); 524 } catch (IllegalArgumentException e) { 525 Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + 526 e.getMessage()); 527 } catch (IllegalAccessException e) { 528 Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + 529 e.getMessage()); 530 } catch (InvocationTargetException e) { 531 Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e); 532 throw new RuntimeException("InvocationTargetException has been thrown"); 533 } 534 } else { 535 entityIterator = RawContacts.newEntityIterator(mContentResolver.query( 536 uri, null, selection, selectionArgs, null)); 537 } 538 539 if (entityIterator == null) { 540 Log.e(LOG_TAG, "EntityIterator is null"); 541 return ""; 542 } 543 544 if (!entityIterator.hasNext()) { 545 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); 546 return ""; 547 } 548 549 while (entityIterator.hasNext()) { 550 Entity entity = entityIterator.next(); 551 for (NamedContentValues namedContentValues : entity.getSubValues()) { 552 ContentValues contentValues = namedContentValues.values; 553 String key = contentValues.getAsString(Data.MIMETYPE); 554 if (key != null) { 555 List<ContentValues> contentValuesList = 556 contentValuesListMap.get(key); 557 if (contentValuesList == null) { 558 contentValuesList = new ArrayList<ContentValues>(); 559 contentValuesListMap.put(key, contentValuesList); 560 } 561 contentValuesList.add(contentValues); 562 } 563 } 564 } 565 } finally { 566 if (entityIterator != null) { 567 entityIterator.close(); 568 } 569 } 570 571 return buildVCard(contentValuesListMap); 572 } 573 574 private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback; 575 /** 576 * <p> 577 * Set a callback for phone number formatting. It will be called every time when this object 578 * receives a phone number for printing. 579 * </p> 580 * <p> 581 * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored 582 * and the callback should be responsible for everything about phone number formatting. 583 * </p> 584 * <p> 585 * Caution: This interface will change. Please don't use without any strong reason. 586 * </p> 587 */ setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback)588 public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) { 589 mPhoneTranslationCallback = callback; 590 } 591 592 /** 593 * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in 594 * {ContactsContract}. Developers can override this method to customize the output. 595 */ buildVCard(final Map<String, List<ContentValues>> contentValuesListMap)596 public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) { 597 if (contentValuesListMap == null) { 598 Log.e(LOG_TAG, "The given map is null. Ignore and return empty String"); 599 return ""; 600 } else { 601 final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset); 602 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) 603 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) 604 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE), 605 mPhoneTranslationCallback) 606 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) 607 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) 608 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) 609 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); 610 if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { 611 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); 612 } 613 builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) 614 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) 615 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) 616 .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE)) 617 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); 618 return builder.toString(); 619 } 620 } 621 terminate()622 public void terminate() { 623 closeCursorIfAppropriate(); 624 mTerminateCalled = true; 625 } 626 closeCursorIfAppropriate()627 private void closeCursorIfAppropriate() { 628 if (!mCursorSuppliedFromOutside && mCursor != null) { 629 try { 630 mCursor.close(); 631 } catch (SQLiteException e) { 632 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 633 } 634 mCursor = null; 635 } 636 } 637 638 @Override finalize()639 protected void finalize() throws Throwable { 640 try { 641 if (!mTerminateCalled) { 642 Log.e(LOG_TAG, "finalized() is called before terminate() being called"); 643 } 644 } finally { 645 super.finalize(); 646 } 647 } 648 649 /** 650 * @return returns the number of available entities. The return value is undefined 651 * when this object is not ready yet (typically when {{@link #init()} is not called 652 * or when {@link #terminate()} is already called). 653 */ getCount()654 public int getCount() { 655 if (mCursor == null) { 656 Log.w(LOG_TAG, "This object is not ready yet."); 657 return 0; 658 } 659 return mCursor.getCount(); 660 } 661 662 /** 663 * @return true when there's no entity to be built. The return value is undefined 664 * when this object is not ready yet. 665 */ isAfterLast()666 public boolean isAfterLast() { 667 if (mCursor == null) { 668 Log.w(LOG_TAG, "This object is not ready yet."); 669 return false; 670 } 671 return mCursor.isAfterLast(); 672 } 673 674 /** 675 * @return Returns the error reason. 676 */ getErrorReason()677 public String getErrorReason() { 678 return mErrorReason; 679 } 680 } 681