VCardComposer.java revision 592988d307e8d305ca163c4e58da0fb350054194
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.StructuredName;
42import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
43import android.provider.ContactsContract.CommonDataKinds.Website;
44import android.telephony.PhoneNumberUtils;
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 = "vcard.VCardComposer";
104
105    // TODO: Should be configurable?
106    public static final int DEFAULT_PHONE_TYPE = Phone.TYPE_HOME;
107    public static final int DEFAULT_POSTAL_TYPE = StructuredPostal.TYPE_HOME;
108    public static final int DEFAULT_EMAIL_TYPE = Email.TYPE_OTHER;
109
110    public static final String FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO =
111        "Failed to get database information";
112
113    public static final String FAILURE_REASON_NO_ENTRY =
114        "There's no exportable in the database";
115
116    public static final String FAILURE_REASON_NOT_INITIALIZED =
117        "The vCard composer object is not correctly initialized";
118
119    /** Should be visible only from developers... (no need to translate, hopefully) */
120    public static final String FAILURE_REASON_UNSUPPORTED_URI =
121        "The Uri vCard composer received is not supported by the composer.";
122
123    public static final String NO_ERROR = "No error";
124
125    public static final String VCARD_TYPE_STRING_DOCOMO = "docomo";
126
127    // Property for call log entry
128    private static final String VCARD_PROPERTY_X_TIMESTAMP = "X-IRMC-CALL-DATETIME";
129    private static final String VCARD_PROPERTY_CALLTYPE_INCOMING = "INCOMING";
130    private static final String VCARD_PROPERTY_CALLTYPE_OUTGOING = "OUTGOING";
131    private static final String VCARD_PROPERTY_CALLTYPE_MISSED = "MISSED";
132
133    private static final String VCARD_DATA_VCARD = "VCARD";
134    private static final String VCARD_DATA_PUBLIC = "PUBLIC";
135
136    private static final String VCARD_PARAM_SEPARATOR = ";";
137    private static final String VCARD_END_OF_LINE = "\r\n";
138    private static final String VCARD_DATA_SEPARATOR = ":";
139    private static final String VCARD_ITEM_SEPARATOR = ";";
140    private static final String VCARD_WS = " ";
141    private static final String VCARD_PARAM_EQUAL = "=";
142
143    private static final String VCARD_PARAM_ENCODING_QP = "ENCODING=QUOTED-PRINTABLE";
144
145    private static final String VCARD_PARAM_ENCODING_BASE64_V21 = "ENCODING=BASE64";
146    private static final String VCARD_PARAM_ENCODING_BASE64_V30 = "ENCODING=b";
147
148    private static final String SHIFT_JIS = "SHIFT_JIS";
149    private static final String UTF_8 = "UTF-8";
150
151    /**
152     * Special URI for testing.
153     */
154    public static final String VCARD_TEST_AUTHORITY = "com.android.unit_tests.vcard";
155    public static final Uri VCARD_TEST_AUTHORITY_URI =
156        Uri.parse("content://" + VCARD_TEST_AUTHORITY);
157    public static final Uri CONTACTS_TEST_CONTENT_URI =
158        Uri.withAppendedPath(VCARD_TEST_AUTHORITY_URI, "contacts");
159
160    private static final Uri sDataRequestUri;
161    private static final Map<Integer, String> sImMap;
162
163    /**
164     * See the comment in {@link VCardConfig#FLAG_REFRAIN_QP_TO_PRIMARY_PROPERTIES}.
165     */
166    private static final Set<String> sPrimaryPropertyNameSet;
167
168    static {
169        Uri.Builder builder = RawContacts.CONTENT_URI.buildUpon();
170        builder.appendQueryParameter(Data.FOR_EXPORT_ONLY, "1");
171        sDataRequestUri = builder.build();
172        sImMap = new HashMap<Integer, String>();
173        sImMap.put(Im.PROTOCOL_AIM, Constants.PROPERTY_X_AIM);
174        sImMap.put(Im.PROTOCOL_MSN, Constants.PROPERTY_X_MSN);
175        sImMap.put(Im.PROTOCOL_YAHOO, Constants.PROPERTY_X_YAHOO);
176        sImMap.put(Im.PROTOCOL_ICQ, Constants.PROPERTY_X_ICQ);
177        sImMap.put(Im.PROTOCOL_JABBER, Constants.PROPERTY_X_JABBER);
178        sImMap.put(Im.PROTOCOL_SKYPE, Constants.PROPERTY_X_SKYPE_USERNAME);
179        // Google talk is a special case.
180
181        // TODO: incomplete. Implement properly
182        sPrimaryPropertyNameSet = new HashSet<String>();
183        sPrimaryPropertyNameSet.add(Constants.PROPERTY_N);
184        sPrimaryPropertyNameSet.add(Constants.PROPERTY_FN);
185        sPrimaryPropertyNameSet.add(Constants.PROPERTY_SOUND);
186    }
187
188    public static interface OneEntryHandler {
189        public boolean onInit(Context context);
190        public boolean onEntryCreated(String vcard);
191        public void onTerminate();
192    }
193
194    /**
195     * <p>
196     * An useful example handler, which emits VCard String to outputstream one by one.
197     * </p>
198     * <p>
199     * The input OutputStream object is closed() on {{@link #onTerminate()}.
200     * Must not close the stream outside.
201     * </p>
202     */
203    public class HandlerForOutputStream implements OneEntryHandler {
204        @SuppressWarnings("hiding")
205        private static final String LOG_TAG = "vcard.VCardComposer.HandlerForOutputStream";
206
207        final private OutputStream mOutputStream; // mWriter will close this.
208        private Writer mWriter;
209
210        private boolean mOnTerminateIsCalled = false;
211
212        /**
213         * Input stream will be closed on the detruction of this object.
214         */
215        public HandlerForOutputStream(OutputStream outputStream) {
216            mOutputStream = outputStream;
217        }
218
219        public boolean onInit(Context context) {
220            try {
221                mWriter = new BufferedWriter(new OutputStreamWriter(
222                        mOutputStream, mCharsetString));
223            } catch (UnsupportedEncodingException e1) {
224                Log.e(LOG_TAG, "Unsupported charset: " + mCharsetString);
225                mErrorReason = "Encoding is not supported (usually this does not happen!): "
226                        + mCharsetString;
227                return false;
228            }
229
230            if (mIsDoCoMo) {
231                try {
232                    // Create one empty entry.
233                    mWriter.write(createOneEntryInternal("-1"));
234                } catch (IOException e) {
235                    Log.e(LOG_TAG,
236                            "IOException occurred during exportOneContactData: "
237                                    + e.getMessage());
238                    mErrorReason = "IOException occurred: " + e.getMessage();
239                    return false;
240                }
241            }
242            return true;
243        }
244
245        public boolean onEntryCreated(String vcard) {
246            try {
247                mWriter.write(vcard);
248            } catch (IOException e) {
249                Log.e(LOG_TAG,
250                        "IOException occurred during exportOneContactData: "
251                                + e.getMessage());
252                mErrorReason = "IOException occurred: " + e.getMessage();
253                return false;
254            }
255            return true;
256        }
257
258        public void onTerminate() {
259            mOnTerminateIsCalled = true;
260            if (mWriter != null) {
261                try {
262                    // Flush and sync the data so that a user is able to pull
263                    // the SDCard just after
264                    // the export.
265                    mWriter.flush();
266                    if (mOutputStream != null
267                            && mOutputStream instanceof FileOutputStream) {
268                            ((FileOutputStream) mOutputStream).getFD().sync();
269                    }
270                } catch (IOException e) {
271                    Log.d(LOG_TAG,
272                            "IOException during closing the output stream: "
273                                    + e.getMessage());
274                } finally {
275                    try {
276                        mWriter.close();
277                    } catch (IOException e) {
278                    }
279                }
280            }
281        }
282
283        @Override
284        public void finalize() {
285            if (!mOnTerminateIsCalled) {
286                onTerminate();
287            }
288        }
289    }
290
291    private final Context mContext;
292    private final int mVCardType;
293    private final boolean mCareHandlerErrors;
294    private final ContentResolver mContentResolver;
295
296    // Convenient member variables about the restriction of the vCard format.
297    // Used for not calling the same methods returning same results.
298    private final boolean mIsV30;
299    private final boolean mIsJapaneseMobilePhone;
300    private final boolean mOnlyOneNoteFieldIsAvailable;
301    private final boolean mIsDoCoMo;
302    private final boolean mUsesQuotedPrintable;
303    private final boolean mUsesAndroidProperty;
304    private final boolean mUsesDefactProperty;
305    private final boolean mUsesUtf8;
306    private final boolean mUsesShiftJis;
307    private final boolean mAppendTypeParamName;
308    private final boolean mRefrainsQPToPrimaryProperties;
309    private final boolean mNeedsToConvertPhoneticString;
310
311    private Cursor mCursor;
312    private int mIdColumn;
313
314    private final String mCharsetString;
315    private final String mVCardCharsetParameter;
316    private boolean mTerminateIsCalled;
317    final private List<OneEntryHandler> mHandlerList;
318
319    private String mErrorReason = NO_ERROR;
320
321    private boolean mIsCallLogComposer;
322
323    private static final String[] sContactsProjection = new String[] {
324        Contacts._ID,
325    };
326
327    /** The projection to use when querying the call log table */
328    private static final String[] sCallLogProjection = new String[] {
329            Calls.NUMBER, Calls.DATE, Calls.TYPE, Calls.CACHED_NAME, Calls.CACHED_NUMBER_TYPE,
330            Calls.CACHED_NUMBER_LABEL
331    };
332    private static final int NUMBER_COLUMN_INDEX = 0;
333    private static final int DATE_COLUMN_INDEX = 1;
334    private static final int CALL_TYPE_COLUMN_INDEX = 2;
335    private static final int CALLER_NAME_COLUMN_INDEX = 3;
336    private static final int CALLER_NUMBERTYPE_COLUMN_INDEX = 4;
337    private static final int CALLER_NUMBERLABEL_COLUMN_INDEX = 5;
338
339    private static final String FLAG_TIMEZONE_UTC = "Z";
340
341    public VCardComposer(Context context) {
342        this(context, VCardConfig.VCARD_TYPE_DEFAULT, true);
343    }
344
345    public VCardComposer(Context context, int vcardType) {
346        this(context, vcardType, true);
347    }
348
349    public VCardComposer(Context context, String vcardTypeStr, boolean careHandlerErrors) {
350        this(context, VCardConfig.getVCardTypeFromString(vcardTypeStr), careHandlerErrors);
351    }
352
353    /**
354     * Construct for supporting call log entry vCard composing.
355     */
356    public VCardComposer(Context context, int vcardType, boolean careHandlerErrors) {
357        mContext = context;
358        mVCardType = vcardType;
359        mCareHandlerErrors = careHandlerErrors;
360        mContentResolver = context.getContentResolver();
361
362        mIsV30 = VCardConfig.isV30(vcardType);
363        mUsesQuotedPrintable = VCardConfig.usesQuotedPrintable(vcardType);
364        mIsDoCoMo = VCardConfig.isDoCoMo(vcardType);
365        mIsJapaneseMobilePhone = VCardConfig.needsToConvertPhoneticString(vcardType);
366        mOnlyOneNoteFieldIsAvailable = VCardConfig.onlyOneNoteFieldIsAvailable(vcardType);
367        mUsesAndroidProperty = VCardConfig.usesAndroidSpecificProperty(vcardType);
368        mUsesDefactProperty = VCardConfig.usesDefactProperty(vcardType);
369        mUsesUtf8 = VCardConfig.usesUtf8(vcardType);
370        mUsesShiftJis = VCardConfig.usesShiftJis(vcardType);
371        mRefrainsQPToPrimaryProperties = VCardConfig.refrainsQPToPrimaryProperties(vcardType);
372        mAppendTypeParamName = VCardConfig.appendTypeParamName(vcardType);
373        mNeedsToConvertPhoneticString = VCardConfig.needsToConvertPhoneticString(vcardType);
374        mHandlerList = new ArrayList<OneEntryHandler>();
375
376        if (mIsDoCoMo) {
377            String charset;
378            try {
379                charset = CharsetUtils.charsetForVendor(SHIFT_JIS, "docomo").name();
380            } catch (UnsupportedCharsetException e) {
381                Log.e(LOG_TAG, "DoCoMo-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
382                charset = SHIFT_JIS;
383            }
384            mCharsetString = charset;
385            // Do not use mCharsetString bellow since it is different from "SHIFT_JIS" but
386            // may be "DOCOMO_SHIFT_JIS" or something like that (internal expression used in
387            // Android, not shown to the public).
388            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
389        } else if (mUsesShiftJis) {
390            String charset;
391            try {
392                charset = CharsetUtils.charsetForVendor(SHIFT_JIS).name();
393            } catch (UnsupportedCharsetException e) {
394                Log.e(LOG_TAG, "Vendor-specific SHIFT_JIS was not found. Use SHIFT_JIS as is.");
395                charset = SHIFT_JIS;
396            }
397            mCharsetString = charset;
398            mVCardCharsetParameter = "CHARSET=" + SHIFT_JIS;
399        } else {
400            mCharsetString = UTF_8;
401            mVCardCharsetParameter = "CHARSET=" + UTF_8;
402        }
403    }
404
405    /**
406     * Must call before {{@link #init()}.
407     */
408    public void addHandler(OneEntryHandler handler) {
409        if (handler != null) {
410            mHandlerList.add(handler);
411        }
412    }
413
414    /**
415     * @return Returns true when initialization is successful and all the other
416     *          methods are available. Returns false otherwise.
417     */
418    public boolean init() {
419        return init(null, null);
420    }
421
422    public boolean init(final String selection, final String[] selectionArgs) {
423        return init(Contacts.CONTENT_URI, selection, selectionArgs, null);
424    }
425
426    /**
427     * Note that this is unstable interface, may be deleted in the future.
428     */
429    public boolean init(final Uri contentUri, final String selection,
430            final String[] selectionArgs, final String sortOrder) {
431        if (contentUri == null) {
432            return false;
433        }
434        if (mCareHandlerErrors) {
435            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
436                    mHandlerList.size());
437            for (OneEntryHandler handler : mHandlerList) {
438                if (!handler.onInit(mContext)) {
439                    for (OneEntryHandler finished : finishedList) {
440                        finished.onTerminate();
441                    }
442                    return false;
443                }
444            }
445        } else {
446            // Just ignore the false returned from onInit().
447            for (OneEntryHandler handler : mHandlerList) {
448                handler.onInit(mContext);
449            }
450        }
451
452        final String[] projection;
453        if (CallLog.Calls.CONTENT_URI.equals(contentUri)) {
454            projection = sCallLogProjection;
455            mIsCallLogComposer = true;
456        } else if (Contacts.CONTENT_URI.equals(contentUri) ||
457                CONTACTS_TEST_CONTENT_URI.equals(contentUri)) {
458            projection = sContactsProjection;
459        } else {
460            mErrorReason = FAILURE_REASON_UNSUPPORTED_URI;
461            return false;
462        }
463        mCursor = mContentResolver.query(
464                contentUri, projection, selection, selectionArgs, sortOrder);
465
466        if (mCursor == null) {
467            mErrorReason = FAILURE_REASON_FAILED_TO_GET_DATABASE_INFO;
468            return false;
469        }
470
471        if (getCount() == 0 || !mCursor.moveToFirst()) {
472            try {
473                mCursor.close();
474            } catch (SQLiteException e) {
475                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): " + e.getMessage());
476            } finally {
477                mCursor = null;
478                mErrorReason = FAILURE_REASON_NO_ENTRY;
479            }
480            return false;
481        }
482
483        if (mIsCallLogComposer) {
484            mIdColumn = -1;
485        } else {
486            mIdColumn = mCursor.getColumnIndex(Contacts._ID);
487        }
488
489        return true;
490    }
491
492    public boolean createOneEntry() {
493        if (mCursor == null || mCursor.isAfterLast()) {
494            mErrorReason = FAILURE_REASON_NOT_INITIALIZED;
495            return false;
496        }
497        String name = null;
498        String vcard;
499        try {
500            if (mIsCallLogComposer) {
501                vcard = createOneCallLogEntryInternal();
502            } else {
503                if (mIdColumn >= 0) {
504                    vcard = createOneEntryInternal(mCursor.getString(mIdColumn));
505                } else {
506                    Log.e(LOG_TAG, "Incorrect mIdColumn: " + mIdColumn);
507                    return true;
508                }
509            }
510        } catch (OutOfMemoryError error) {
511            // Maybe some data (e.g. photo) is too big to have in memory. But it
512            // should be rare.
513            Log.e(LOG_TAG, "OutOfMemoryError occured. Ignore the entry: "
514                    + name);
515            System.gc();
516            // TODO: should tell users what happened?
517            return true;
518        } finally {
519            mCursor.moveToNext();
520        }
521
522        // This function does not care the OutOfMemoryError on the handler side
523        // :-P
524        if (mCareHandlerErrors) {
525            List<OneEntryHandler> finishedList = new ArrayList<OneEntryHandler>(
526                    mHandlerList.size());
527            for (OneEntryHandler handler : mHandlerList) {
528                if (!handler.onEntryCreated(vcard)) {
529                    return false;
530                }
531            }
532        } else {
533            for (OneEntryHandler handler : mHandlerList) {
534                handler.onEntryCreated(vcard);
535            }
536        }
537
538        return true;
539    }
540
541    private String createOneEntryInternal(final String contactId) {
542        final Map<String, List<ContentValues>> contentValuesListMap =
543                new HashMap<String, List<ContentValues>>();
544        final String selection = Data.CONTACT_ID + "=?";
545        final String[] selectionArgs = new String[] {contactId};
546        // The resolver may return the entity iterator with no data. It is possiible.
547        // e.g. If all the data in the contact of the given contact id are not exportable ones,
548        //      they are hidden from the view of this method, though contact id itself exists.
549        boolean dataExists = false;
550        EntityIterator entityIterator = null;
551        try {
552            entityIterator = mContentResolver.queryEntities(
553                    sDataRequestUri, selection, selectionArgs, null);
554            dataExists = entityIterator.hasNext();
555            while (entityIterator.hasNext()) {
556                Entity entity = entityIterator.next();
557                for (NamedContentValues namedContentValues : entity.getSubValues()) {
558                    ContentValues contentValues = namedContentValues.values;
559                    String key = contentValues.getAsString(Data.MIMETYPE);
560                    if (key != null) {
561                        List<ContentValues> contentValuesList =
562                                contentValuesListMap.get(key);
563                        if (contentValuesList == null) {
564                            contentValuesList = new ArrayList<ContentValues>();
565                            contentValuesListMap.put(key, contentValuesList);
566                        }
567                        contentValuesList.add(contentValues);
568                    }
569                }
570            }
571        } catch (RemoteException e) {
572            Log.e(LOG_TAG, String.format("RemoteException at id %s (%s)",
573                    contactId, e.getMessage()));
574            return "";
575        } finally {
576            if (entityIterator != null) {
577                entityIterator.close();
578            }
579        }
580
581        if (!dataExists) {
582            return "";
583        }
584
585        final StringBuilder builder = new StringBuilder();
586        appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
587        if (mIsV30) {
588            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30);
589        } else {
590            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21);
591        }
592
593        appendStructuredNames(builder, contentValuesListMap);
594        appendNickNames(builder, contentValuesListMap);
595        appendPhones(builder, contentValuesListMap);
596        appendEmails(builder, contentValuesListMap);
597        appendPostals(builder, contentValuesListMap);
598        appendIms(builder, contentValuesListMap);
599        appendWebsites(builder, contentValuesListMap);
600        appendBirthday(builder, contentValuesListMap);
601        appendOrganizations(builder, contentValuesListMap);
602        appendPhotos(builder, contentValuesListMap);
603        appendNotes(builder, contentValuesListMap);
604        // TODO: GroupMembership, Relation, Event other than birthday.
605
606        if (mIsDoCoMo) {
607            appendVCardLine(builder, Constants.PROPERTY_X_CLASS, VCARD_DATA_PUBLIC);
608            appendVCardLine(builder, Constants.PROPERTY_X_REDUCTION, "");
609            appendVCardLine(builder, Constants.PROPERTY_X_NO, "");
610            appendVCardLine(builder, Constants.PROPERTY_X_DCM_HMN_MODE, "");
611        }
612
613        appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD);
614
615        return builder.toString();
616    }
617
618    public void terminate() {
619        for (OneEntryHandler handler : mHandlerList) {
620            handler.onTerminate();
621        }
622
623        if (mCursor != null) {
624            try {
625                mCursor.close();
626            } catch (SQLiteException e) {
627                Log.e(LOG_TAG, "SQLiteException on Cursor#close(): "
628                        + e.getMessage());
629            }
630            mCursor = null;
631        }
632
633        mTerminateIsCalled = true;
634    }
635
636    @Override
637    public void finalize() {
638        if (!mTerminateIsCalled) {
639            terminate();
640        }
641    }
642
643    public int getCount() {
644        if (mCursor == null) {
645            return 0;
646        }
647        return mCursor.getCount();
648    }
649
650    public boolean isAfterLast() {
651        if (mCursor == null) {
652            return false;
653        }
654        return mCursor.isAfterLast();
655    }
656
657    /**
658     * @return Return the error reason if possible.
659     */
660    public String getErrorReason() {
661        return mErrorReason;
662    }
663
664    private void appendStructuredNames(final StringBuilder builder,
665            final Map<String, List<ContentValues>> contentValuesListMap) {
666        final List<ContentValues> contentValuesList = contentValuesListMap
667                .get(StructuredName.CONTENT_ITEM_TYPE);
668        if (contentValuesList != null && contentValuesList.size() > 0) {
669            appendStructuredNamesInternal(builder, contentValuesList);
670        } else if (mIsDoCoMo) {
671            appendVCardLine(builder, Constants.PROPERTY_N, "");
672        } else if (mIsV30) {
673            // vCard 3.0 requires "N" and "FN" properties.
674            appendVCardLine(builder, Constants.PROPERTY_N, "");
675            appendVCardLine(builder, Constants.PROPERTY_FN, "");
676        }
677    }
678
679    private boolean containsNonEmptyName(ContentValues contentValues) {
680        final String familyName = contentValues.getAsString(StructuredName.FAMILY_NAME);
681        final String middleName = contentValues.getAsString(StructuredName.MIDDLE_NAME);
682        final String givenName = contentValues.getAsString(StructuredName.GIVEN_NAME);
683        final String prefix = contentValues.getAsString(StructuredName.PREFIX);
684        final String suffix = contentValues.getAsString(StructuredName.SUFFIX);
685        final String phoneticFamilyName =
686                contentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
687        final String phoneticMiddleName =
688                contentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
689        final String phoneticGivenName =
690                contentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
691        final String displayName = contentValues.getAsString(StructuredName.DISPLAY_NAME);
692        return !(TextUtils.isEmpty(familyName) && TextUtils.isEmpty(middleName) &&
693                TextUtils.isEmpty(givenName) && TextUtils.isEmpty(prefix) &&
694                TextUtils.isEmpty(suffix) && TextUtils.isEmpty(phoneticFamilyName) &&
695                TextUtils.isEmpty(phoneticMiddleName) && TextUtils.isEmpty(phoneticGivenName) &&
696                TextUtils.isEmpty(displayName));
697    }
698
699    private void appendStructuredNamesInternal(final StringBuilder builder,
700            final List<ContentValues> contentValuesList) {
701        // For safety, we'll emit just one value around StructuredName, as external importers
702        // may get confused with multiple "N", "FN", etc. properties, though it is valid in
703        // vCard spec.
704        ContentValues primaryContentValues = null;
705        ContentValues subprimaryContentValues = null;
706        for (ContentValues contentValues : contentValuesList) {
707            if (contentValues == null){
708                continue;
709            }
710            Integer isSuperPrimary = contentValues.getAsInteger(StructuredName.IS_SUPER_PRIMARY);
711            if (isSuperPrimary != null && isSuperPrimary > 0) {
712                // We choose "super primary" ContentValues.
713                primaryContentValues = contentValues;
714                break;
715            } else if (primaryContentValues == null) {
716                // We choose the first "primary" ContentValues
717                // if "super primary" ContentValues does not exist.
718                Integer isPrimary = contentValues.getAsInteger(StructuredName.IS_PRIMARY);
719                if (isPrimary != null && isPrimary > 0 &&
720                        containsNonEmptyName(contentValues)) {
721                    primaryContentValues = contentValues;
722                    // Do not break, since there may be ContentValues with "super primary"
723                    // afterword.
724                } else if (subprimaryContentValues == null &&
725                        containsNonEmptyName(contentValues)) {
726                    subprimaryContentValues = contentValues;
727                }
728            }
729        }
730
731        if (primaryContentValues == null) {
732            if (subprimaryContentValues != null) {
733                // We choose the first ContentValues if any "primary" ContentValues does not exist.
734                primaryContentValues = subprimaryContentValues;
735            } else {
736                Log.e(LOG_TAG, "All ContentValues given from database is empty.");
737                primaryContentValues = new ContentValues();
738            }
739        }
740
741        final String familyName = primaryContentValues.getAsString(StructuredName.FAMILY_NAME);
742        final String middleName = primaryContentValues.getAsString(StructuredName.MIDDLE_NAME);
743        final String givenName = primaryContentValues.getAsString(StructuredName.GIVEN_NAME);
744        final String prefix = primaryContentValues.getAsString(StructuredName.PREFIX);
745        final String suffix = primaryContentValues.getAsString(StructuredName.SUFFIX);
746        final String displayName = primaryContentValues.getAsString(StructuredName.DISPLAY_NAME);
747
748        if (!TextUtils.isEmpty(familyName) || !TextUtils.isEmpty(givenName)) {
749            final boolean shouldAppendCharsetParameterToName =
750                !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) &&
751                shouldAppendCharsetParameters(Arrays.asList(
752                        familyName, givenName, middleName, prefix, suffix));
753            final boolean reallyUseQuotedPrintableToName =
754                    (!mRefrainsQPToPrimaryProperties &&
755                            !(VCardUtils.containsOnlyNonCrLfPrintableAscii(familyName) &&
756                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(givenName) &&
757                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(middleName) &&
758                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(prefix) &&
759                                    VCardUtils.containsOnlyNonCrLfPrintableAscii(suffix)));
760
761            final String formattedName;
762            if (!TextUtils.isEmpty(displayName)) {
763                formattedName = displayName;
764            } else {
765                formattedName = VCardUtils.constructNameFromElements(
766                        VCardConfig.getNameOrderType(mVCardType),
767                        familyName, middleName, givenName, prefix, suffix);
768            }
769            final boolean shouldAppendCharsetParameterToFN =
770                    !(mIsV30 && UTF_8.equalsIgnoreCase(mCharsetString)) &&
771                    shouldAppendCharsetParameter(formattedName);
772            final boolean reallyUseQuotedPrintableToFN =
773                    !mRefrainsQPToPrimaryProperties &&
774                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedName);
775
776            final String encodedFamily;
777            final String encodedGiven;
778            final String encodedMiddle;
779            final String encodedPrefix;
780            final String encodedSuffix;
781            if (reallyUseQuotedPrintableToName) {
782                encodedFamily = encodeQuotedPrintable(familyName);
783                encodedGiven = encodeQuotedPrintable(givenName);
784                encodedMiddle = encodeQuotedPrintable(middleName);
785                encodedPrefix = encodeQuotedPrintable(prefix);
786                encodedSuffix = encodeQuotedPrintable(suffix);
787            } else {
788                encodedFamily = escapeCharacters(familyName);
789                encodedGiven = escapeCharacters(givenName);
790                encodedMiddle = escapeCharacters(middleName);
791                encodedPrefix = escapeCharacters(prefix);
792                encodedSuffix = escapeCharacters(suffix);
793            }
794
795            final String encodedFormattedname =
796                    (reallyUseQuotedPrintableToFN ?
797                            encodeQuotedPrintable(formattedName) : escapeCharacters(formattedName));
798
799            builder.append(Constants.PROPERTY_N);
800            if (mIsDoCoMo) {
801                if (shouldAppendCharsetParameterToName) {
802                    builder.append(VCARD_PARAM_SEPARATOR);
803                    builder.append(mVCardCharsetParameter);
804                }
805                if (reallyUseQuotedPrintableToName) {
806                    builder.append(VCARD_PARAM_SEPARATOR);
807                    builder.append(VCARD_PARAM_ENCODING_QP);
808                }
809                builder.append(VCARD_DATA_SEPARATOR);
810                // DoCoMo phones require that all the elements in the "family name" field.
811                builder.append(formattedName);
812                builder.append(VCARD_ITEM_SEPARATOR);
813                builder.append(VCARD_ITEM_SEPARATOR);
814                builder.append(VCARD_ITEM_SEPARATOR);
815                builder.append(VCARD_ITEM_SEPARATOR);
816            } else {
817                if (shouldAppendCharsetParameterToName) {
818                    builder.append(VCARD_PARAM_SEPARATOR);
819                    builder.append(mVCardCharsetParameter);
820                }
821                if (reallyUseQuotedPrintableToName) {
822                    builder.append(VCARD_PARAM_SEPARATOR);
823                    builder.append(VCARD_PARAM_ENCODING_QP);
824                }
825                builder.append(VCARD_DATA_SEPARATOR);
826                builder.append(encodedFamily);
827                builder.append(VCARD_ITEM_SEPARATOR);
828                builder.append(encodedGiven);
829                builder.append(VCARD_ITEM_SEPARATOR);
830                builder.append(encodedMiddle);
831                builder.append(VCARD_ITEM_SEPARATOR);
832                builder.append(encodedPrefix);
833                builder.append(VCARD_ITEM_SEPARATOR);
834                builder.append(encodedSuffix);
835            }
836            builder.append(VCARD_END_OF_LINE);
837
838            // FN property
839            builder.append(Constants.PROPERTY_FN);
840            if (shouldAppendCharsetParameterToFN) {
841                builder.append(VCARD_PARAM_SEPARATOR);
842                builder.append(mVCardCharsetParameter);
843            }
844            if (reallyUseQuotedPrintableToFN) {
845                builder.append(VCARD_PARAM_SEPARATOR);
846                builder.append(VCARD_PARAM_ENCODING_QP);
847            }
848            builder.append(VCARD_DATA_SEPARATOR);
849            builder.append(encodedFormattedname);
850            builder.append(VCARD_END_OF_LINE);
851        } else if (!TextUtils.isEmpty(displayName)) {
852            final boolean reallyUseQuotedPrintableToDisplayName =
853                (!mRefrainsQPToPrimaryProperties &&
854                        !VCardUtils.containsOnlyNonCrLfPrintableAscii(displayName));
855            final String encodedDisplayName =
856                    reallyUseQuotedPrintableToDisplayName ?
857                            encodeQuotedPrintable(displayName) :
858                                escapeCharacters(displayName);
859
860            builder.append(Constants.PROPERTY_N);
861            if (shouldAppendCharsetParameter(displayName)) {
862                builder.append(VCARD_PARAM_SEPARATOR);
863                builder.append(mVCardCharsetParameter);
864            }
865            if (reallyUseQuotedPrintableToDisplayName) {
866                builder.append(VCARD_PARAM_SEPARATOR);
867                builder.append(VCARD_PARAM_ENCODING_QP);
868            }
869            builder.append(VCARD_DATA_SEPARATOR);
870            builder.append(encodedDisplayName);
871            builder.append(VCARD_ITEM_SEPARATOR);
872            builder.append(VCARD_ITEM_SEPARATOR);
873            builder.append(VCARD_ITEM_SEPARATOR);
874            builder.append(VCARD_ITEM_SEPARATOR);
875            builder.append(VCARD_END_OF_LINE);
876            builder.append(Constants.PROPERTY_FN);
877
878            // Note: "CHARSET" param is not allowed in vCard 3.0, but we may add it
879            //       when it would be useful for external importers, assuming no external
880            //       importer allows this vioration.
881            if (shouldAppendCharsetParameter(displayName)) {
882                builder.append(VCARD_PARAM_SEPARATOR);
883                builder.append(mVCardCharsetParameter);
884            }
885            builder.append(VCARD_DATA_SEPARATOR);
886            builder.append(encodedDisplayName);
887            builder.append(VCARD_END_OF_LINE);
888        } else if (mIsV30) {
889            // vCard 3.0 specification requires these fields.
890            appendVCardLine(builder, Constants.PROPERTY_N, "");
891            appendVCardLine(builder, Constants.PROPERTY_FN, "");
892        } else if (mIsDoCoMo) {
893            appendVCardLine(builder, Constants.PROPERTY_N, "");
894        }
895
896        final String phoneticFamilyName;
897        final String phoneticMiddleName;
898        final String phoneticGivenName;
899        {
900            String tmpPhoneticFamilyName =
901                primaryContentValues.getAsString(StructuredName.PHONETIC_FAMILY_NAME);
902            String tmpPhoneticMiddleName =
903                primaryContentValues.getAsString(StructuredName.PHONETIC_MIDDLE_NAME);
904            String tmpPhoneticGivenName =
905                primaryContentValues.getAsString(StructuredName.PHONETIC_GIVEN_NAME);
906            if (mNeedsToConvertPhoneticString) {
907                phoneticFamilyName = VCardUtils.toHalfWidthString(tmpPhoneticFamilyName);
908                phoneticMiddleName = VCardUtils.toHalfWidthString(tmpPhoneticMiddleName);
909                phoneticGivenName = VCardUtils.toHalfWidthString(tmpPhoneticGivenName);
910            } else {
911                phoneticFamilyName = tmpPhoneticFamilyName;
912                phoneticMiddleName = tmpPhoneticMiddleName;
913                phoneticGivenName = tmpPhoneticGivenName;
914            }
915        }
916        if (!(TextUtils.isEmpty(phoneticFamilyName)
917                && TextUtils.isEmpty(phoneticMiddleName)
918                && TextUtils.isEmpty(phoneticGivenName))) {
919
920            if (mIsV30) {
921                final String sortString = VCardUtils
922                        .constructNameFromElements(mVCardType,
923                                phoneticFamilyName, phoneticMiddleName, phoneticGivenName);
924                builder.append(Constants.PROPERTY_SORT_STRING);
925                if (shouldAppendCharsetParameter(sortString)) {
926                    builder.append(VCARD_PARAM_SEPARATOR);
927                    builder.append(mVCardCharsetParameter);
928                }
929                builder.append(VCARD_DATA_SEPARATOR);
930                builder.append(escapeCharacters(sortString));
931                builder.append(VCARD_END_OF_LINE);
932            } else if (mIsJapaneseMobilePhone) {
933                // Note: There is no appropriate property for expressing
934                //       phonetic name in vCard 2.1, while there is in
935                //       vCard 3.0 (SORT-STRING).
936                //       We chose to use DoCoMo's way when the device is Japanese one
937                //       since it is supported by
938                //       a lot of Japanese mobile phones. This is "X-" property, so
939                //       any parser hopefully would not get confused with this.
940                builder.append(Constants.PROPERTY_SOUND);
941                builder.append(VCARD_PARAM_SEPARATOR);
942                builder.append(Constants.PARAM_TYPE_X_IRMC_N);
943
944                boolean reallyUseQuotedPrintable =
945                    (!mRefrainsQPToPrimaryProperties
946                            && !(VCardUtils.containsOnlyNonCrLfPrintableAscii(
947                                    phoneticFamilyName)
948                                    && VCardUtils.containsOnlyNonCrLfPrintableAscii(
949                                            phoneticMiddleName)
950                                    && VCardUtils.containsOnlyNonCrLfPrintableAscii(
951                                            phoneticGivenName)));
952
953                final String encodedPhoneticFamilyName;
954                final String encodedPhoneticMiddleName;
955                final String encodedPhoneticGivenName;
956                if (reallyUseQuotedPrintable) {
957                    encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
958                    encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
959                    encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
960                } else {
961                    encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
962                    encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
963                    encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
964                }
965
966                if (shouldAppendCharsetParameters(Arrays.asList(
967                        encodedPhoneticFamilyName, encodedPhoneticMiddleName,
968                        encodedPhoneticGivenName))) {
969                    builder.append(VCARD_PARAM_SEPARATOR);
970                    builder.append(mVCardCharsetParameter);
971                }
972                builder.append(VCARD_DATA_SEPARATOR);
973                // DoCoMo's specification requires vCard composer to use just the first
974                // column.
975                {
976                    boolean first = true;
977                    if (!TextUtils.isEmpty(encodedPhoneticFamilyName)) {
978                        builder.append(encodedPhoneticFamilyName);
979                        first = false;
980                    }
981                    if (!TextUtils.isEmpty(encodedPhoneticMiddleName)) {
982                        if (first) {
983                            first = false;
984                        } else {
985                            builder.append(' ');
986                        }
987                        builder.append(encodedPhoneticMiddleName);
988                    }
989                    if (!TextUtils.isEmpty(encodedPhoneticGivenName)) {
990                        if (!first) {
991                            builder.append(' ');
992                        }
993                        builder.append(encodedPhoneticGivenName);
994                    }
995                }
996                builder.append(VCARD_ITEM_SEPARATOR);
997                builder.append(VCARD_ITEM_SEPARATOR);
998                builder.append(VCARD_ITEM_SEPARATOR);
999                builder.append(VCARD_ITEM_SEPARATOR);
1000                builder.append(VCARD_END_OF_LINE);
1001            }
1002        } else if (mIsDoCoMo) {
1003            builder.append(Constants.PROPERTY_SOUND);
1004            builder.append(VCARD_PARAM_SEPARATOR);
1005            builder.append(Constants.PARAM_TYPE_X_IRMC_N);
1006            builder.append(VCARD_DATA_SEPARATOR);
1007            builder.append(VCARD_ITEM_SEPARATOR);
1008            builder.append(VCARD_ITEM_SEPARATOR);
1009            builder.append(VCARD_ITEM_SEPARATOR);
1010            builder.append(VCARD_ITEM_SEPARATOR);
1011            builder.append(VCARD_END_OF_LINE);
1012        }
1013
1014        if (mUsesDefactProperty) {
1015            if (!TextUtils.isEmpty(phoneticGivenName)) {
1016                final boolean reallyUseQuotedPrintable =
1017                    (mUsesQuotedPrintable &&
1018                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticGivenName));
1019                final String encodedPhoneticGivenName;
1020                if (reallyUseQuotedPrintable) {
1021                    encodedPhoneticGivenName = encodeQuotedPrintable(phoneticGivenName);
1022                } else {
1023                    encodedPhoneticGivenName = escapeCharacters(phoneticGivenName);
1024                }
1025                builder.append(Constants.PROPERTY_X_PHONETIC_FIRST_NAME);
1026                if (shouldAppendCharsetParameter(phoneticGivenName)) {
1027                    builder.append(VCARD_PARAM_SEPARATOR);
1028                    builder.append(mVCardCharsetParameter);
1029                }
1030                if (reallyUseQuotedPrintable) {
1031                    builder.append(VCARD_PARAM_SEPARATOR);
1032                    builder.append(VCARD_PARAM_ENCODING_QP);
1033                }
1034                builder.append(VCARD_DATA_SEPARATOR);
1035                builder.append(encodedPhoneticGivenName);
1036                builder.append(VCARD_END_OF_LINE);
1037            }
1038            if (!TextUtils.isEmpty(phoneticMiddleName)) {
1039                final boolean reallyUseQuotedPrintable =
1040                    (mUsesQuotedPrintable &&
1041                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticMiddleName));
1042                final String encodedPhoneticMiddleName;
1043                if (reallyUseQuotedPrintable) {
1044                    encodedPhoneticMiddleName = encodeQuotedPrintable(phoneticMiddleName);
1045                } else {
1046                    encodedPhoneticMiddleName = escapeCharacters(phoneticMiddleName);
1047                }
1048                builder.append(Constants.PROPERTY_X_PHONETIC_MIDDLE_NAME);
1049                if (shouldAppendCharsetParameter(phoneticMiddleName)) {
1050                    builder.append(VCARD_PARAM_SEPARATOR);
1051                    builder.append(mVCardCharsetParameter);
1052                }
1053                if (reallyUseQuotedPrintable) {
1054                    builder.append(VCARD_PARAM_SEPARATOR);
1055                    builder.append(VCARD_PARAM_ENCODING_QP);
1056                }
1057                builder.append(VCARD_DATA_SEPARATOR);
1058                builder.append(encodedPhoneticMiddleName);
1059                builder.append(VCARD_END_OF_LINE);
1060            }
1061            if (!TextUtils.isEmpty(phoneticFamilyName)) {
1062                final boolean reallyUseQuotedPrintable =
1063                    (mUsesQuotedPrintable &&
1064                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(phoneticFamilyName));
1065                final String encodedPhoneticFamilyName;
1066                if (reallyUseQuotedPrintable) {
1067                    encodedPhoneticFamilyName = encodeQuotedPrintable(phoneticFamilyName);
1068                } else {
1069                    encodedPhoneticFamilyName = escapeCharacters(phoneticFamilyName);
1070                }
1071                builder.append(Constants.PROPERTY_X_PHONETIC_LAST_NAME);
1072                if (shouldAppendCharsetParameter(phoneticFamilyName)) {
1073                    builder.append(VCARD_PARAM_SEPARATOR);
1074                    builder.append(mVCardCharsetParameter);
1075                }
1076                if (reallyUseQuotedPrintable) {
1077                    builder.append(VCARD_PARAM_SEPARATOR);
1078                    builder.append(VCARD_PARAM_ENCODING_QP);
1079                }
1080                builder.append(VCARD_DATA_SEPARATOR);
1081                builder.append(encodedPhoneticFamilyName);
1082                builder.append(VCARD_END_OF_LINE);
1083            }
1084        }
1085    }
1086
1087    private void appendNickNames(final StringBuilder builder,
1088            final Map<String, List<ContentValues>> contentValuesListMap) {
1089        final List<ContentValues> contentValuesList = contentValuesListMap
1090                .get(Nickname.CONTENT_ITEM_TYPE);
1091        if (contentValuesList == null) {
1092            return;
1093        }
1094
1095        final boolean useAndroidProperty;
1096        if (mIsV30) {
1097            useAndroidProperty = false;
1098        } else if (mUsesAndroidProperty) {
1099            useAndroidProperty = true;
1100        } else {
1101            // There's no way to add this field.
1102            return;
1103        }
1104
1105        for (ContentValues contentValues : contentValuesList) {
1106            final String nickname = contentValues.getAsString(Nickname.NAME);
1107            if (TextUtils.isEmpty(nickname)) {
1108                continue;
1109            }
1110            if (useAndroidProperty) {
1111                appendAndroidSpecificProperty(builder, Nickname.CONTENT_ITEM_TYPE,
1112                        contentValues);
1113            } else {
1114                appendVCardLineWithCharsetAndQPDetection(builder,
1115                        Constants.PROPERTY_NICKNAME, nickname);
1116            }
1117        }
1118    }
1119
1120    private void appendPhones(final StringBuilder builder,
1121            final Map<String, List<ContentValues>> contentValuesListMap) {
1122        final List<ContentValues> contentValuesList = contentValuesListMap
1123                .get(Phone.CONTENT_ITEM_TYPE);
1124        boolean phoneLineExists = false;
1125        if (contentValuesList != null) {
1126            Set<String> phoneSet = new HashSet<String>();
1127            for (ContentValues contentValues : contentValuesList) {
1128                final Integer typeAsObject = contentValues.getAsInteger(Phone.TYPE);
1129                final String label = contentValues.getAsString(Phone.LABEL);
1130                final Integer isPrimaryAsInteger = contentValues.getAsInteger(Phone.IS_PRIMARY);
1131                final boolean isPrimary = (isPrimaryAsInteger != null ?
1132                        (isPrimaryAsInteger > 0) : false);
1133                String phoneNumber = contentValues.getAsString(Phone.NUMBER);
1134                if (phoneNumber != null) {
1135                    phoneNumber = phoneNumber.trim();
1136                }
1137                if (TextUtils.isEmpty(phoneNumber)) {
1138                    continue;
1139                }
1140                int type = (typeAsObject != null ? typeAsObject : DEFAULT_PHONE_TYPE);
1141                if (type == Phone.TYPE_PAGER) {
1142                    phoneLineExists = true;
1143                    if (!phoneSet.contains(phoneNumber)) {
1144                        phoneSet.add(phoneNumber);
1145                        appendVCardTelephoneLine(builder, type, label, phoneNumber, isPrimary);
1146                    }
1147                } else {
1148                    // The entry "may" have several phone numbers when the contact entry is
1149                    // corrupted because of its original source.
1150                    //
1151                    // e.g. I encountered the entry like the following.
1152                    // "111-222-3333 (Miami)\n444-555-6666 (Broward; 305-653-6796 (Miami); ..."
1153                    // This kind of entry is not able to be inserted via Android devices, but
1154                    // possible if the source of the data is already corrupted.
1155                    List<String> phoneNumberList = splitIfSeveralPhoneNumbersExist(phoneNumber);
1156                    if (phoneNumberList.isEmpty()) {
1157                        continue;
1158                    }
1159                    phoneLineExists = true;
1160                    for (String actualPhoneNumber : phoneNumberList) {
1161                        if (!phoneSet.contains(actualPhoneNumber)) {
1162                            final int format = VCardUtils.getPhoneNumberFormat(mVCardType);
1163                            final String formattedPhoneNumber =
1164                                    PhoneNumberUtils.formatNumber(actualPhoneNumber, format);
1165                            phoneSet.add(actualPhoneNumber);
1166                            appendVCardTelephoneLine(builder, type, label,
1167                                    formattedPhoneNumber, isPrimary);
1168                        }
1169                    }
1170                }
1171            }
1172        }
1173
1174        if (!phoneLineExists && mIsDoCoMo) {
1175            appendVCardTelephoneLine(builder, Phone.TYPE_HOME, "", "", false);
1176        }
1177    }
1178
1179    private List<String> splitIfSeveralPhoneNumbersExist(final String phoneNumber) {
1180        List<String> phoneList = new ArrayList<String>();
1181
1182        StringBuilder builder = new StringBuilder();
1183        final int length = phoneNumber.length();
1184        for (int i = 0; i < length; i++) {
1185            final char ch = phoneNumber.charAt(i);
1186            if (Character.isDigit(ch)) {
1187                builder.append(ch);
1188            } else if ((ch == ';' || ch == '\n') && builder.length() > 0) {
1189                phoneList.add(builder.toString());
1190                builder = new StringBuilder();
1191            }
1192        }
1193        if (builder.length() > 0) {
1194            phoneList.add(builder.toString());
1195        }
1196
1197        return phoneList;
1198    }
1199
1200    private void appendEmails(final StringBuilder builder,
1201            final Map<String, List<ContentValues>> contentValuesListMap) {
1202        final List<ContentValues> contentValuesList = contentValuesListMap
1203                .get(Email.CONTENT_ITEM_TYPE);
1204
1205        boolean emailAddressExists = false;
1206        if (contentValuesList != null) {
1207            final Set<String> addressSet = new HashSet<String>();
1208            for (ContentValues contentValues : contentValuesList) {
1209                String emailAddress = contentValues.getAsString(Email.DATA);
1210                if (emailAddress != null) {
1211                    emailAddress = emailAddress.trim();
1212                }
1213                if (TextUtils.isEmpty(emailAddress)) {
1214                    continue;
1215                }
1216                Integer typeAsObject = contentValues.getAsInteger(Email.TYPE);
1217                final int type = (typeAsObject != null ?
1218                        typeAsObject : DEFAULT_EMAIL_TYPE);
1219                final String label = contentValues.getAsString(Email.LABEL);
1220                Integer isPrimaryAsInteger = contentValues.getAsInteger(Email.IS_PRIMARY);
1221                final boolean isPrimary = (isPrimaryAsInteger != null ?
1222                        (isPrimaryAsInteger > 0) : false);
1223                emailAddressExists = true;
1224                if (!addressSet.contains(emailAddress)) {
1225                    addressSet.add(emailAddress);
1226                    appendVCardEmailLine(builder, type, label, emailAddress, isPrimary);
1227                }
1228            }
1229        }
1230
1231        if (!emailAddressExists && mIsDoCoMo) {
1232            appendVCardEmailLine(builder, Email.TYPE_HOME, "", "", false);
1233        }
1234    }
1235
1236    private void appendPostals(final StringBuilder builder,
1237            final Map<String, List<ContentValues>> contentValuesListMap) {
1238        final List<ContentValues> contentValuesList = contentValuesListMap
1239                .get(StructuredPostal.CONTENT_ITEM_TYPE);
1240        if (contentValuesList != null) {
1241            if (mIsDoCoMo) {
1242                appendPostalsForDoCoMo(builder, contentValuesList);
1243            } else {
1244                appendPostalsForGeneric(builder, contentValuesList);
1245            }
1246        } else if (mIsDoCoMo) {
1247            builder.append(Constants.PROPERTY_ADR);
1248            builder.append(VCARD_PARAM_SEPARATOR);
1249            builder.append(Constants.PARAM_TYPE_HOME);
1250            builder.append(VCARD_DATA_SEPARATOR);
1251            builder.append(VCARD_END_OF_LINE);
1252        }
1253    }
1254
1255    /**
1256     * Tries to append just one line. If there's no appropriate address
1257     * information, append an empty line.
1258     */
1259    private void appendPostalsForDoCoMo(final StringBuilder builder,
1260            final List<ContentValues> contentValuesList) {
1261        // TODO: from old, inefficient code. fix this.
1262        if (appendPostalsForDoCoMoInternal(builder, contentValuesList,
1263                StructuredPostal.TYPE_HOME)) {
1264            return;
1265        }
1266        if (appendPostalsForDoCoMoInternal(builder, contentValuesList,
1267                StructuredPostal.TYPE_WORK)) {
1268            return;
1269        }
1270        if (appendPostalsForDoCoMoInternal(builder, contentValuesList,
1271                StructuredPostal.TYPE_OTHER)) {
1272            return;
1273        }
1274        if (appendPostalsForDoCoMoInternal(builder, contentValuesList,
1275                StructuredPostal.TYPE_CUSTOM)) {
1276            return;
1277        }
1278
1279        Log.w(LOG_TAG,
1280                "Should not come here. Must have at least one postal data.");
1281    }
1282
1283    private boolean appendPostalsForDoCoMoInternal(final StringBuilder builder,
1284            final List<ContentValues> contentValuesList, Integer preferedType) {
1285        for (ContentValues contentValues : contentValuesList) {
1286            final Integer type = contentValues.getAsInteger(StructuredPostal.TYPE);
1287            final String label = contentValues.getAsString(StructuredPostal.LABEL);
1288            if (type == preferedType) {
1289                // Note: Not sure why we need to emit "empty" line even when actual
1290                //       data does not exist. There may be some reason or may not.
1291                //       We keep safer side since the previous implementation did so.
1292                appendVCardPostalLine(builder, type, label, contentValues, true, true);
1293                return true;
1294            }
1295        }
1296        return false;
1297    }
1298
1299    private void appendPostalsForGeneric(final StringBuilder builder,
1300            final List<ContentValues> contentValuesList) {
1301        for (ContentValues contentValues : contentValuesList) {
1302            if (contentValues == null) {
1303                continue;
1304            }
1305            final Integer typeAsObject = contentValues.getAsInteger(StructuredPostal.TYPE);
1306            final int type = (typeAsObject != null ?
1307                    typeAsObject : DEFAULT_POSTAL_TYPE);
1308            final String label = contentValues.getAsString(StructuredPostal.LABEL);
1309            final Integer isPrimaryAsInteger =
1310                contentValues.getAsInteger(StructuredPostal.IS_PRIMARY);
1311            final boolean isPrimary = (isPrimaryAsInteger != null ?
1312                    (isPrimaryAsInteger > 0) : false);
1313            appendVCardPostalLine(builder, type, label, contentValues, isPrimary, false);
1314        }
1315    }
1316
1317    private void appendIms(final StringBuilder builder,
1318            final Map<String, List<ContentValues>> contentValuesListMap) {
1319        final List<ContentValues> contentValuesList = contentValuesListMap
1320                .get(Im.CONTENT_ITEM_TYPE);
1321        if (contentValuesList == null) {
1322            return;
1323        }
1324        for (ContentValues contentValues : contentValuesList) {
1325            final Integer protocolAsObject = contentValues.getAsInteger(Im.PROTOCOL);
1326            if (protocolAsObject == null) {
1327                continue;
1328            }
1329            final String propertyName = VCardUtils.getPropertyNameForIm(protocolAsObject);
1330            if (propertyName == null) {
1331                continue;
1332            }
1333            String data = contentValues.getAsString(Im.DATA);
1334            if (data != null) {
1335                data = data.trim();
1336            }
1337            if (TextUtils.isEmpty(data)) {
1338                continue;
1339            }
1340            final String typeAsString;
1341            {
1342                final Integer typeAsInteger = contentValues.getAsInteger(Im.TYPE);
1343                switch (typeAsInteger != null ? typeAsInteger : Im.TYPE_OTHER) {
1344                    case Im.TYPE_HOME: {
1345                        typeAsString = Constants.PARAM_TYPE_HOME;
1346                        break;
1347                    }
1348                    case Im.TYPE_WORK: {
1349                        typeAsString = Constants.PARAM_TYPE_WORK;
1350                        break;
1351                    }
1352                    case Im.TYPE_CUSTOM: {
1353                        final String label = contentValues.getAsString(Im.LABEL);
1354                        typeAsString = (label != null ? "X-" + label : null);
1355                        break;
1356                    }
1357                    case Im.TYPE_OTHER:  // Ignore
1358                    default: {
1359                        typeAsString = null;
1360                        break;
1361                    }
1362                }
1363            }
1364
1365            List<String> parameterList = new ArrayList<String>();
1366            if (!TextUtils.isEmpty(typeAsString)) {
1367                parameterList.add(typeAsString);
1368            }
1369            final Integer isPrimaryAsInteger = contentValues.getAsInteger(Im.IS_PRIMARY);
1370            final boolean isPrimary = (isPrimaryAsInteger != null ?
1371                    (isPrimaryAsInteger > 0) : false);
1372            if (isPrimary) {
1373                parameterList.add(Constants.PARAM_TYPE_PREF);
1374            }
1375
1376            appendVCardLineWithCharsetAndQPDetection(
1377                    builder, propertyName, parameterList, data);
1378        }
1379    }
1380
1381    private void appendWebsites(final StringBuilder builder,
1382            final Map<String, List<ContentValues>> contentValuesListMap) {
1383        final List<ContentValues> contentValuesList = contentValuesListMap
1384                .get(Website.CONTENT_ITEM_TYPE);
1385        if (contentValuesList == null) {
1386            return;
1387        }
1388        for (ContentValues contentValues : contentValuesList) {
1389            String website = contentValues.getAsString(Website.URL);
1390            if (website != null) {
1391                website = website.trim();
1392            }
1393
1394            // Note: vCard 3.0 does not allow any parameter addition toward "URL"
1395            //       property, while there's no document in vCard 2.1.
1396            //
1397            // TODO: Should we allow adding it when appropriate?
1398            //       (Actually, we drop some data. Using "group.X-URL-TYPE" or something
1399            //        may help)
1400            if (!TextUtils.isEmpty(website)) {
1401                appendVCardLine(builder, Constants.PROPERTY_URL, website);
1402            }
1403        }
1404    }
1405
1406    /**
1407     * Theoretically, there must be only one birthday for each vCard entry.
1408     * Also, we are afraid of some importer's parse error during its import.
1409     * We emit only one birthday entry even when there are more than one.
1410     */
1411    private void appendBirthday(final StringBuilder builder,
1412            final Map<String, List<ContentValues>> contentValuesListMap) {
1413        final List<ContentValues> contentValuesList =
1414                contentValuesListMap.get(Event.CONTENT_ITEM_TYPE);
1415        if (contentValuesList == null) {
1416            return;
1417        }
1418        String primaryBirthday = null;
1419        String secondaryBirthday = null;
1420        for (ContentValues contentValues : contentValuesList) {
1421            if (contentValues == null) {
1422                continue;
1423            }
1424            final Integer eventType = contentValues.getAsInteger(Event.TYPE);
1425            if (eventType == null || !eventType.equals(Event.TYPE_BIRTHDAY)) {
1426                continue;
1427            }
1428            final String birthdayCandidate = contentValues.getAsString(Event.START_DATE);
1429            if (birthdayCandidate == null) {
1430                continue;
1431            }
1432            final Integer isSuperPrimaryAsInteger =
1433                contentValues.getAsInteger(Event.IS_SUPER_PRIMARY);
1434            final boolean isSuperPrimary = (isSuperPrimaryAsInteger != null ?
1435                    (isSuperPrimaryAsInteger > 0) : false);
1436            if (isSuperPrimary) {
1437                // "super primary" birthday should the prefered one.
1438                primaryBirthday = birthdayCandidate;
1439                break;
1440            }
1441            final Integer isPrimaryAsInteger =
1442                contentValues.getAsInteger(Event.IS_PRIMARY);
1443            final boolean isPrimary = (isPrimaryAsInteger != null ?
1444                    (isPrimaryAsInteger > 0) : false);
1445            if (isPrimary) {
1446                // We don't break here since "super primary" birthday may exist later.
1447                primaryBirthday = birthdayCandidate;
1448            } else if (secondaryBirthday == null) {
1449                // First entry is set to the "secondary" candidate.
1450                secondaryBirthday = birthdayCandidate;
1451            }
1452        }
1453
1454        final String birthday;
1455        if (primaryBirthday != null) {
1456            birthday = primaryBirthday.trim();
1457        } else if (secondaryBirthday != null){
1458            birthday = secondaryBirthday.trim();
1459        } else {
1460            return;
1461        }
1462        appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_BDAY, birthday);
1463    }
1464
1465    private void appendOrganizations(final StringBuilder builder,
1466            final Map<String, List<ContentValues>> contentValuesListMap) {
1467        final List<ContentValues> contentValuesList = contentValuesListMap
1468                .get(Organization.CONTENT_ITEM_TYPE);
1469        if (contentValuesList != null) {
1470            for (ContentValues contentValues : contentValuesList) {
1471                String company = contentValues.getAsString(Organization.COMPANY);
1472                if (company != null) {
1473                    company = company.trim();
1474                }
1475                String department = contentValues.getAsString(Organization.DEPARTMENT);
1476                if (department != null) {
1477                    department = department.trim();
1478                }
1479                String title = contentValues.getAsString(Organization.TITLE);
1480                if (title != null) {
1481                    title = title.trim();
1482                }
1483
1484                StringBuilder orgBuilder = new StringBuilder();
1485                if (!TextUtils.isEmpty(company)) {
1486                    orgBuilder.append(company);
1487                }
1488                if (!TextUtils.isEmpty(department)) {
1489                    if (orgBuilder.length() > 0) {
1490                        orgBuilder.append(';');
1491                    }
1492                    orgBuilder.append(department);
1493                }
1494                final String orgline = orgBuilder.toString();
1495                appendVCardLine(builder, Constants.PROPERTY_ORG, orgline,
1496                        !VCardUtils.containsOnlyPrintableAscii(orgline),
1497                        (mUsesQuotedPrintable &&
1498                                !VCardUtils.containsOnlyNonCrLfPrintableAscii(orgline)));
1499
1500                if (!TextUtils.isEmpty(title)) {
1501                    appendVCardLine(builder, Constants.PROPERTY_TITLE, title,
1502                            !VCardUtils.containsOnlyPrintableAscii(title),
1503                            (mUsesQuotedPrintable &&
1504                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(title)));
1505                }
1506            }
1507        }
1508    }
1509
1510    private void appendPhotos(final StringBuilder builder,
1511            final Map<String, List<ContentValues>> contentValuesListMap) {
1512        final List<ContentValues> contentValuesList = contentValuesListMap
1513                .get(Photo.CONTENT_ITEM_TYPE);
1514        if (contentValuesList != null) {
1515            for (ContentValues contentValues : contentValuesList) {
1516                byte[] data = contentValues.getAsByteArray(Photo.PHOTO);
1517                if (data == null) {
1518                    continue;
1519                }
1520                final String photoType;
1521                // Use some heuristics for guessing the format of the image.
1522                // TODO: there should be some general API for detecting the file format.
1523                if (data.length >= 3 && data[0] == 'G' && data[1] == 'I'
1524                        && data[2] == 'F') {
1525                    photoType = "GIF";
1526                } else if (data.length >= 4 && data[0] == (byte) 0x89
1527                        && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') {
1528                    // Note: vCard 2.1 officially does not support PNG, but we may
1529                    // have it and using X- word like "X-PNG" may not let importers
1530                    // know it is PNG. So we use the String "PNG" as is...
1531                    photoType = "PNG";
1532                } else if (data.length >= 2 && data[0] == (byte) 0xff
1533                        && data[1] == (byte) 0xd8) {
1534                    photoType = "JPEG";
1535                } else {
1536                    Log.d(LOG_TAG, "Unknown photo type. Ignore.");
1537                    continue;
1538                }
1539                final String photoString = VCardUtils.encodeBase64(data);
1540                if (photoString.length() > 0) {
1541                    appendVCardPhotoLine(builder, photoString, photoType);
1542                }
1543            }
1544        }
1545    }
1546
1547    private void appendNotes(final StringBuilder builder,
1548            final Map<String, List<ContentValues>> contentValuesListMap) {
1549        final List<ContentValues> contentValuesList =
1550            contentValuesListMap.get(Note.CONTENT_ITEM_TYPE);
1551        if (contentValuesList != null) {
1552            if (mOnlyOneNoteFieldIsAvailable) {
1553                StringBuilder noteBuilder = new StringBuilder();
1554                boolean first = true;
1555                for (ContentValues contentValues : contentValuesList) {
1556                    String note = contentValues.getAsString(Note.NOTE);
1557                    if (note == null) {
1558                        note = "";
1559                    }
1560                    if (note.length() > 0) {
1561                        if (first) {
1562                            first = false;
1563                        } else {
1564                            noteBuilder.append('\n');
1565                        }
1566                        noteBuilder.append(note);
1567                    }
1568                }
1569                final String noteStr = noteBuilder.toString();
1570                // This means we scan noteStr completely twice, which is redundant.
1571                // But for now, we assume this is not so time-consuming..
1572                final boolean shouldAppendCharsetInfo =
1573                    !VCardUtils.containsOnlyPrintableAscii(noteStr);
1574                final boolean reallyUseQuotedPrintable =
1575                        (mUsesQuotedPrintable &&
1576                            !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1577                appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr,
1578                        shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1579            } else {
1580                for (ContentValues contentValues : contentValuesList) {
1581                    final String noteStr = contentValues.getAsString(Note.NOTE);
1582                    if (!TextUtils.isEmpty(noteStr)) {
1583                        final boolean shouldAppendCharsetInfo =
1584                                !VCardUtils.containsOnlyPrintableAscii(noteStr);
1585                        final boolean reallyUseQuotedPrintable =
1586                                (mUsesQuotedPrintable &&
1587                                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(noteStr));
1588                        appendVCardLine(builder, Constants.PROPERTY_NOTE, noteStr,
1589                                shouldAppendCharsetInfo, reallyUseQuotedPrintable);
1590                    }
1591                }
1592            }
1593        }
1594    }
1595
1596    private void appendAndroidSpecificProperty(final StringBuilder builder,
1597            final String mimeType, ContentValues contentValues) {
1598        List<String> rawDataList = new ArrayList<String>();
1599        rawDataList.add(mimeType);
1600        final List<String> columnNameList;
1601        if (Nickname.CONTENT_ITEM_TYPE.equals(mimeType)) {
1602
1603        } else {
1604            // If you add the other field, please check all the columns are able to be
1605            // converted to String.
1606            //
1607            // e.g. BLOB is not what we can handle here now.
1608            return;
1609        }
1610
1611        for (int i = 1; i <= Constants.MAX_DATA_COLUMN; i++) {
1612            String value = contentValues.getAsString("data" + i);
1613            if (value == null) {
1614                value = "";
1615            }
1616            rawDataList.add(value);
1617        }
1618
1619        appendVCardLineWithCharsetAndQPDetection(builder,
1620                Constants.PROPERTY_X_ANDROID_CUSTOM, rawDataList);
1621    }
1622
1623    /**
1624     * Append '\' to the characters which should be escaped. The character set is different
1625     * not only between vCard 2.1 and vCard 3.0 but also among each device.
1626     *
1627     * Note that Quoted-Printable string must not be input here.
1628     */
1629    @SuppressWarnings("fallthrough")
1630    private String escapeCharacters(final String unescaped) {
1631        if (TextUtils.isEmpty(unescaped)) {
1632            return "";
1633        }
1634
1635        final StringBuilder tmpBuilder = new StringBuilder();
1636        final int length = unescaped.length();
1637        for (int i = 0; i < length; i++) {
1638            final char ch = unescaped.charAt(i);
1639            switch (ch) {
1640                case ';': {
1641                    tmpBuilder.append('\\');
1642                    tmpBuilder.append(';');
1643                    break;
1644                }
1645                case '\r': {
1646                    if (i + 1 < length) {
1647                        char nextChar = unescaped.charAt(i);
1648                        if (nextChar == '\n') {
1649                            break;
1650                        } else {
1651                            // fall through
1652                        }
1653                    } else {
1654                        // fall through
1655                    }
1656                }
1657                case '\n': {
1658                    // In vCard 2.1, there's no specification about this, while
1659                    // vCard 3.0 explicitly requires this should be encoded to "\n".
1660                    tmpBuilder.append("\\n");
1661                    break;
1662                }
1663                case '\\': {
1664                    if (mIsV30) {
1665                        tmpBuilder.append("\\\\");
1666                        break;
1667                    } else {
1668                        // fall through
1669                    }
1670                }
1671                case '<':
1672                case '>': {
1673                    if (mIsDoCoMo) {
1674                        tmpBuilder.append('\\');
1675                        tmpBuilder.append(ch);
1676                    } else {
1677                        tmpBuilder.append(ch);
1678                    }
1679                    break;
1680                }
1681                case ',': {
1682                    if (mIsV30) {
1683                        tmpBuilder.append("\\,");
1684                    } else {
1685                        tmpBuilder.append(ch);
1686                    }
1687                    break;
1688                }
1689                default: {
1690                    tmpBuilder.append(ch);
1691                    break;
1692                }
1693            }
1694        }
1695        return tmpBuilder.toString();
1696    }
1697
1698    private void appendVCardPhotoLine(final StringBuilder builder,
1699            final String encodedData, final String photoType) {
1700        StringBuilder tmpBuilder = new StringBuilder();
1701        tmpBuilder.append(Constants.PROPERTY_PHOTO);
1702        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1703        if (mIsV30) {
1704            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V30);
1705        } else {
1706            tmpBuilder.append(VCARD_PARAM_ENCODING_BASE64_V21);
1707        }
1708        tmpBuilder.append(VCARD_PARAM_SEPARATOR);
1709        appendTypeParameter(tmpBuilder, photoType);
1710        tmpBuilder.append(VCARD_DATA_SEPARATOR);
1711        tmpBuilder.append(encodedData);
1712
1713        final String tmpStr = tmpBuilder.toString();
1714        tmpBuilder = new StringBuilder();
1715        int lineCount = 0;
1716        int length = tmpStr.length();
1717        for (int i = 0; i < length; i++) {
1718            tmpBuilder.append(tmpStr.charAt(i));
1719            lineCount++;
1720            if (lineCount > 72) {
1721                tmpBuilder.append(VCARD_END_OF_LINE);
1722                tmpBuilder.append(VCARD_WS);
1723                lineCount = 0;
1724            }
1725        }
1726        builder.append(tmpBuilder.toString());
1727        builder.append(VCARD_END_OF_LINE);
1728        builder.append(VCARD_END_OF_LINE);
1729    }
1730
1731    private class PostalStruct {
1732        final boolean reallyUseQuotedPrintable;
1733        final boolean appendCharset;
1734        final String addressData;
1735        public PostalStruct(final boolean reallyUseQuotedPrintable,
1736                final boolean appendCharset, final String addressData) {
1737            this.reallyUseQuotedPrintable = reallyUseQuotedPrintable;
1738            this.appendCharset = appendCharset;
1739            this.addressData = addressData;
1740        }
1741    }
1742
1743    /**
1744     * @return null when there's no information available to construct the data.
1745     */
1746    private PostalStruct tryConstructPostalStruct(ContentValues contentValues) {
1747        boolean reallyUseQuotedPrintable = false;
1748        boolean appendCharset = false;
1749
1750        boolean dataArrayExists = false;
1751        String[] dataArray = VCardUtils.getVCardPostalElements(contentValues);
1752        for (String data : dataArray) {
1753            if (!TextUtils.isEmpty(data)) {
1754                dataArrayExists = true;
1755                if (!appendCharset && !VCardUtils.containsOnlyPrintableAscii(data)) {
1756                    appendCharset = true;
1757                }
1758                if (mUsesQuotedPrintable && !VCardUtils.containsOnlyNonCrLfPrintableAscii(data)) {
1759                    reallyUseQuotedPrintable = true;
1760                    break;
1761                }
1762            }
1763        }
1764
1765        if (dataArrayExists) {
1766            StringBuffer addressBuffer = new StringBuffer();
1767            boolean first = true;
1768            for (String data : dataArray) {
1769                if (first) {
1770                    first = false;
1771                } else {
1772                    addressBuffer.append(VCARD_ITEM_SEPARATOR);
1773                }
1774                if (!TextUtils.isEmpty(data)) {
1775                    if (reallyUseQuotedPrintable) {
1776                        addressBuffer.append(encodeQuotedPrintable(data));
1777                    } else {
1778                        addressBuffer.append(escapeCharacters(data));
1779                    }
1780                }
1781            }
1782            return new PostalStruct(reallyUseQuotedPrintable, appendCharset,
1783                    addressBuffer.toString());
1784        }
1785
1786        String formattedAddress =
1787            contentValues.getAsString(StructuredPostal.FORMATTED_ADDRESS);
1788        if (!TextUtils.isEmpty(formattedAddress)) {
1789            reallyUseQuotedPrintable =
1790                !VCardUtils.containsOnlyPrintableAscii(formattedAddress);
1791            appendCharset =
1792                !VCardUtils.containsOnlyNonCrLfPrintableAscii(formattedAddress);
1793            if (reallyUseQuotedPrintable) {
1794                formattedAddress = encodeQuotedPrintable(formattedAddress);
1795            } else {
1796                formattedAddress = escapeCharacters(formattedAddress);
1797            }
1798            // We use the second value ("Extended Address").
1799            //
1800            // adr-value    = 0*6(text-value ";") text-value
1801            //              ; PO Box, Extended Address, Street, Locality, Region, Postal
1802            //              ; Code, Country Name
1803            StringBuffer addressBuffer = new StringBuffer();
1804            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1805            addressBuffer.append(formattedAddress);
1806            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1807            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1808            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1809            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1810            addressBuffer.append(VCARD_ITEM_SEPARATOR);
1811            return new PostalStruct(
1812                    reallyUseQuotedPrintable, appendCharset, addressBuffer.toString());
1813        }
1814        return null;  // There's no data available.
1815    }
1816
1817    private void appendVCardPostalLine(final StringBuilder builder,
1818            final int type, final String label, final ContentValues contentValues,
1819            final boolean isPrimary, final boolean emitLineEveryTime) {
1820        final boolean reallyUseQuotedPrintable;
1821        final boolean appendCharset;
1822        final String addressData;
1823        {
1824            PostalStruct postalStruct = tryConstructPostalStruct(contentValues);
1825            if (postalStruct == null) {
1826                if (emitLineEveryTime) {
1827                    reallyUseQuotedPrintable = false;
1828                    appendCharset = false;
1829                    addressData = "";
1830                } else {
1831                    return;
1832                }
1833            } else {
1834                reallyUseQuotedPrintable = postalStruct.reallyUseQuotedPrintable;
1835                appendCharset = postalStruct.appendCharset;
1836                addressData = postalStruct.addressData;
1837            }
1838        }
1839
1840        List<String> parameterList = new ArrayList<String>();
1841        if (isPrimary) {
1842            parameterList.add(Constants.PARAM_TYPE_PREF);
1843        }
1844        switch (type) {
1845            case StructuredPostal.TYPE_HOME: {
1846                parameterList.add(Constants.PARAM_TYPE_HOME);
1847                break;
1848            }
1849            case StructuredPostal.TYPE_WORK: {
1850                parameterList.add(Constants.PARAM_TYPE_WORK);
1851                break;
1852            }
1853            case StructuredPostal.TYPE_CUSTOM: {
1854                if (mUsesAndroidProperty && !TextUtils.isEmpty(label)
1855                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1856                    // We're not sure whether the label is valid in the spec
1857                    // ("IANA-token" in the vCard 3.0 is unclear...)
1858                    // Just  for safety, we add "X-" at the beggining of each label.
1859                    // Also checks the label obeys with vCard 3.0 spec.
1860                    parameterList.add("X-" + label);
1861                }
1862                break;
1863            }
1864            case StructuredPostal.TYPE_OTHER: {
1865                break;
1866            }
1867            default: {
1868                Log.e(LOG_TAG, "Unknown StructuredPostal type: " + type);
1869                break;
1870            }
1871        }
1872
1873        // Actual data construction starts from here.
1874        // TODO: add a new version of appendVCardLine() for this purpose.
1875
1876        builder.append(Constants.PROPERTY_ADR);
1877        builder.append(VCARD_PARAM_SEPARATOR);
1878
1879        // Parameters
1880        {
1881            boolean shouldAppendParamSeparator = false;
1882            if (!parameterList.isEmpty()) {
1883                appendTypeParameters(builder, parameterList);
1884                shouldAppendParamSeparator = true;
1885            }
1886
1887            if (appendCharset) {
1888                // Strictly, vCard 3.0 does not allow exporters to emit charset information,
1889                // but we will add it since the information should be useful for importers,
1890                //
1891                // Assume no parser does not emit error with this parameter in vCard 3.0.
1892                if (shouldAppendParamSeparator) {
1893                    builder.append(VCARD_PARAM_SEPARATOR);
1894                }
1895                builder.append(mVCardCharsetParameter);
1896                shouldAppendParamSeparator = true;
1897            }
1898
1899            if (reallyUseQuotedPrintable) {
1900                if (shouldAppendParamSeparator) {
1901                    builder.append(VCARD_PARAM_SEPARATOR);
1902                }
1903                builder.append(VCARD_PARAM_ENCODING_QP);
1904                shouldAppendParamSeparator = true;
1905            }
1906        }
1907
1908        builder.append(VCARD_DATA_SEPARATOR);
1909        builder.append(addressData);
1910        builder.append(VCARD_END_OF_LINE);
1911    }
1912
1913    private void appendVCardEmailLine(final StringBuilder builder,
1914            final int type, final String label,
1915            final String rawData, final boolean isPrimary) {
1916        final String typeAsString;
1917        switch (type) {
1918            case Email.TYPE_CUSTOM: {
1919                // For backward compatibility.
1920                // Detail: Until Donut, there isn't TYPE_MOBILE for email while there is now.
1921                //         To support mobile type at that time, this custom label had been used.
1922                if (android.provider.Contacts.ContactMethodsColumns.MOBILE_EMAIL_TYPE_NAME
1923                        .equals(label)) {
1924                    typeAsString = Constants.PARAM_TYPE_CELL;
1925                } else if (mUsesAndroidProperty && !TextUtils.isEmpty(label)
1926                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
1927                    typeAsString = "X-" + label;
1928                } else {
1929                    typeAsString = null;
1930                }
1931                break;
1932            }
1933            case Email.TYPE_HOME: {
1934                typeAsString = Constants.PARAM_TYPE_HOME;
1935                break;
1936            }
1937            case Email.TYPE_WORK: {
1938                typeAsString = Constants.PARAM_TYPE_WORK;
1939                break;
1940            }
1941            case Email.TYPE_OTHER: {
1942                typeAsString = null;
1943                break;
1944            }
1945            case Email.TYPE_MOBILE: {
1946                typeAsString = Constants.PARAM_TYPE_CELL;
1947                break;
1948            }
1949            default: {
1950                Log.e(LOG_TAG, "Unknown Email type: " + type);
1951                typeAsString = null;
1952                break;
1953            }
1954        }
1955
1956        final List<String> parameterList = new ArrayList<String>();
1957        if (isPrimary) {
1958            parameterList.add(Constants.PARAM_TYPE_PREF);
1959        }
1960        if (!TextUtils.isEmpty(typeAsString)) {
1961            parameterList.add(typeAsString);
1962        }
1963
1964        appendVCardLineWithCharsetAndQPDetection(builder, Constants.PROPERTY_EMAIL,
1965                parameterList, rawData);
1966    }
1967
1968    private void appendVCardTelephoneLine(final StringBuilder builder,
1969            final Integer typeAsObject, final String label,
1970            final String encodedData, boolean isPrimary) {
1971        builder.append(Constants.PROPERTY_TEL);
1972        builder.append(VCARD_PARAM_SEPARATOR);
1973
1974        final int typeAsPrimitive;
1975        if (typeAsObject == null) {
1976            typeAsPrimitive = Phone.TYPE_OTHER;
1977        } else {
1978            typeAsPrimitive = typeAsObject;
1979        }
1980
1981        ArrayList<String> parameterList = new ArrayList<String>();
1982        switch (typeAsPrimitive) {
1983        case Phone.TYPE_HOME:
1984            parameterList.addAll(
1985                    Arrays.asList(Constants.PARAM_TYPE_HOME));
1986            break;
1987        case Phone.TYPE_WORK:
1988            parameterList.addAll(
1989                    Arrays.asList(Constants.PARAM_TYPE_WORK));
1990            break;
1991        case Phone.TYPE_FAX_HOME:
1992            parameterList.addAll(
1993                    Arrays.asList(Constants.PARAM_TYPE_HOME, Constants.PARAM_TYPE_FAX));
1994            break;
1995        case Phone.TYPE_FAX_WORK:
1996            parameterList.addAll(
1997                    Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_FAX));
1998            break;
1999        case Phone.TYPE_MOBILE:
2000            parameterList.add(Constants.PARAM_TYPE_CELL);
2001            break;
2002        case Phone.TYPE_PAGER:
2003            if (mIsDoCoMo) {
2004                // Not sure about the reason, but previous implementation had
2005                // used "VOICE" instead of "PAGER"
2006                parameterList.add(Constants.PARAM_TYPE_VOICE);
2007            } else {
2008                parameterList.add(Constants.PARAM_TYPE_PAGER);
2009            }
2010            break;
2011        case Phone.TYPE_OTHER:
2012            parameterList.add(Constants.PARAM_TYPE_VOICE);
2013            break;
2014        case Phone.TYPE_CAR:
2015            parameterList.add(Constants.PARAM_TYPE_CAR);
2016            break;
2017        case Phone.TYPE_COMPANY_MAIN:
2018            // There's no relevant field in vCard (at least 2.1).
2019            parameterList.add(Constants.PARAM_TYPE_WORK);
2020            isPrimary = true;
2021            break;
2022        case Phone.TYPE_ISDN:
2023            parameterList.add(Constants.PARAM_TYPE_ISDN);
2024            break;
2025        case Phone.TYPE_MAIN:
2026            isPrimary = true;
2027            break;
2028        case Phone.TYPE_OTHER_FAX:
2029            parameterList.add(Constants.PARAM_TYPE_FAX);
2030            break;
2031        case Phone.TYPE_TELEX:
2032            parameterList.add(Constants.PARAM_TYPE_TLX);
2033            break;
2034        case Phone.TYPE_WORK_MOBILE:
2035            parameterList.addAll(
2036                    Arrays.asList(Constants.PARAM_TYPE_WORK, Constants.PARAM_TYPE_CELL));
2037            break;
2038        case Phone.TYPE_WORK_PAGER:
2039            parameterList.add(Constants.PARAM_TYPE_WORK);
2040            // See above.
2041            if (mIsDoCoMo) {
2042                parameterList.add(Constants.PARAM_TYPE_VOICE);
2043            } else {
2044                parameterList.add(Constants.PARAM_TYPE_PAGER);
2045            }
2046            break;
2047        case Phone.TYPE_MMS:
2048            parameterList.add(Constants.PARAM_TYPE_MSG);
2049            break;
2050        case Phone.TYPE_CUSTOM:
2051            if (mUsesAndroidProperty && !TextUtils.isEmpty(label)
2052                        && VCardUtils.containsOnlyAlphaDigitHyphen(label)) {
2053                // Note: Strictly, vCard 2.1 does not allow "X-" parameter without
2054                //       "TYPE=" string.
2055                parameterList.add("X-" + label);
2056            } else {
2057                // Just ignore the custom type.
2058                parameterList.add(Constants.PARAM_TYPE_VOICE);
2059            }
2060            break;
2061        case Phone.TYPE_RADIO:
2062        case Phone.TYPE_TTY_TDD:
2063        default:
2064            break;
2065        }
2066
2067        if (isPrimary) {
2068            parameterList.add(Constants.PARAM_TYPE_PREF);
2069        }
2070
2071        if (parameterList.isEmpty()) {
2072            appendUncommonPhoneType(builder, typeAsPrimitive);
2073        } else {
2074            appendTypeParameters(builder, parameterList);
2075        }
2076
2077        builder.append(VCARD_DATA_SEPARATOR);
2078        builder.append(encodedData);
2079        builder.append(VCARD_END_OF_LINE);
2080    }
2081
2082    /**
2083     * Appends phone type string which may not be available in some devices.
2084     */
2085    private void appendUncommonPhoneType(final StringBuilder builder, final Integer type) {
2086        if (mIsDoCoMo) {
2087            // The previous implementation for DoCoMo had been conservative
2088            // about miscellaneous types.
2089            builder.append(Constants.PARAM_TYPE_VOICE);
2090        } else {
2091            String phoneType = VCardUtils.getPhoneTypeString(type);
2092            if (phoneType != null) {
2093                appendTypeParameter(builder, phoneType);
2094            } else {
2095                Log.e(LOG_TAG, "Unknown or unsupported (by vCard) Phone type: " + type);
2096            }
2097        }
2098    }
2099
2100    // appendVCardLine() variants accepting one String.
2101
2102    private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder,
2103            final String propertyName, final String rawData) {
2104        appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawData);
2105    }
2106
2107    private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder,
2108            final String propertyName,
2109            final List<String> parameterList, final String rawData) {
2110        final boolean needCharset =
2111            (mUsesQuotedPrintable && !VCardUtils.containsOnlyPrintableAscii(rawData));
2112        final boolean reallyUseQuotedPrintable =
2113            !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData);
2114        appendVCardLine(builder, propertyName, parameterList,
2115                rawData, needCharset, reallyUseQuotedPrintable);
2116    }
2117
2118    private void appendVCardLine(final StringBuilder builder,
2119            final String propertyName, final String rawData) {
2120        appendVCardLine(builder, propertyName, rawData, false, false);
2121    }
2122
2123    private void appendVCardLine(final StringBuilder builder,
2124            final String propertyName, final String rawData, final boolean needCharset,
2125            boolean needQuotedPrintable) {
2126        appendVCardLine(builder, propertyName, null, rawData, needCharset, needQuotedPrintable);
2127    }
2128
2129    private void appendVCardLine(final StringBuilder builder,
2130            final String propertyName,
2131            final List<String> parameterList,
2132            final String rawData, final boolean needCharset,
2133            boolean needQuotedPrintable) {
2134        builder.append(propertyName);
2135        if (parameterList != null && parameterList.size() > 0) {
2136            builder.append(VCARD_PARAM_SEPARATOR);
2137            appendTypeParameters(builder, parameterList);
2138        }
2139        if (needCharset) {
2140            builder.append(VCARD_PARAM_SEPARATOR);
2141            builder.append(mVCardCharsetParameter);
2142        }
2143
2144        final String encodedData;
2145        if (needQuotedPrintable) {
2146            builder.append(VCARD_PARAM_SEPARATOR);
2147            builder.append(VCARD_PARAM_ENCODING_QP);
2148            encodedData = encodeQuotedPrintable(rawData);
2149        } else {
2150            // TODO: one line may be too huge, which may be invalid in vCard spec, though
2151            //       several (even well-known) applications do not care this.
2152            encodedData = escapeCharacters(rawData);
2153        }
2154
2155        builder.append(VCARD_DATA_SEPARATOR);
2156        builder.append(encodedData);
2157        builder.append(VCARD_END_OF_LINE);
2158    }
2159
2160    // appendVCardLine() variants accepting List<String>.
2161
2162    private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder,
2163            final String propertyName, final List<String> rawDataList) {
2164        appendVCardLineWithCharsetAndQPDetection(builder, propertyName, null, rawDataList);
2165    }
2166
2167    private void appendVCardLineWithCharsetAndQPDetection(final StringBuilder builder,
2168            final String propertyName,
2169            final List<String> parameterList, final List<String> rawDataList) {
2170        boolean needCharset = false;
2171        boolean reallyUseQuotedPrintable = false;
2172        for (String rawData : rawDataList) {
2173            if (!needCharset && mUsesQuotedPrintable &&
2174                    !VCardUtils.containsOnlyPrintableAscii(rawData)) {
2175                needCharset = true;
2176            }
2177            if (!reallyUseQuotedPrintable &&
2178                    !VCardUtils.containsOnlyNonCrLfPrintableAscii(rawData)) {
2179                reallyUseQuotedPrintable = true;
2180            }
2181            if (needCharset && reallyUseQuotedPrintable) {
2182                break;
2183            }
2184        }
2185
2186        appendVCardLine(builder, propertyName, parameterList,
2187                rawDataList, needCharset, reallyUseQuotedPrintable);
2188    }
2189
2190    /*
2191    private void appendVCardLine(final StringBuilder builder,
2192            final String propertyName, final List<String> rawDataList) {
2193        appendVCardLine(builder, propertyName, rawDataList, false, false);
2194    }
2195
2196    private void appendVCardLine(final StringBuilder builder,
2197            final String propertyName, final List<String> rawDataList,
2198            final boolean needCharset, boolean needQuotedPrintable) {
2199        appendVCardLine(builder, propertyName, null, rawDataList, needCharset, needQuotedPrintable);
2200    }*/
2201
2202    private void appendVCardLine(final StringBuilder builder,
2203            final String propertyName,
2204            final List<String> parameterList,
2205            final List<String> rawDataList, final boolean needCharset,
2206            boolean needQuotedPrintable) {
2207        builder.append(propertyName);
2208        if (parameterList != null && parameterList.size() > 0) {
2209            builder.append(VCARD_PARAM_SEPARATOR);
2210            appendTypeParameters(builder, parameterList);
2211        }
2212        if (needCharset) {
2213            builder.append(VCARD_PARAM_SEPARATOR);
2214            builder.append(mVCardCharsetParameter);
2215        }
2216
2217        builder.append(VCARD_DATA_SEPARATOR);
2218        boolean first = true;
2219        for (String rawData : rawDataList) {
2220            final String encodedData;
2221            if (needQuotedPrintable) {
2222                builder.append(VCARD_PARAM_SEPARATOR);
2223                builder.append(VCARD_PARAM_ENCODING_QP);
2224                encodedData = encodeQuotedPrintable(rawData);
2225            } else {
2226                // TODO: one line may be too huge, which may be invalid in vCard spec, though
2227                //       several (even well-known) applications do not care this.
2228                encodedData = escapeCharacters(rawData);
2229            }
2230
2231            if (first) {
2232                first = false;
2233            } else {
2234                builder.append(VCARD_ITEM_SEPARATOR);
2235            }
2236            builder.append(encodedData);
2237        }
2238        builder.append(VCARD_END_OF_LINE);
2239    }
2240
2241    /**
2242     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2243     */
2244    private void appendTypeParameters(final StringBuilder builder,
2245            final List<String> types) {
2246        // We may have to make this comma separated form like "TYPE=DOM,WORK" in the future,
2247        // which would be recommended way in vcard 3.0 though not valid in vCard 2.1.
2248        boolean first = true;
2249        for (String type : types) {
2250            if (first) {
2251                first = false;
2252            } else {
2253                builder.append(VCARD_PARAM_SEPARATOR);
2254            }
2255            appendTypeParameter(builder, type);
2256        }
2257    }
2258
2259    /**
2260     * VCARD_PARAM_SEPARATOR must be appended before this method being called.
2261     */
2262    private void appendTypeParameter(final StringBuilder builder, final String type) {
2263        // Refrain from using appendType() so that "TYPE=" is not be appended when the
2264        // device is DoCoMo's (just for safety).
2265        //
2266        // Note: In vCard 3.0, Type strings also can be like this: "TYPE=HOME,PREF"
2267        if ((mIsV30 || mAppendTypeParamName) && !mIsDoCoMo) {
2268            builder.append(Constants.PARAM_TYPE).append(VCARD_PARAM_EQUAL);
2269        }
2270        builder.append(type);
2271    }
2272
2273    /**
2274     * Returns true when the property line should contain charset parameter
2275     * information. This method may return true even when vCard version is 3.0.
2276     *
2277     * Strictly, adding charset information is invalid in VCard 3.0.
2278     * However we'll add the info only when charset we use is not UTF-8
2279     * in vCard 3.0 format, since parser side may be able to use the charset
2280     * via this field, though we may encounter another problem by adding it.
2281     *
2282     * e.g. Japanese mobile phones use Shift_Jis while RFC 2426
2283     * recommends UTF-8. By adding this field, parsers may be able
2284     * to know this text is NOT UTF-8 but Shift_Jis.
2285     */
2286    private boolean shouldAppendCharsetParameter(final String propertyValue) {
2287        return (!(mIsV30 && mUsesUtf8) && !VCardUtils.containsOnlyPrintableAscii(propertyValue));
2288    }
2289
2290    private boolean shouldAppendCharsetParameters(final List<String> propertyValueList) {
2291        if (mIsV30 && mUsesUtf8) {
2292            return false;
2293        }
2294        for (String propertyValue : propertyValueList) {
2295            if (!VCardUtils.containsOnlyPrintableAscii(propertyValue)) {
2296                return true;
2297            }
2298        }
2299        return false;
2300    }
2301
2302    private String encodeQuotedPrintable(String str) {
2303        if (TextUtils.isEmpty(str)) {
2304            return "";
2305        }
2306        {
2307            // Replace "\n" and "\r" with "\r\n".
2308            StringBuilder tmpBuilder = new StringBuilder();
2309            int length = str.length();
2310            for (int i = 0; i < length; i++) {
2311                char ch = str.charAt(i);
2312                if (ch == '\r') {
2313                    if (i + 1 < length && str.charAt(i + 1) == '\n') {
2314                        i++;
2315                    }
2316                    tmpBuilder.append("\r\n");
2317                } else if (ch == '\n') {
2318                    tmpBuilder.append("\r\n");
2319                } else {
2320                    tmpBuilder.append(ch);
2321                }
2322            }
2323            str = tmpBuilder.toString();
2324        }
2325
2326        final StringBuilder tmpBuilder = new StringBuilder();
2327        int index = 0;
2328        int lineCount = 0;
2329        byte[] strArray = null;
2330
2331        try {
2332            strArray = str.getBytes(mCharsetString);
2333        } catch (UnsupportedEncodingException e) {
2334            Log.e(LOG_TAG, "Charset " + mCharsetString + " cannot be used. "
2335                    + "Try default charset");
2336            strArray = str.getBytes();
2337        }
2338        while (index < strArray.length) {
2339            tmpBuilder.append(String.format("=%02X", strArray[index]));
2340            index += 1;
2341            lineCount += 3;
2342
2343            if (lineCount >= 67) {
2344                // Specification requires CRLF must be inserted before the
2345                // length of the line
2346                // becomes more than 76.
2347                // Assuming that the next character is a multi-byte character,
2348                // it will become
2349                // 6 bytes.
2350                // 76 - 6 - 3 = 67
2351                tmpBuilder.append("=\r\n");
2352                lineCount = 0;
2353            }
2354        }
2355
2356        return tmpBuilder.toString();
2357    }
2358
2359    //// The methods bellow are for call log history ////
2360
2361    /**
2362     * This static function is to compose vCard for phone own number
2363     */
2364    public String composeVCardForPhoneOwnNumber(int phonetype, String phoneName,
2365            String phoneNumber, boolean vcardVer21) {
2366        final StringBuilder builder = new StringBuilder();
2367        appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
2368        if (!vcardVer21) {
2369            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30);
2370        } else {
2371            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21);
2372        }
2373
2374        boolean needCharset = false;
2375        if (!(VCardUtils.containsOnlyPrintableAscii(phoneName))) {
2376            needCharset = true;
2377        }
2378        appendVCardLine(builder, Constants.PROPERTY_FN, phoneName, needCharset, false);
2379        appendVCardLine(builder, Constants.PROPERTY_N, phoneName, needCharset, false);
2380
2381        String label = Integer.toString(phonetype);
2382        appendVCardTelephoneLine(builder, phonetype, label, phoneNumber, false);
2383
2384        appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD);
2385
2386        return builder.toString();
2387    }
2388
2389    /**
2390     * Format according to RFC 2445 DATETIME type.
2391     * The format is: ("%Y%m%dT%H%M%SZ").
2392     */
2393    private final String toRfc2455Format(final long millSecs) {
2394        Time startDate = new Time();
2395        startDate.set(millSecs);
2396        String date = startDate.format2445();
2397        return date + FLAG_TIMEZONE_UTC;
2398    }
2399
2400    /**
2401     * Try to append the property line for a call history time stamp field if possible.
2402     * Do nothing if the call log type gotton from the database is invalid.
2403     */
2404    private void tryAppendCallHistoryTimeStampField(final StringBuilder builder) {
2405        // Extension for call history as defined in
2406        // in the Specification for Ic Mobile Communcation - ver 1.1,
2407        // Oct 2000. This is used to send the details of the call
2408        // history - missed, incoming, outgoing along with date and time
2409        // to the requesting device (For example, transferring phone book
2410        // when connected over bluetooth)
2411        //
2412        // e.g. "X-IRMC-CALL-DATETIME;MISSED:20050320T100000Z"
2413        final int callLogType = mCursor.getInt(CALL_TYPE_COLUMN_INDEX);
2414        final String callLogTypeStr;
2415        switch (callLogType) {
2416            case Calls.INCOMING_TYPE: {
2417                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_INCOMING;
2418                break;
2419            }
2420            case Calls.OUTGOING_TYPE: {
2421                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_OUTGOING;
2422                break;
2423            }
2424            case Calls.MISSED_TYPE: {
2425                callLogTypeStr = VCARD_PROPERTY_CALLTYPE_MISSED;
2426                break;
2427            }
2428            default: {
2429                Log.w(LOG_TAG, "Call log type not correct.");
2430                return;
2431            }
2432        }
2433
2434        final long dateAsLong = mCursor.getLong(DATE_COLUMN_INDEX);
2435        builder.append(VCARD_PROPERTY_X_TIMESTAMP);
2436        builder.append(VCARD_PARAM_SEPARATOR);
2437        appendTypeParameter(builder, callLogTypeStr);
2438        builder.append(VCARD_DATA_SEPARATOR);
2439        builder.append(toRfc2455Format(dateAsLong));
2440        builder.append(VCARD_END_OF_LINE);
2441    }
2442
2443    private String createOneCallLogEntryInternal() {
2444        final StringBuilder builder = new StringBuilder();
2445        appendVCardLine(builder, Constants.PROPERTY_BEGIN, VCARD_DATA_VCARD);
2446        if (mIsV30) {
2447            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V30);
2448        } else {
2449            appendVCardLine(builder, Constants.PROPERTY_VERSION, Constants.VERSION_V21);
2450        }
2451        String name = mCursor.getString(CALLER_NAME_COLUMN_INDEX);
2452        if (TextUtils.isEmpty(name)) {
2453            name = mCursor.getString(NUMBER_COLUMN_INDEX);
2454        }
2455        final boolean needCharset = !(VCardUtils.containsOnlyPrintableAscii(name));
2456        appendVCardLine(builder, Constants.PROPERTY_FN, name, needCharset, false);
2457        appendVCardLine(builder, Constants.PROPERTY_N, name, needCharset, false);
2458
2459        String number = mCursor.getString(NUMBER_COLUMN_INDEX);
2460        int type = mCursor.getInt(CALLER_NUMBERTYPE_COLUMN_INDEX);
2461        String label = mCursor.getString(CALLER_NUMBERLABEL_COLUMN_INDEX);
2462        if (TextUtils.isEmpty(label)) {
2463            label = Integer.toString(type);
2464        }
2465        appendVCardTelephoneLine(builder, type, label, number, false);
2466        tryAppendCallHistoryTimeStampField(builder);
2467        appendVCardLine(builder, Constants.PROPERTY_END, VCARD_DATA_VCARD);
2468        return builder.toString();
2469    }
2470}
2471