content: /*
   * Copyright (C) 2009 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
   * use this file except in compliance with the License. You may obtain a copy of
   * the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations under
   * the License.
   */
  package com.android.vcard;
  
  import android.content.ContentResolver;
  import android.content.ContentValues;
  import android.content.Context;
  import android.content.Entity;
  import android.content.Entity.NamedContentValues;
  import android.content.EntityIterator;
  import android.database.Cursor;
  import android.database.sqlite.SQLiteException;
  import android.net.Uri;
  import android.provider.ContactsContract.CommonDataKinds.Email;
  import android.provider.ContactsContract.CommonDataKinds.Event;
  import android.provider.ContactsContract.CommonDataKinds.Im;
  import android.provider.ContactsContract.CommonDataKinds.Nickname;
  import android.provider.ContactsContract.CommonDataKinds.Note;
  import android.provider.ContactsContract.CommonDataKinds.Organization;
  import android.provider.ContactsContract.CommonDataKinds.Phone;
  import android.provider.ContactsContract.CommonDataKinds.Photo;
  import android.provider.ContactsContract.CommonDataKinds.Relation;
  import android.provider.ContactsContract.CommonDataKinds.SipAddress;
  import android.provider.ContactsContract.CommonDataKinds.StructuredName;
  import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
  import android.provider.ContactsContract.CommonDataKinds.Website;
  import android.provider.ContactsContract.Contacts;
  import android.provider.ContactsContract.Data;
  import android.provider.ContactsContract.RawContacts;
  import android.provider.ContactsContract.RawContactsEntity;
  import android.provider.ContactsContract;
  import android.text.TextUtils;
  import android.util.Log;
  
  import java.lang.reflect.InvocationTargetException;
  import java.lang.reflect.Method;
  import java.util.ArrayList;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  /**
   * <p>
   * The class for composing vCard from Contacts information.
   * </p>
   * <p>
   * Usually, this class should be used like this.
   * </p>
   * <pre class="prettyprint">VCardComposer composer = null;
   * try {
   *     composer = new VCardComposer(context);
   *     composer.addHandler(
   *             composer.new HandlerForOutputStream(outputStream));
   *     if (!composer.init()) {
   *         // Do something handling the situation.
   *         return;
   *     }
   *     while (!composer.isAfterLast()) {
   *         if (mCanceled) {
   *             // Assume a user may cancel this operation during the export.
   *             return;
   *         }
   *         if (!composer.createOneEntry()) {
   *             // Do something handling the error situation.
   *             return;
   *         }
   *     }
   * } finally {
   *     if (composer != null) {
   *         composer.terminate();
   *     }
   * }</pre>
   * <p>
   * Users have to manually take care of memory efficiency. Even one vCard may contain
   * image of non-trivial size for mobile devices.
   * </p>
   * <p>
   * {@link VCardBuilder} is used to build each vCard.
   * </p>
   */
  public class VCardComposer {
      private static final String LOG_TAG = "VCardComposer";
      private static final boolean DEBUG = false;
  
      public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
          "Failed to get database information";
  
      public static final String FAILURE_REASON_NO_ENTRY =
          "There's no exportable in the database";
  
      public static final String FAILURE_REASON_NOT_INITIALIZED =
          "The vCard composer object is not correctly initialized";
  
      /** Should be visible only from developers... (no need to translate, hopefully) */
      public static final String FAILURE_REASON_UNSUPPORTED_URI =
          "The Uri vCard composer received is not supported by the composer.";
  
      public static final String NO_ERROR = "No error";
  
      // Strictly speaking, "Shift_JIS" is the most appropriate, but we use upper version here,
      // since usual vCard devices for Japanese devices already use it.
      private static final String SHIFT_JIS = "SHIFT_JIS";
      private static final String UTF_8 = "UTF-8";
  
      private static final Map<Integer, String> sImMap;
  
      static {
          sImMap = new HashMap<Integer, String>();
          sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
          sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
          sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
          sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
          sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
          sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
          // We don't add Google talk here since it has to be handled separately.
      }
  
      private final int mVCardType;
      private final ContentResolver mContentResolver;
  
      private final boolean mIsDoCoMo;
      /**
       * Used only when {@link #mIsDoCoMo} is true. Set to true when the first vCard for DoCoMo
       * vCard is emitted.
       */
      private boolean mFirstVCardEmittedInDoCoMoCase;
  
      private Cursor mCursor;
      private boolean mCursorSuppliedFromOutside;
      private int mIdColumn;
      private Uri mContentUriForRawContactsEntity;
  
      private final String mCharset;
  
      private boolean mInitDone;
      private String mErrorReason = NO_ERROR;
  
      /**
       * Set to false when one of {@link #init()} variants is called, and set to true when
       * {@link #terminate()} is called. Initially set to true.
       */
      private boolean mTerminateCalled = true;
  
      private RawContactEntitlesInfoCallback mRawContactEntitlesInfoCallback;
  
      private static final String[] sContactsProjection = new String[] {
          Contacts._ID,
      };
  
      public VCardComposer(Context context) {
          this(context, VCardConfig.VCARD_TYPE_DEFAULT, null, true);
      }
  
      /**
       * The variant which sets charset to null and sets careHandlerErrors to true.
       */
      public VCardComposer(Context context, int vcardType) {
          this(context, vcardType, null, true);
      }
  
      public VCardComposer(Context context, int vcardType, String charset) {
          this(context, vcardType, charset, true);
      }
  
      /**
       * The variant which sets charset to null.
       */
      public VCardComposer(final Context context, final int vcardType,
              final boolean careHandlerErrors) {
          this(context, vcardType, null, careHandlerErrors);
      }
  
      /**
       * Constructs for supporting call log entry vCard composing.
       *
       * @param context Context to be used during the composition.
       * @param vcardType The type of vCard, typically available via {@link VCardConfig}.
       * @param charset The charset to be used. Use null when you don't need the charset.
       * @param careHandlerErrors If true, This object returns false everytime
       */
      public VCardComposer(final Context context, final int vcardType, String charset,
              final boolean careHandlerErrors) {
          this(context, context.getContentResolver(), vcardType, charset, careHandlerErrors);
      }
  
      /**
       * Just for testing for now.
       * @param resolver {@link ContentResolver} which used by this object.
       * @hide
       */
      public VCardComposer(final Context context, ContentResolver resolver,
              final int vcardType, String charset, final boolean careHandlerErrors) {
          // Not used right now
          // mContext = context;
          mVCardType = vcardType;
          mContentResolver = resolver;
  
          mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
  
          charset = (TextUtils.isEmpty(charset) ? VCardConfig.DEFAULT_EXPORT_CHARSET : charset);
          final boolean shouldAppendCharsetParam = !(
                  VCardConfig.isVersion30(vcardType) && UTF_8.equalsIgnoreCase(charset));
  
          if (mIsDoCoMo || shouldAppendCharsetParam) {
              if (SHIFT_JIS.equalsIgnoreCase(charset)) {
                  mCharset = charset;
              } else {
                  /* Log.w(LOG_TAG,
                          "The charset \"" + charset + "\" is used while "
                          + SHIFT_JIS + " is needed to be used."); */
                  if (TextUtils.isEmpty(charset)) {
                      mCharset = SHIFT_JIS;
                  } else {
                      mCharset = charset;
                  }
              }
          } else {
              if (TextUtils.isEmpty(charset)) {
                  mCharset = UTF_8;
              } else {
                  mCharset = charset;
              }
          }
  
          Log.d(LOG_TAG, "Use the charset \"" + mCharset + "\"");
      }
  
      /**
       * Initializes this object using default {@link Contacts#CONTENT_URI}.
       *
       * You can call this method or a variant of this method just once. In other words, you cannot
       * reuse this object.
       *
       * @return Returns true when initialization is successful and all the other
       *          methods are available. Returns false otherwise.
       */
      public boolean init() {
          return init(null, null);
      }
  
      /**
       * Special variant of init(), which accepts a Uri for obtaining {@link RawContactsEntity} from
       * {@link ContentResolver} with {@link Contacts#_ID}.
       * <code>
       * String selection = Data.CONTACT_ID + "=?";
       * String[] selectionArgs = new String[] {contactId};
       * Cursor cursor = mContentResolver.query(
       *         contentUriForRawContactsEntity, null, selection, selectionArgs, null)
       * </code>
       *
       * You can call this method or a variant of this method just once. In other words, you cannot
       * reuse this object.
       *
       * @deprecated Use {@link #init(Uri, String[], String, String[], String, Uri)} if you really
       * need to change the default Uri.
       */
      @Deprecated
      public boolean initWithRawContactsEntityUri(Uri contentUriForRawContactsEntity) {
          return init(Contacts.CONTENT_URI, sContactsProjection, null, null, null,
                  contentUriForRawContactsEntity);
      }
  
      /**
       * Initializes this object using default {@link Contacts#CONTENT_URI} and given selection
       * arguments.
       */
      public boolean init(final String selection, final String[] selectionArgs) {
          return init(Contacts.CONTENT_URI, sContactsProjection, selection, selectionArgs,
                  null, null);
      }
  
      /**
       * Note that this is unstable interface, may be deleted in the future.
       */
      public boolean init(final Uri contentUri, final String selection,
              final String[] selectionArgs, final String sortOrder) {
          return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder, null);
      }
  
      /**
       * @param contentUri Uri for obtaining the list of contactId. Used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param selection selection used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param selectionArgs selectionArgs used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param sortOrder sortOrder used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
       * contactId.
       * Note that this is an unstable interface, may be deleted in the future.
       */
      public boolean init(final Uri contentUri, final String selection,
              final String[] selectionArgs, final String sortOrder,
              final Uri contentUriForRawContactsEntity) {
          return init(contentUri, sContactsProjection, selection, selectionArgs, sortOrder,
                  contentUriForRawContactsEntity);
      }
  
      /**
       * A variant of init(). Currently just for testing. Use other variants for init().
       *
       * First we'll create {@link Cursor} for the list of contactId.
       *
       * <code>
       * Cursor cursorForId = mContentResolver.query(
       *         contentUri, projection, selection, selectionArgs, sortOrder);
       * </code>
       *
       * After that, we'll obtain data for each contactId in the list.
       *
       * <code>
       * Cursor cursorForContent = mContentResolver.query(
       *         contentUriForRawContactsEntity, null,
       *         Data.CONTACT_ID + "=?", new String[] {contactId}, null)
       * </code>
       *
       * {@link #createOneEntry()} or its variants let the caller obtain each entry from
       * <code>cursorForContent</code> above.
       *
       * @param contentUri Uri for obtaining the list of contactId. Used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param projection projection used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param selection selection used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param selectionArgs selectionArgs used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param sortOrder sortOrder used with
       * {@link ContentResolver#query(Uri, String[], String, String[], String)}
       * @param contentUriForRawContactsEntity Uri for obtaining entries relevant to each
       * contactId.
       * @return true when successful
       *
       * @hide
       */
      public boolean init(final Uri contentUri, final String[] projection,
              final String selection, final String[] selectionArgs,
              final String sortOrder, Uri contentUriForRawContactsEntity) {
          if (!ContactsContract.AUTHORITY.equals(contentUri.getAuthority())) {
              if (DEBUG) Log.d(LOG_TAG, "Unexpected contentUri: " + contentUri);
              mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
              return false;
          }
  
          if (!initInterFirstPart(contentUriForRawContactsEntity)) {
              return false;
          }
          if (!initInterCursorCreationPart(contentUri, projection, selection, selectionArgs,
                  sortOrder)) {
              return false;
          }
          if (!initInterMainPart()) {
              return false;
          }
          return initInterLastPart();
      }
  
      /**
       * Just for testing for now. Do not use.
       * @hide
       */
      public boolean init(Cursor cursor) {
          return initWithCallback(cursor, null);
      }
  
      /**
      * @param cursor Cursor that used to get contact id
      * @param rawContactEntitlesInfoCallback Callback that return RawContactEntitlesInfo
      * Note that this is an unstable interface, may be deleted in the future.
      *
      * @return true when successful
      */
      public boolean initWithCallback(Cursor cursor,
              RawContactEntitlesInfoCallback rawContactEntitlesInfoCallback) {
          if (!initInterFirstPart(null)) {
              return false;
          }
          mCursorSuppliedFromOutside = true;
          mCursor = cursor;
          mRawContactEntitlesInfoCallback = rawContactEntitlesInfoCallback;
          if (!initInterMainPart()) {
              return false;
          }
          return initInterLastPart();
      }
  
      private boolean initInterFirstPart(Uri contentUriForRawContactsEntity) {
          mContentUriForRawContactsEntity =
                  (contentUriForRawContactsEntity != null ? contentUriForRawContactsEntity :
                          RawContactsEntity.CONTENT_URI);
          if (mInitDone) {
              Log.e(LOG_TAG, "init() is already called");
              return false;
          }
          return true;
      }
  
      private boolean initInterCursorCreationPart(
              final Uri contentUri, final String[] projection,
              final String selection, final String[] selectionArgs, final String sortOrder) {
          mCursorSuppliedFromOutside = false;
          mCursor = mContentResolver.query(
                  contentUri, projection, selection, selectionArgs, sortOrder);
          if (mCursor == null) {
              Log.e(LOG_TAG, String.format("Cursor became null unexpectedly"));
              mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
              return false;
          }
          return true;
      }
  
      private boolean initInterMainPart() {
          if (mCursor.getCount() == 0 || !mCursor.moveToFirst()) {
              if (DEBUG) {
                  Log.d(LOG_TAG,
                      String.format("mCursor has an error (getCount: %d): ", mCursor.getCount()));
              }
              closeCursorIfAppropriate();
              return false;
          }
          mIdColumn = mCursor.getColumnIndex(Data.CONTACT_ID);
          if (mIdColumn < 0) {
              mIdColumn = mCursor.getColumnIndex(Contacts._ID);
          }
          return mIdColumn >= 0;
      }
  
      private boolean initInterLastPart() {
          mInitDone = true;
          mTerminateCalled = false;
          return true;
      }
  
      /**
       * @return a vCard string.
       */
      public String createOneEntry() {
          return createOneEntry(null);
      }
  
      /**
       * @hide
       */
      public String createOneEntry(Method getEntityIteratorMethod) {
          if (mIsDoCoMo && !mFirstVCardEmittedInDoCoMoCase) {
              mFirstVCardEmittedInDoCoMoCase = true;
              // Previously we needed to emit empty data for this specific case, but actually
              // this doesn't work now, as resolver doesn't return any data with "-1" contactId.
              // TODO: re-introduce or remove this logic. Needs to modify unit test when we
              // re-introduce the logic.
              // return createOneEntryInternal("-1", getEntityIteratorMethod);
          }
  
          final String vcard = createOneEntryInternal(mCursor.getLong(mIdColumn),
                  getEntityIteratorMethod);
          if (!mCursor.moveToNext()) {
              Log.e(LOG_TAG, "Cursor#moveToNext() returned false");
          }
          return vcard;
      }
  
      /**
       *  Class that store rawContactEntitlesUri and contactId
       */
      public static class RawContactEntitlesInfo {
          public final Uri rawContactEntitlesUri;
          public final long contactId;
          public RawContactEntitlesInfo(Uri rawContactEntitlesUri, long contactId) {
              this.rawContactEntitlesUri = rawContactEntitlesUri;
              this.contactId = contactId;
          }
      }
  
      /**
      * Listener for getting raw contact entitles info
      */
      public interface RawContactEntitlesInfoCallback {
          /**
          * Callback to get RawContactEntitlesInfo from contact id
          *
          * @param contactId Contact id that you want to process.
          * @return RawContactEntitlesInfo that ready to process.
          */
          RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId);
      }
  
      private String createOneEntryInternal(long contactId,
              final Method getEntityIteratorMethod) {
          final Map<String, List<ContentValues>> contentValuesListMap =
                  new HashMap<String, List<ContentValues>>();
          // The resolver may return the entity iterator with no data. It is possible.
          // e.g. If all the data in the contact of the given contact id are not exportable ones,
          //      they are hidden from the view of this method, though contact id itself exists.
          EntityIterator entityIterator = null;
          try {
              Uri uri = mContentUriForRawContactsEntity;
              if (mRawContactEntitlesInfoCallback != null) {
                  RawContactEntitlesInfo rawContactEntitlesInfo =
                          mRawContactEntitlesInfoCallback.getRawContactEntitlesInfo(contactId);
                  uri = rawContactEntitlesInfo.rawContactEntitlesUri;
                  contactId = rawContactEntitlesInfo.contactId;
              }
              final String selection = Data.CONTACT_ID + "=?";
              final String[] selectionArgs = new String[] {String.valueOf(contactId)};
              if (getEntityIteratorMethod != null) {
                  // Please note that this branch is executed by unit tests only
                  try {
                      entityIterator = (EntityIterator)getEntityIteratorMethod.invoke(null,
                              mContentResolver, uri, selection, selectionArgs, null);
                  } catch (IllegalArgumentException e) {
                      Log.e(LOG_TAG, "IllegalArgumentException has been thrown: " +
                              e.getMessage());
                  } catch (IllegalAccessException e) {
                      Log.e(LOG_TAG, "IllegalAccessException has been thrown: " +
                              e.getMessage());
                  } catch (InvocationTargetException e) {
                      Log.e(LOG_TAG, "InvocationTargetException has been thrown: ", e);
                      throw new RuntimeException("InvocationTargetException has been thrown");
                  }
              } else {
                  entityIterator = RawContacts.newEntityIterator(mContentResolver.query(
                          uri, null, selection, selectionArgs, null));
              }
  
              if (entityIterator == null) {
                  Log.e(LOG_TAG, "EntityIterator is null");
                  return "";
              }
  
              if (!entityIterator.hasNext()) {
                  Log.w(LOG_TAG, "Data does not exist. contactId: " + contactId);
                  return "";
              }
  
              while (entityIterator.hasNext()) {
                  Entity entity = entityIterator.next();
                  for (NamedContentValues namedContentValues : entity.getSubValues()) {
                      ContentValues contentValues = namedContentValues.values;
                      String key = contentValues.getAsString(Data.MIMETYPE);
                      if (key != null) {
                          List<ContentValues> contentValuesList =
                                  contentValuesListMap.get(key);
                          if (contentValuesList == null) {
                              contentValuesList = new ArrayList<ContentValues>();
                              contentValuesListMap.put(key, contentValuesList);
                          }
                          contentValuesList.add(contentValues);
                      }
                  }
              }
          } finally {
              if (entityIterator != null) {
                  entityIterator.close();
              }
          }
  
          return buildVCard(contentValuesListMap);
      }
  
      private VCardPhoneNumberTranslationCallback mPhoneTranslationCallback;
      /**
       * <p>
       * Set a callback for phone number formatting. It will be called every time when this object
       * receives a phone number for printing.
       * </p>
       * <p>
       * When this is set {@link VCardConfig#FLAG_REFRAIN_PHONE_NUMBER_FORMATTING} will be ignored
       * and the callback should be responsible for everything about phone number formatting.
       * </p>
       * <p>
       * Caution: This interface will change. Please don't use without any strong reason.
       * </p>
       */
      public void setPhoneNumberTranslationCallback(VCardPhoneNumberTranslationCallback callback) {
          mPhoneTranslationCallback = callback;
      }
  
      /**
       * Builds and returns vCard using given map, whose key is CONTENT_ITEM_TYPE defined in
       * {ContactsContract}. Developers can override this method to customize the output.
       */
      public String buildVCard(final Map<String, List<ContentValues>> contentValuesListMap) {
          if (contentValuesListMap == null) {
              Log.e(LOG_TAG, "The given map is null. Ignore and return empty String");
              return "";
          } else {
              final VCardBuilder builder = new VCardBuilder(mVCardType, mCharset);
              builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
                      .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
                      .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE),
                              mPhoneTranslationCallback)
                      .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
                      .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
                      .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
                      .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE));
              if ((mVCardType & VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT) == 0) {
                  builder.appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE));
              }
              builder.appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
                      .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
                      .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
                      .appendSipAddresses(contentValuesListMap.get(SipAddress.CONTENT_ITEM_TYPE))
                      .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
              return builder.toString();
          }
      }
  
      public void terminate() {
          closeCursorIfAppropriate();
          mTerminateCalled = true;
      }
  
      private void closeCursorIfAppropriate() {
          if (!mCursorSuppliedFromOutside && mCursor != null) {
              try {
                  mCursor.close();
              } catch (SQLiteException e) {
                  Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
              }
              mCursor = null;
          }
      }
  
      @Override
      protected void finalize() throws Throwable {
          try {
              if (!mTerminateCalled) {
                  Log.e(LOG_TAG, "finalized() is called before terminate() being called");
              }
          } finally {
              super.finalize();
          }
      }
  
      /**
       * @return returns the number of available entities. The return value is undefined
       * when this object is not ready yet (typically when {{@link #init()} is not called
       * or when {@link #terminate()} is already called).
       */
      public int getCount() {
          if (mCursor == null) {
              Log.w(LOG_TAG, "This object is not ready yet.");
              return 0;
          }
          return mCursor.getCount();
      }
  
      /**
       * @return true when there's no entity to be built. The return value is undefined
       * when this object is not ready yet.
       */
      public boolean isAfterLast() {
          if (mCursor == null) {
              Log.w(LOG_TAG, "This object is not ready yet.");
              return false;
          }
          return mCursor.isAfterLast();
      }
  
      /**
       * @return Returns the error reason.
       */
      public String getErrorReason() {
          return mErrorReason;
      }
  }
  