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