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