VCardComposer.java revision 49c0decf46d4f7082a17e595fba2c501a8369452
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 android.pim.vcard;
17
18import android.content.ContentResolver;
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.Entity;
22import android.content.EntityIterator;
23import android.content.Entity.NamedContentValues;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteException;
26import android.net.Uri;
27import android.os.RemoteException;
28import android.provider.CallLog;
29import android.provider.CallLog.Calls;
30import android.provider.ContactsContract.Contacts;
31import android.provider.ContactsContract.Data;
32import android.provider.ContactsContract.RawContacts;
33import android.provider.ContactsContract.CommonDataKinds.Email;
34import android.provider.ContactsContract.CommonDataKinds.Event;
35import android.provider.ContactsContract.CommonDataKinds.Im;
36import android.provider.ContactsContract.CommonDataKinds.Nickname;
37import android.provider.ContactsContract.CommonDataKinds.Note;
38import android.provider.ContactsContract.CommonDataKinds.Organization;
39import android.provider.ContactsContract.CommonDataKinds.Phone;
40import android.provider.ContactsContract.CommonDataKinds.Photo;
41import android.provider.ContactsContract.CommonDataKinds.Relation;
42import android.provider.ContactsContract.CommonDataKinds.StructuredName;
43import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
44import android.provider.ContactsContract.CommonDataKinds.Website;
45import android.text.TextUtils;
46import android.text.format.Time;
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.nio.charset.UnsupportedCharsetException;
58import java.util.ArrayList;
59import java.util.Arrays;
60import java.util.HashMap;
61import java.util.HashSet;
62import java.util.List;
63import java.util.Map;
64import java.util.Set;
65
66/**
67 * <p>
68 * The class for composing VCard from Contacts information. Note that this is
69 * completely differnt implementation from
70 * android.syncml.pim.vcard.VCardComposer, which is not maintained anymore.
71 * </p>
72 *
73 * <p>
74 * Usually, this class should be used like this.
75 * </p>
76 *
77 * <pre class="prettyprint">VCardComposer composer = null;
78 * try {
79 *     composer = new VCardComposer(context);
80 *     composer.addHandler(
81 *             composer.new HandlerForOutputStream(outputStream));
82 *     if (!composer.init()) {
83 *         // Do something handling the situation.
84 *         return;
85 *     }
86 *     while (!composer.isAfterLast()) {
87 *         if (mCanceled) {
88 *             // Assume a user may cancel this operation during the export.
89 *             return;
90 *         }
91 *         if (!composer.createOneEntry()) {
92 *             // Do something handling the error situation.
93 *             return;
94 *         }
95 *     }
96 * } finally {
97 *     if (composer != null) {
98 *         composer.terminate();
99 *     }
100 * } </pre>
101 */
102public class VCardComposer {
103    private static final String LOG_TAG = "VCardComposer";
104
105    public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
106    public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
107    public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
108
109    public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
110        "Failed to get database information";
111
112    public static final String FAILURE_REASON_NO_ENTRY =
113        "There's no exportable in the database";
114
115    public static final String FAILURE_REASON_NOT_INITIALIZED =
116        "The vCard composer object is not correctly initialized";
117
118    /** Should be visible only from developers... (no need to translate, hopefully) */
119    public static final String FAILURE_REASON_UNSUPPORTED_URI =
120        "The Uri vCard composer received is not supported by the composer.";
121
122    public static final String NO_ERROR = "No error";
123
124    public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
125
126    // Property for call log entry
127    private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
128    private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING";
129    private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING";
130    private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
131
132    private static final String SHIFT_JIS = "SHIFT_JIS";
133    private static final String UTF_8 = "UTF-8";
134
135    /**
136     * Special URI for testing.
137     */
138    public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
139    public static final Uri VCARD_TEST_AUTHORITY_URI =
140        Uri.parse("content://" + VCARD_TEST_AUTHORITY);
141    public static final Uri CONTACTS_TEST_CONTENT_URI =
142        Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
143
144    private static final Uri sDataRequestUri;
145    private static final Map<Integer, String> sImMap;
146
147    static {
148        Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon();
149        builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1");
150        sDataRequestUri = builder.build();
151        sImMap = new HashMap<Integer, String>();
152        sImMap.put(Im.PROTOCOL_AIM, VCardConstants.PROPERTY_X_AIM);
153        sImMap.put(Im.PROTOCOL_MSN, VCardConstants.PROPERTY_X_MSN);
154        sImMap.put(Im.PROTOCOL_YAHOO, VCardConstants.PROPERTY_X_YAHOO);
155        sImMap.put(Im.PROTOCOL_ICQ, VCardConstants.PROPERTY_X_ICQ);
156        sImMap.put(Im.PROTOCOL_JABBER, VCardConstants.PROPERTY_X_JABBER);
157        sImMap.put(Im.PROTOCOL_SKYPE, VCardConstants.PROPERTY_X_SKYPE_USERNAME);
158        // Google talk is a special case.
159    }
160
161    public static interface OneEntryHandler {
162        public boolean onInit(Context context);
163        public boolean onEntryCreated(String vcard);
164        public void onTerminate();
165    }
166
167    /**
168     * <p>
169     * An useful example handler, which emits VCard String to outputstream one by one.
170     * </p>
171     * <p>
172     * The input OutputStream object is closed() on {@link #onTerminate()}.
173     * Must not close the stream outside.
174     * </p>
175     */
176    public class HandlerForOutputStream implements OneEntryHandler {
177        @SuppressWarnings("hiding")
178        private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream";
179
180        final private OutputStream mOutputStream; // mWriter will close this.
181        private Writer mWriter;
182
183        private boolean mOnTerminateIsCalled = false;
184
185        /**
186         * Input stream will be closed on the detruction of this object.
187         */
188        public HandlerForOutputStream(OutputStream outputStream) {
189            mOutputStream = outputStream;
190        }
191
192        public boolean onInit(Context context) {
193            try {
194                mWriter = new BufferedWriter(new OutputStreamWriter(
195                        mOutputStream, mCharsetString));
196            } catch (UnsupportedEncodingException e1) {
197                Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString);
198                mErrorReason = "Encoding is not supported (usually this does not happen!): "
199                        + mCharsetString;
200                return false;
201            }
202
203            if (mIsDoCoMo) {
204                try {
205                    // Create one empty entry.
206                    mWriter.write(createOneEntryInternal("-1"));
207                } catch (IOException e) {
208                    Log.e(LOG_TAG,
209                            "IOException occurred during exportOneContactData: "
210                                    + e.getMessage());
211                    mErrorReason = "IOException occurred: " + e.getMessage();
212                    return false;
213                }
214            }
215            return true;
216        }
217
218        public boolean onEntryCreated(String vcard) {
219            try {
220                mWriter.write(vcard);
221            } catch (IOException e) {
222                Log.e(LOG_TAG,
223                        "IOException occurred during exportOneContactData: "
224                                + e.getMessage());
225                mErrorReason = "IOException occurred: " + e.getMessage();
226                return false;
227            }
228            return true;
229        }
230
231        public void onTerminate() {
232            mOnTerminateIsCalled = true;
233            if (mWriter != null) {
234                try {
235                    // Flush and sync the data so that a user is able to pull
236                    // the SDCard just after
237                    // the export.
238                    mWriter.flush();
239                    if (mOutputStream != null
240                            && mOutputStream instanceof FileOutputStream) {
241                            ((FileOutputStream) mOutputStream).getFD().sync();
242                    }
243                } catch (IOException e) {
244                    Log.d(LOG_TAG,
245                            "IOException during closing the output stream: "
246                                    + e.getMessage());
247                } finally {
248                    try {
249                        mWriter.close();
250                    } catch (IOException e) {
251                    }
252                }
253            }
254        }
255
256        @Override
257        public void finalize() {
258            if (!mOnTerminateIsCalled) {
259                onTerminate();
260            }
261        }
262    }
263
264    private final Context mContext;
265    private final int mVCardType;
266    private final boolean mCareHandlerErrors;
267    private final ContentResolver mContentResolver;
268
269    private final boolean mIsDoCoMo;
270    private final boolean mUsesShiftJis;
271    private Cursor mCursor;
272    private int mIdColumn;
273
274    private final String mCharsetString;
275    private boolean mTerminateIsCalled;
276    final private List<OneEntryHandler> mHandlerList;
277
278    private String mErrorReason = NO_ERROR;
279
280    private boolean mIsCallLogComposer;
281
282    private static final String[] sContactsProjection = new String[] {
283        Contacts._ID,
284    };
285
286    /** The projection to use when querying the call log table */
287    private static final String[] sCallLogProjection = new String[] {
288            Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
289            Calls.CACHED_NUMBER_LABEL
290    };
291    private static final int NUMBER_COLUMN_INDEX = 0;
292    private static final int DATE_COLUMN_INDEX = 1;
293    private static final int CALL_TYPE_COLUMN_INDEX = 2;
294    private static final int CALLER_NAME_COLUMN_INDEX = 3;
295    private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
296    private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
297
298    private static final String FLAG_TIMEZONE_UTC = "Z";
299
300    public VCardComposer(Context context) {
301        this(context, VCardConfig.VCARD_TYPE_DEFAULT, true);
302    }
303
304    public VCardComposer(Context context, int vcardType) {
305        this(context, vcardType, true);
306    }
307
308    public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) {
309        this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors);
310    }
311
312    /**
313     * Construct for supporting call log entry vCard composing.
314     */
315    public VCardComposer(final Context context, final int vcardType,
316            final boolean careHandlerErrors) {
317        mContext = context;
318        mVCardType = vcardType;
319        mCareHandlerErrors = careHandlerErrors;
320        mContentResolver = context.getContentResolver();
321
322        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
323        mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
324        mHandlerList = new ArrayList<OneEntryHandler>();
325
326        if (mIsDoCoMo) {
327            String charset;
328            try {
329                charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
330            } catch (UnsupportedCharsetException e) {
331                Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
332                charset = SHIFT_JIS;
333            }
334            mCharsetString = charset;
335        } else if (mUsesShiftJis) {
336            String charset;
337            try {
338                charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
339            } catch (UnsupportedCharsetException e) {
340                Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
341                charset = SHIFT_JIS;
342            }
343            mCharsetString = charset;
344        } else {
345            mCharsetString = UTF_8;
346        }
347    }
348
349    /**
350     * Must be called before {@link #init()}.
351     */
352    public void addHandler(OneEntryHandler handler) {
353        if (handler != null) {
354            mHandlerList.add(handler);
355        }
356    }
357
358    /**
359     * @return Returns true when initialization is successful and all the other
360     *          methods are available. Returns false otherwise.
361     */
362    public boolean init() {
363        return init(null, null);
364    }
365
366    public boolean init(final String selection, final String[] selectionArgs) {
367        return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
368    }
369
370    /**
371     * Note that this is unstable interface, may be deleted in the future.
372     */
373    public boolean init(final Uri contentUri, final String selection,
374            final String[] selectionArgs, final String sortOrder) {
375        if (contentUri == null) {
376            return false;
377        }
378        if (mCareHandlerErrors) {
379            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
380                    mHandlerList.size());
381            for (OneEntryHandler handler : mHandlerList) {
382                if (!handler.onInit(mContext)) {
383                    for (OneEntryHandler finished : finishedList) {
384                        finished.onTerminate();
385                    }
386                    return false;
387                }
388            }
389        } else {
390            // Just ignore the false returned from onInit().
391            for (OneEntryHandler handler : mHandlerList) {
392                handler.onInit(mContext);
393            }
394        }
395
396        final String[] projection;
397        if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
398            projection = sCallLogProjection;
399            mIsCallLogComposer = true;
400        } else if (Contacts.CONTENT_URI.equals(contentUri) ||
401                CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
402            projection = sContactsProjection;
403        } else {
404            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
405            return false;
406        }
407        mCursor = mContentResolver.query(
408                contentUri, projection, selection, selectionArgs, sortOrder);
409
410        if (mCursor == null) {
411            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
412            return false;
413        }
414
415        if (getCount() == 0 || !mCursor.moveToFirst()) {
416            try {
417                mCursor.close();
418            } catch (SQLiteException e) {
419                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
420            } finally {
421                mCursor = null;
422                mErrorReason = FAILURE_REASON_NO_ENTRY;
423            }
424            return false;
425        }
426
427        if (mIsCallLogComposer) {
428            mIdColumn = -1;
429        } else {
430            mIdColumn = mCursor.getColumnIndex(Contacts._ID);
431        }
432
433        return true;
434    }
435
436    public boolean createOneEntry() {
437        if (mCursor == null || mCursor.isAfterLast()) {
438            mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
439            return false;
440        }
441        String name = null;
442        String vcard;
443        try {
444            if (mIsCallLogComposer) {
445                vcard = createOneCallLogEntryInternal();
446            } else {
447                if (mIdColumn >= 0) {
448                    vcard = createOneEntryInternal(mCursor.getString(mIdColumn));
449                } else {
450                    Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
451                    return true;
452                }
453            }
454        } catch (OutOfMemoryError error) {
455            // Maybe some data (e.g. photo) is too big to have in memory. But it
456            // should be rare.
457            Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: " + name);
458            System.gc();
459            // TODO: should tell users what happened?
460            return true;
461        } finally {
462            mCursor.moveToNext();
463        }
464
465        // This function does not care the OutOfMemoryError on the handler side
466        // :-P
467        if (mCareHandlerErrors) {
468            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
469                    mHandlerList.size());
470            for (OneEntryHandler handler : mHandlerList) {
471                if (!handler.onEntryCreated(vcard)) {
472                    return false;
473                }
474            }
475        } else {
476            for (OneEntryHandler handler : mHandlerList) {
477                handler.onEntryCreated(vcard);
478            }
479        }
480
481        return true;
482    }
483
484    private String createOneEntryInternal(final String contactId) {
485        final Map<String, List<ContentValues>> contentValuesListMap =
486                new HashMap<String, List<ContentValues>>();
487        final String selection = Data.CONTACT_ID + "=?";
488        final String[] selectionArgs = new String[] {contactId};
489        // The resolver may return the entity iterator with no data. It is possiible.
490        // e.g. If all the data in the contact of the given contact id are not exportable ones,
491        //      they are hidden from the view of this method, though contact id itself exists.
492        boolean dataExists = false;
493        EntityIterator entityIterator = null;
494        try {
495            entityIterator = mContentResolver.queryEntities(
496                    sDataRequestUri, selection, selectionArgs, null);
497            dataExists = entityIterator.hasNext();
498            while (entityIterator.hasNext()) {
499                Entity entity = entityIterator.next();
500                for (NamedContentValues namedContentValues : entity.getSubValues()) {
501                    ContentValues contentValues = namedContentValues.values;
502                    String key = contentValues.getAsString(Data.MIMETYPE);
503                    if (key != null) {
504                        List<ContentValues> contentValuesList =
505                                contentValuesListMap.get(key);
506                        if (contentValuesList == null) {
507                            contentValuesList = new ArrayList<ContentValues>();
508                            contentValuesListMap.put(key, contentValuesList);
509                        }
510                        contentValuesList.add(contentValues);
511                    }
512                }
513            }
514        } catch (RemoteException e) {
515            Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)",
516                    contactId, e.getMessage()));
517            return "";
518        } finally {
519            if (entityIterator != null) {
520                entityIterator.close();
521            }
522        }
523
524        if (!dataExists) {
525            return "";
526        }
527
528        final VCardBuilder builder = new VCardBuilder(mVCardType);
529        builder.appendNameProperties(contentValuesListMap.get(StructuredName.CONTENT_ITEM_TYPE))
530                .appendNickNames(contentValuesListMap.get(Nickname.CONTENT_ITEM_TYPE))
531                .appendPhones(contentValuesListMap.get(Phone.CONTENT_ITEM_TYPE))
532                .appendEmails(contentValuesListMap.get(Email.CONTENT_ITEM_TYPE))
533                .appendPostals(contentValuesListMap.get(StructuredPostal.CONTENT_ITEM_TYPE))
534                .appendOrganizations(contentValuesListMap.get(Organization.CONTENT_ITEM_TYPE))
535                .appendWebsites(contentValuesListMap.get(Website.CONTENT_ITEM_TYPE))
536                .appendPhotos(contentValuesListMap.get(Photo.CONTENT_ITEM_TYPE))
537                .appendNotes(contentValuesListMap.get(Note.CONTENT_ITEM_TYPE))
538                .appendEvents(contentValuesListMap.get(Event.CONTENT_ITEM_TYPE))
539                .appendIms(contentValuesListMap.get(Im.CONTENT_ITEM_TYPE))
540                .appendRelation(contentValuesListMap.get(Relation.CONTENT_ITEM_TYPE));
541        return builder.toString();
542    }
543
544    public void terminate() {
545        for (OneEntryHandler handler : mHandlerList) {
546            handler.onTerminate();
547        }
548
549        if (mCursor != null) {
550            try {
551                mCursor.close();
552            } catch (SQLiteException e) {
553                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
554            }
555            mCursor = null;
556        }
557
558        mTerminateIsCalled = true;
559    }
560
561    @Override
562    public void finalize() {
563        if (!mTerminateIsCalled) {
564            terminate();
565        }
566    }
567
568    public int getCount() {
569        if (mCursor == null) {
570            return 0;
571        }
572        return mCursor.getCount();
573    }
574
575    public boolean isAfterLast() {
576        if (mCursor == null) {
577            return false;
578        }
579        return mCursor.isAfterLast();
580    }
581
582    /**
583     * @return Return the error reason if possible.
584     */
585    public String getErrorReason() {
586        return mErrorReason;
587    }
588
589    /**
590     * This static function is to compose vCard for phone own number
591     */
592    public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
593            String phoneNumber, boolean vcardVer21) {
594        final int vcardType = (vcardVer21 ?
595                VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8 :
596                    VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8);
597        final VCardBuilder builder = new VCardBuilder(vcardType);
598        boolean needCharset = false;
599        if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
600            needCharset = true;
601        }
602        builder.appendLine(VCardConstants.PROPERTY_FN, phoneName, needCharset, false);
603        builder.appendLine(VCardConstants.PROPERTY_N, phoneName, needCharset, false);
604
605        if (!TextUtils.isEmpty(phoneNumber)) {
606            String label = Integer.toString(phonetype);
607            builder.appendTelLine(phonetype, label, phoneNumber, false);
608        }
609
610        return builder.toString();
611    }
612
613    /**
614     * Format according to RFC 2445 DATETIME type.
615     * The format is: ("%Y%m%dT%H%M%SZ").
616     */
617    private final String toRfc2455Format(final long millSecs) {
618        Time startDate = new Time();
619        startDate.set(millSecs);
620        String date = startDate.format2445();
621        return date + FLAG_TIMEZONE_UTC;
622    }
623
624    /**
625     * Try to append the property line for a call history time stamp field if possible.
626     * Do nothing if the call log type gotton from the database is invalid.
627     */
628    private void tryAppendCallHistoryTimeStampField(final VCardBuilder builder) {
629        // Extension for call history as defined in
630        // in the Specification for Ic Mobile Communcation - ver 1.1,
631        // Oct 2000. This is used to send the details of the call
632        // history - missed, incoming, outgoing along with date and time
633        // to the requesting device (For example, transferring phone book
634        // when connected over bluetooth)
635        //
636        // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
637        final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
638        final String callLogTypeStr;
639        switch (callLogType) {
640            case Calls.INCOMING_TYPE: {
641                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
642                break;
643            }
644            case Calls.OUTGOING_TYPE: {
645                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
646                break;
647            }
648            case Calls.MISSED_TYPE: {
649                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
650                break;
651            }
652            default: {
653                Log.w(LOG_TAG, "Call log type not correct.");
654                return;
655            }
656        }
657
658        final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
659        builder.appendLine(VCARD_PROPERTY_X_TIMESTAMP,
660                Arrays.asList(callLogTypeStr), toRfc2455Format(dateAsLong));
661    }
662
663    private String createOneCallLogEntryInternal() {
664        final VCardBuilder builder = new VCardBuilder(VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8);
665        String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
666        if (TextUtils.isEmpty(name)) {
667            name = mCursor.getString(NUMBER_COLUMN_INDEX);
668        }
669        final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
670        builder.appendLine(VCardConstants.PROPERTY_FN, name, needCharset, false);
671        builder.appendLine(VCardConstants.PROPERTY_N, name, needCharset, false);
672
673        final String number = mCursor.getString(NUMBER_COLUMN_INDEX);
674        final int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
675        String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
676        if (TextUtils.isEmpty(label)) {
677            label = Integer.toString(type);
678        }
679        builder.appendTelLine(type, label, number, false);
680        tryAppendCallHistoryTimeStampField(builder);
681        return builder.toString();
682    }
683}
684