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 android.pim.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.EntityIterator; 23 import android.content.Entity.NamedContentValues; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteException; 26 import android.net.Uri; 27 import android.pim.vcard.exception.VCardException; 28 import android.provider.ContactsContract.Contacts; 29 import android.provider.ContactsContract.Data; 30 import android.provider.ContactsContract.RawContacts; 31 import android.provider.ContactsContract.RawContactsEntity; 32 import android.provider.ContactsContract.CommonDataKinds.Email; 33 import android.provider.ContactsContract.CommonDataKinds.Event; 34 import android.provider.ContactsContract.CommonDataKinds.Im; 35 import android.provider.ContactsContract.CommonDataKinds.Nickname; 36 import android.provider.ContactsContract.CommonDataKinds.Note; 37 import android.provider.ContactsContract.CommonDataKinds.Organization; 38 import android.provider.ContactsContract.CommonDataKinds.Phone; 39 import android.provider.ContactsContract.CommonDataKinds.Photo; 40 import android.provider.ContactsContract.CommonDataKinds.Relation; 41 import android.provider.ContactsContract.CommonDataKinds.StructuredName; 42 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 43 import android.provider.ContactsContract.CommonDataKinds.Website; 44 import android.util.CharsetUtils; 45 import android.util.Log; 46 47 import java.io.BufferedWriter; 48 import java.io.FileOutputStream; 49 import java.io.IOException; 50 import java.io.OutputStream; 51 import java.io.OutputStreamWriter; 52 import java.io.UnsupportedEncodingException; 53 import java.io.Writer; 54 import java.lang.reflect.InvocationTargetException; 55 import java.lang.reflect.Method; 56 import java.nio.charset.UnsupportedCharsetException; 57 import java.util.ArrayList; 58 import java.util.HashMap; 59 import java.util.List; 60 import java.util.Map; 61 62 /** 63 * <p> 64 * The class for composing VCard from Contacts information. Note that this is 65 * completely differnt implementation from 66 * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore. 67 * </p> 68 * 69 * <p> 70 * Usually, this class should be used like this. 71 * </p> 72 * 73 * <pre class="prettyprint">VCardComposer composer = null; 74 * try { 75 * composer = new VCardComposer(context); 76 * composer.addHandler( 77 * composer.new HandlerForOutputStream(outputStream)); 78 * if (!composer.init()) { 79 * // Do something handling the situation. 80 * return; 81 * } 82 * while (!composer.isAfterLast()) { 83 * if (mCanceled) { 84 * // Assume a user may cancel this operation during the export. 85 * return; 86 * } 87 * if (!composer.createOneEntry()) { 88 * // Do something handling the error situation. 89 * return; 90 * } 91 * } 92 * } finally { 93 * if (composer != null) { 94 * composer.terminate(); 95 * } 96 * } </pre> 97 */ 98 public class VCardComposer { 99 private static final String LOG_TAG = "VCardComposer"; 100 101 public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME; 102 public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME; 103 public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER; 104 105 public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO = 106 "Failed to get database information"; 107 108 public static final String FAILURE_REASON_NO_ENTRY = 109 "There's no exportable in the database"; 110 111 public static final String FAILURE_REASON_NOT_INITIALIZED = 112 "The vCard composer object is not correctly initialized"; 113 114 /** Should be visible only from developers... (no need to translate, hopefully) */ 115 public static final String FAILURE_REASON_UNSUPPORTED_URI = 116 "The Uri vCard composer received is not supported by the composer."; 117 118 public static final String NO_ERROR = "No error"; 119 120 public static final String VCARD_TYPE_STRING_DOCOMO = "docomo"; 121 122 private static final String SHIFT_JIS = "SHIFT_JIS"; 123 private static final String UTF_8 = "UTF-8"; 124 125 /** 126 * Special URI for testing. 127 */ 128 public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard"; 129 public static final Uri VCARD_TEST_AUTHORITY_URI = 130 Uri.parse("content://" + VCARD_TEST_AUTHORITY); 131 public static final Uri CONTACTS_TEST_CONTENT_URI = 132 Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts"); 133 134 private static final Map<Integer, String> sImMap; 135 136 static { 137 sImMap = new HashMap<Integer, String>(); 138 sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM); 139 sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN); 140 sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO); 141 sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ); 142 sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER); 143 sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME); 144 // Google talk is a special case. 145 } 146 147 public static interface OneEntryHandler { 148 public boolean onInit(Context context); 149 public boolean onEntryCreated(String vcard); 150 public void onTerminate(); 151 } 152 153 /** 154 * <p> 155 * An useful example handler, which emits VCard String to outputstream one by one. 156 * </p> 157 * <p> 158 * The input OutputStream object is closed() on {@link #onTerminate()}. 159 * Must not close the stream outside. 160 * </p> 161 */ 162 public class HandlerForOutputStream implements OneEntryHandler { 163 @SuppressWarnings("hiding") 164 private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream"; 165 166 final private OutputStream mOutputStream; // mWriter will close this. 167 private Writer mWriter; 168 169 private boolean mOnTerminateIsCalled = false; 170 171 /** 172 * Input stream will be closed on the detruction of this object. 173 */ 174 public HandlerForOutputStream(OutputStream outputStream) { 175 mOutputStream = outputStream; 176 } 177 178 public boolean onInit(Context context) { 179 try { 180 mWriter = new BufferedWriter(new OutputStreamWriter( 181 mOutputStream, mCharsetString)); 182 } catch (UnsupportedEncodingException e1) { 183 Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString); 184 mErrorReason = "Encoding is not supported (usually this does not happen!): " 185 + mCharsetString; 186 return false; 187 } 188 189 if (mIsDoCoMo) { 190 try { 191 // Create one empty entry. 192 mWriter.write(createOneEntryInternal("-1", null)); 193 } catch (VCardException e) { 194 Log.e(LOG_TAG, "VCardException has been thrown during on Init(): " + 195 e.getMessage()); 196 return false; 197 } catch (IOException e) { 198 Log.e(LOG_TAG, 199 "IOException occurred during exportOneContactData: " 200 + e.getMessage()); 201 mErrorReason = "IOException occurred: " + e.getMessage(); 202 return false; 203 } 204 } 205 return true; 206 } 207 208 public boolean onEntryCreated(String vcard) { 209 try { 210 mWriter.write(vcard); 211 } catch (IOException e) { 212 Log.e(LOG_TAG, 213 "IOException occurred during exportOneContactData: " 214 + e.getMessage()); 215 mErrorReason = "IOException occurred: " + e.getMessage(); 216 return false; 217 } 218 return true; 219 } 220 221 public void onTerminate() { 222 mOnTerminateIsCalled = true; 223 if (mWriter != null) { 224 try { 225 // Flush and sync the data so that a user is able to pull 226 // the SDCard just after 227 // the export. 228 mWriter.flush(); 229 if (mOutputStream != null 230 && mOutputStream instanceof FileOutputStream) { 231 ((FileOutputStream) mOutputStream).getFD().sync(); 232 } 233 } catch (IOException e) { 234 Log.d(LOG_TAG, 235 "IOException during closing the output stream: " 236 + e.getMessage()); 237 } finally { 238 try { 239 mWriter.close(); 240 } catch (IOException e) { 241 } 242 } 243 } 244 } 245 246 @Override 247 public void finalize() { 248 if (!mOnTerminateIsCalled) { 249 onTerminate(); 250 } 251 } 252 } 253 254 private final Context mContext; 255 private final int mVCardType; 256 private final boolean mCareHandlerErrors; 257 private final ContentResolver mContentResolver; 258 259 private final boolean mIsDoCoMo; 260 private final boolean mUsesShiftJis; 261 private Cursor mCursor; 262 private int mIdColumn; 263 264 private final String mCharsetString; 265 private boolean mTerminateIsCalled; 266 private final List<OneEntryHandler> mHandlerList; 267 268 private String mErrorReason = NO_ERROR; 269 270 private static final String[] sContactsProjection = new String[] { 271 Contacts._ID, 272 }; 273 274 public VCardComposer(Context context) { 275 this(context, VCardConfig.VCARD_TYPE_DEFAULT, true); 276 } 277 278 public VCardComposer(Context context, int vcardType) { 279 this(context, vcardType, true); 280 } 281 282 public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) { 283 this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors); 284 } 285 286 /** 287 * Construct for supporting call log entry vCard composing. 288 */ 289 public VCardComposer(final Context context, final int vcardType, 290 final boolean careHandlerErrors) { 291 mContext = context; 292 mVCardType = vcardType; 293 mCareHandlerErrors = careHandlerErrors; 294 mContentResolver = context.getContentResolver(); 295 296 mIsDoCoMo = VCardConfig.isDoCoMo(vcardType); 297 mUsesShiftJis = VCardConfig.usesShiftJis(vcardType); 298 mHandlerList = new ArrayList<OneEntryHandler>(); 299 300 if (mIsDoCoMo) { 301 String charset; 302 try { 303 charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name(); 304 } catch (UnsupportedCharsetException e) { 305 Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 306 charset = SHIFT_JIS; 307 } 308 mCharsetString = charset; 309 } else if (mUsesShiftJis) { 310 String charset; 311 try { 312 charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name(); 313 } catch (UnsupportedCharsetException e) { 314 Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is."); 315 charset = SHIFT_JIS; 316 } 317 mCharsetString = charset; 318 } else { 319 mCharsetString = UTF_8; 320 } 321 } 322 323 /** 324 * Must be called before {@link #init()}. 325 */ 326 public void addHandler(OneEntryHandler handler) { 327 if (handler != null) { 328 mHandlerList.add(handler); 329 } 330 } 331 332 /** 333 * @return Returns true when initialization is successful and all the other 334 * methods are available. Returns false otherwise. 335 */ 336 public boolean init() { 337 return init(null, null); 338 } 339 340 public boolean init(final String selection, final String[] selectionArgs) { 341 return init(Contacts.CONTENT_URI, selection, selectionArgs, null); 342 } 343 344 /** 345 * Note that this is unstable interface, may be deleted in the future. 346 */ 347 public boolean init(final Uri contentUri, final String selection, 348 final String[] selectionArgs, final String sortOrder) { 349 if (contentUri == null) { 350 return false; 351 } 352 353 if (mCareHandlerErrors) { 354 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 355 mHandlerList.size()); 356 for (OneEntryHandler handler : mHandlerList) { 357 if (!handler.onInit(mContext)) { 358 for (OneEntryHandler finished : finishedList) { 359 finished.onTerminate(); 360 } 361 return false; 362 } 363 } 364 } else { 365 // Just ignore the false returned from onInit(). 366 for (OneEntryHandler handler : mHandlerList) { 367 handler.onInit(mContext); 368 } 369 } 370 371 final String[] projection; 372 if (Contacts.CONTENT_URI.equals(contentUri) || 373 CONTACTS_TEST_CONTENT_URI.equals(contentUri)) { 374 projection = sContactsProjection; 375 } else { 376 mErrorReason = FAILURE_REASON_UNSUPPORTED_URI; 377 return false; 378 } 379 mCursor = mContentResolver.query( 380 contentUri, projection, selection, selectionArgs, sortOrder); 381 382 if (mCursor == null) { 383 mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO; 384 return false; 385 } 386 387 if (getCount() == 0 || !mCursor.moveToFirst()) { 388 try { 389 mCursor.close(); 390 } catch (SQLiteException e) { 391 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 392 } finally { 393 mCursor = null; 394 mErrorReason = FAILURE_REASON_NO_ENTRY; 395 } 396 return false; 397 } 398 399 mIdColumn = mCursor.getColumnIndex(Contacts._ID); 400 401 return true; 402 } 403 404 public boolean createOneEntry() { 405 return createOneEntry(null); 406 } 407 408 /** 409 * @param getEntityIteratorMethod For Dependency Injection. 410 * @hide just for testing. 411 */ 412 public boolean createOneEntry(Method getEntityIteratorMethod) { 413 if (mCursor == null || mCursor.isAfterLast()) { 414 mErrorReason = FAILURE_REASON_NOT_INITIALIZED; 415 return false; 416 } 417 String vcard; 418 try { 419 if (mIdColumn >= 0) { 420 vcard = createOneEntryInternal(mCursor.getString(mIdColumn), 421 getEntityIteratorMethod); 422 } else { 423 Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn); 424 return true; 425 } 426 } catch (VCardException e) { 427 Log.e(LOG_TAG, "VCardException has been thrown: " + e.getMessage()); 428 return false; 429 } catch (OutOfMemoryError error) { 430 // Maybe some data (e.g. photo) is too big to have in memory. But it 431 // should be rare. 432 Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry."); 433 System.gc(); 434 // TODO: should tell users what happened? 435 return true; 436 } finally { 437 mCursor.moveToNext(); 438 } 439 440 // This function does not care the OutOfMemoryError on the handler side 441 // :-P 442 if (mCareHandlerErrors) { 443 List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>( 444 mHandlerList.size()); 445 for (OneEntryHandler handler : mHandlerList) { 446 if (!handler.onEntryCreated(vcard)) { 447 return false; 448 } 449 } 450 } else { 451 for (OneEntryHandler handler : mHandlerList) { 452 handler.onEntryCreated(vcard); 453 } 454 } 455 456 return true; 457 } 458 459 private String createOneEntryInternal(final String contactId, 460 Method getEntityIteratorMethod) throws VCardException { 461 final Map<String, List<ContentValues>> contentValuesListMap = 462 new HashMap<String, List<ContentValues>>(); 463 // The resolver may return the entity iterator with no data. It is possible. 464 // e.g. If all the data in the contact of the given contact id are not exportable ones, 465 // they are hidden from the view of this method, though contact id itself exists. 466 EntityIterator entityIterator = null; 467 try { 468 final Uri uri = RawContactsEntity.CONTENT_URI.buildUpon() 469 .appendQueryParameter(Data.FOR_EXPORT_ONLY, "1") 470 .build(); 471 final String selection = Data.CONTACT_ID + "=?"; 472 final String[] selectionArgs = new String[] {contactId}; 473 if (getEntityIteratorMethod != null) { 474 // Please note that this branch is executed by some tests only 475 try { 476 entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null, 477 mContentResolver, uri, selection, selectionArgs, null); 478 } catch (IllegalArgumentException e) { 479 Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " + 480 e.getMessage()); 481 } catch (IllegalAccessException e) { 482 Log.e(LOG_TAG, "IllegalAccessException has been thrown: " + 483 e.getMessage()); 484 } catch (InvocationTargetException e) { 485 Log.e(LOG_TAG, "InvocationTargetException has been thrown: "); 486 StackTraceElement[] stackTraceElements = e.getCause().getStackTrace(); 487 for (StackTraceElement element : stackTraceElements) { 488 Log.e(LOG_TAG, " at " + element.toString()); 489 } 490 throw new VCardException("InvocationTargetException has been thrown: " + 491 e.getCause().getMessage()); 492 } 493 } else { 494 entityIterator = RawContacts.newEntityIterator(mContentResolver.query( 495 uri, null, selection, selectionArgs, null)); 496 } 497 498 if (entityIterator == null) { 499 Log.e(LOG_TAG, "EntityIterator is null"); 500 return ""; 501 } 502 503 if (!entityIterator.hasNext()) { 504 Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId); 505 return ""; 506 } 507 508 while (entityIterator.hasNext()) { 509 Entity entity = entityIterator.next(); 510 for (NamedContentValues namedContentValues : entity.getSubValues()) { 511 ContentValues contentValues = namedContentValues.values; 512 String key = contentValues.getAsString(Data.MIMETYPE); 513 if (key != null) { 514 List<ContentValues> contentValuesList = 515 contentValuesListMap.get(key); 516 if (contentValuesList == null) { 517 contentValuesList = new ArrayList<ContentValues>(); 518 contentValuesListMap.put(key, contentValuesList); 519 } 520 contentValuesList.add(contentValues); 521 } 522 } 523 } 524 } finally { 525 if (entityIterator != null) { 526 entityIterator.close(); 527 } 528 } 529 530 final VCardBuilder builder = new VCardBuilder(mVCardType); 531 builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE)) 532 .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE)) 533 .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE)) 534 .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE)) 535 .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE)) 536 .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE)) 537 .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE)); 538 if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) { 539 builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE)); 540 } 541 builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE)) 542 .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE)) 543 .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE)) 544 .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE)); 545 return builder.toString(); 546 } 547 548 public void terminate() { 549 for (OneEntryHandler handler : mHandlerList) { 550 handler.onTerminate(); 551 } 552 553 if (mCursor != null) { 554 try { 555 mCursor.close(); 556 } catch (SQLiteException e) { 557 Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage()); 558 } 559 mCursor = null; 560 } 561 562 mTerminateIsCalled = true; 563 } 564 565 @Override 566 public void finalize() { 567 if (!mTerminateIsCalled) { 568 terminate(); 569 } 570 } 571 572 public int getCount() { 573 if (mCursor == null) { 574 return 0; 575 } 576 return mCursor.getCount(); 577 } 578 579 public boolean isAfterLast() { 580 if (mCursor == null) { 581 return false; 582 } 583 return mCursor.isAfterLast(); 584 } 585 586 /** 587 * @return Return the error reason if possible. 588 */ 589 public String getErrorReason() { 590 return mErrorReason; 591 } 592 } 593