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