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