OSDN Git Service

original
[gb-231r1-is01/Gingerbread_2.3.3_r1_IS01.git] / frameworks / base / core / java / android / pim / vcard / VCardComposer.java
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 }