BluetoothPbapVcardManager.java revision 1321cd814a7406260ba6282d9c71e1a2d8925262
1/*
2 * Copyright (c) 2008-2009, Motorola, Inc.
3 * Copyright (C) 2009-2012, Broadcom Corporation
4 *
5 * All rights reserved.
6 *
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
9 *
10 * - Redistributions of source code must retain the above copyright notice,
11 * this list of conditions and the following disclaimer.
12 *
13 * - Redistributions in binary form must reproduce the above copyright notice,
14 * this list of conditions and the following disclaimer in the documentation
15 * and/or other materials provided with the distribution.
16 *
17 * - Neither the name of the Motorola, Inc. nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 */
33
34package com.android.bluetooth.pbap;
35
36import android.content.ContentResolver;
37import android.content.Context;
38import android.database.CursorWindowAllocationException;
39import android.database.Cursor;
40import android.net.Uri;
41import android.provider.CallLog;
42import android.provider.CallLog.Calls;
43import android.provider.ContactsContract.CommonDataKinds;
44import android.provider.ContactsContract.Contacts;
45import android.provider.ContactsContract.Data;
46import android.provider.ContactsContract.CommonDataKinds.Phone;
47import android.provider.ContactsContract.PhoneLookup;
48import android.telephony.PhoneNumberUtils;
49import android.text.TextUtils;
50import android.util.Log;
51
52import com.android.bluetooth.R;
53import com.android.vcard.VCardComposer;
54import com.android.vcard.VCardConfig;
55import com.android.vcard.VCardPhoneNumberTranslationCallback;
56
57import java.io.IOException;
58import java.io.OutputStream;
59import java.util.ArrayList;
60
61import javax.obex.ServerOperation;
62import javax.obex.Operation;
63import javax.obex.ResponseCodes;
64
65import com.android.bluetooth.Utils;
66
67public class BluetoothPbapVcardManager {
68    private static final String TAG = "BluetoothPbapVcardManager";
69
70    private static final boolean V = BluetoothPbapService.VERBOSE;
71
72    private ContentResolver mResolver;
73
74    private Context mContext;
75
76    static final String[] PHONES_PROJECTION = new String[] {
77            Data._ID, // 0
78            CommonDataKinds.Phone.TYPE, // 1
79            CommonDataKinds.Phone.LABEL, // 2
80            CommonDataKinds.Phone.NUMBER, // 3
81            Contacts.DISPLAY_NAME, // 4
82    };
83
84    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
85
86    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
87
88    static final String[] CONTACTS_PROJECTION = new String[] {
89            Contacts._ID, // 0
90            Contacts.DISPLAY_NAME, // 1
91    };
92
93    static final int CONTACTS_ID_COLUMN_INDEX = 0;
94
95    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
96
97    // call histories use dynamic handles, and handles should order by date; the
98    // most recently one should be the first handle. In table "calls", _id and
99    // date are consistent in ordering, to implement simply, we sort by _id
100    // here.
101    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
102
103    private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
104
105    public BluetoothPbapVcardManager(final Context context) {
106        mContext = context;
107        mResolver = mContext.getContentResolver();
108    }
109
110    /**
111     * Create an owner vcard from the configured profile
112     * @param vcardType21
113     * @return
114     */
115    private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) {
116        // Currently only support Generic Vcard 2.1 and 3.0
117        int vcardType;
118        if (vcardType21) {
119            vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
120        } else {
121            vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
122        }
123
124        if (!BluetoothPbapConfig.includePhotosInVcard()) {
125            vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
126        }
127
128        return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter);
129    }
130
131    public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) {
132        //Owner vCard enhancement: Use "ME" profile if configured
133        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
134            String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter);
135            if (vcard != null && vcard.length() != 0) {
136                return vcard;
137            }
138        }
139        //End enhancement
140
141        BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext);
142        String name = BluetoothPbapService.getLocalPhoneName();
143        String number = BluetoothPbapService.getLocalPhoneNum();
144        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
145                vcardType21);
146        return vcard;
147    }
148
149    public final int getPhonebookSize(final int type) {
150        int size;
151        switch (type) {
152            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
153                size = getContactsSize();
154                break;
155            default:
156                size = getCallHistorySize(type);
157                break;
158        }
159        if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type);
160        return size;
161    }
162
163    public final int getContactsSize() {
164        final Uri myUri = Contacts.CONTENT_URI;
165        int size = 0;
166        Cursor contactCursor = null;
167        try {
168            contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null);
169            if (contactCursor != null) {
170                size = contactCursor.getCount() + 1; // always has the 0.vcf
171            }
172        } catch (CursorWindowAllocationException e) {
173            Log.e(TAG, "CursorWindowAllocationException while getting Contacts size");
174        } finally {
175            if (contactCursor != null) {
176                contactCursor.close();
177                contactCursor = null;
178            }
179        }
180        return size;
181    }
182
183    public final int getCallHistorySize(final int type) {
184        final Uri myUri = CallLog.Calls.CONTENT_URI;
185        String selection = BluetoothPbapObexServer.createSelectionPara(type);
186        int size = 0;
187        Cursor callCursor = null;
188        try {
189            callCursor = mResolver.query(myUri, null, selection, null,
190                    CallLog.Calls.DEFAULT_SORT_ORDER);
191            if (callCursor != null) {
192                size = callCursor.getCount();
193            }
194        } catch (CursorWindowAllocationException e) {
195            Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size");
196        } finally {
197            if (callCursor != null) {
198                callCursor.close();
199                callCursor = null;
200            }
201        }
202        return size;
203    }
204
205    public final ArrayList<String> loadCallHistoryList(final int type) {
206        final Uri myUri = CallLog.Calls.CONTENT_URI;
207        String selection = BluetoothPbapObexServer.createSelectionPara(type);
208        String[] projection = new String[] {
209                Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION
210        };
211        final int CALLS_NUMBER_COLUMN_INDEX = 0;
212        final int CALLS_NAME_COLUMN_INDEX = 1;
213        final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2;
214
215        Cursor callCursor = null;
216        ArrayList<String> list = new ArrayList<String>();
217        try {
218            callCursor = mResolver.query(myUri, projection, selection, null,
219                    CALLLOG_SORT_ORDER);
220            if (callCursor != null) {
221                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
222                        callCursor.moveToNext()) {
223                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
224                    if (TextUtils.isEmpty(name)) {
225                        // name not found, use number instead
226                        final int numberPresentation = callCursor.getInt(
227                                CALLS_NUMBER_PRESENTATION_COLUMN_INDEX);
228                        if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
229                            name = mContext.getString(R.string.unknownNumber);
230                        } else {
231                            name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
232                        }
233                    }
234                    list.add(name);
235                }
236            }
237        } catch (CursorWindowAllocationException e) {
238            Log.e(TAG, "CursorWindowAllocationException while loading CallHistory");
239        } finally {
240            if (callCursor != null) {
241                callCursor.close();
242                callCursor = null;
243            }
244        }
245        return list;
246    }
247
248    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
249        ArrayList<String> nameList = new ArrayList<String>();
250        //Owner vCard enhancement. Use "ME" profile if configured
251        String ownerName = null;
252        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
253            ownerName = BluetoothPbapUtils.getProfileName(mContext);
254        }
255        if (ownerName == null || ownerName.length()==0) {
256            ownerName = BluetoothPbapService.getLocalPhoneName();
257        }
258        nameList.add(ownerName);
259        //End enhancement
260
261        final Uri myUri = Contacts.CONTENT_URI;
262        Cursor contactCursor = null;
263        try {
264            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
265                if (V) Log.v(TAG, "getPhonebookNameList, order by index");
266                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
267                        null, Contacts._ID);
268            } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
269                if (V) Log.v(TAG, "getPhonebookNameList, order by alpha");
270                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
271                        null, Contacts.DISPLAY_NAME);
272            }
273            if (contactCursor != null) {
274                for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
275                        .moveToNext()) {
276                    String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
277                    long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
278                    if (TextUtils.isEmpty(name)) {
279                        name = mContext.getString(android.R.string.unknownName);
280                    }
281                    nameList.add(name + "," + id);
282                }
283            }
284        } catch (CursorWindowAllocationException e) {
285            Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list");
286        } finally {
287            if (contactCursor != null) {
288                contactCursor.close();
289                contactCursor = null;
290            }
291        }
292        return nameList;
293    }
294
295    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
296        ArrayList<String> nameList = new ArrayList<String>();
297        ArrayList<String> tempNameList = new ArrayList<String>();
298
299        Cursor contactCursor = null;
300        Uri uri = null;
301
302        if (phoneNumber != null && phoneNumber.length() == 0) {
303            uri = Contacts.CONTENT_URI;
304        } else {
305            uri = Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
306                Uri.encode(phoneNumber));
307        }
308
309        try {
310            contactCursor = mResolver.query(uri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
311                        null, Contacts._ID);
312
313            if (contactCursor != null) {
314                for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
315                        .moveToNext()) {
316                    String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
317                    long id = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
318                    if (TextUtils.isEmpty(name)) {
319                        name = mContext.getString(android.R.string.unknownName);
320                    }
321                    if (V) Log.v(TAG, "got name " + name + " by number " + phoneNumber + " @" + id);
322                    tempNameList.add(name + "," + id);
323                }
324            }
325        } catch (CursorWindowAllocationException e) {
326            Log.e(TAG, "CursorWindowAllocationException while getting contact names");
327        } finally {
328            if (contactCursor != null) {
329                contactCursor.close();
330                contactCursor = null;
331            }
332        }
333        int tempListSize = tempNameList.size();
334        for (int index = 0; index < tempListSize; index++) {
335            String object = tempNameList.get(index);
336            if (!nameList.contains(object))
337                nameList.add(object);
338        }
339
340        return nameList;
341    }
342
343    public final int composeAndSendCallLogVcards(final int type, Operation op,
344            final int startPoint, final int endPoint, final boolean vcardType21) {
345        if (startPoint < 1 || startPoint > endPoint) {
346            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
347            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
348        }
349        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
350
351        final Uri myUri = CallLog.Calls.CONTENT_URI;
352        final String[] CALLLOG_PROJECTION = new String[] {
353            CallLog.Calls._ID, // 0
354        };
355        final int ID_COLUMN_INDEX = 0;
356
357        Cursor callsCursor = null;
358        long startPointId = 0;
359        long endPointId = 0;
360        try {
361            // Need test to see if order by _ID is ok here, or by date?
362            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
363                    CALLLOG_SORT_ORDER);
364            if (callsCursor != null) {
365                callsCursor.moveToPosition(startPoint - 1);
366                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
367                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
368                if (startPoint == endPoint) {
369                    endPointId = startPointId;
370                } else {
371                    callsCursor.moveToPosition(endPoint - 1);
372                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
373                }
374                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
375            }
376        } catch (CursorWindowAllocationException e) {
377            Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards");
378        } finally {
379            if (callsCursor != null) {
380                callsCursor.close();
381                callsCursor = null;
382            }
383        }
384
385        String recordSelection;
386        if (startPoint == endPoint) {
387            recordSelection = Calls._ID + "=" + startPointId;
388        } else {
389            // The query to call table is by "_id DESC" order, so change
390            // correspondingly.
391            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
392                    + startPointId;
393        }
394
395        String selection;
396        if (typeSelection == null) {
397            selection = recordSelection;
398        } else {
399            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
400        }
401
402        if (V) Log.v(TAG, "Call log query selection is: " + selection);
403
404        return composeAndSendVCards(op, selection, vcardType21, null, false);
405    }
406
407    public final int composeAndSendPhonebookVcards(Operation op, final int startPoint,
408            final int endPoint, final boolean vcardType21, String ownerVCard) {
409        if (startPoint < 1 || startPoint > endPoint) {
410            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
411            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
412        }
413        final Uri myUri = Contacts.CONTENT_URI;
414
415        Cursor contactCursor = null;
416        long startPointId = 0;
417        long endPointId = 0;
418        try {
419            contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null,
420                    Contacts._ID);
421            if (contactCursor != null) {
422                contactCursor.moveToPosition(startPoint - 1);
423                startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
424                if (V) Log.v(TAG, "Query startPointId = " + startPointId);
425                if (startPoint == endPoint) {
426                    endPointId = startPointId;
427                } else {
428                    contactCursor.moveToPosition(endPoint - 1);
429                    endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
430                }
431                if (V) Log.v(TAG, "Query endPointId = " + endPointId);
432            }
433        } catch (CursorWindowAllocationException e) {
434            Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards");
435        } finally {
436            if (contactCursor != null) {
437                contactCursor.close();
438                contactCursor = null;
439            }
440        }
441
442        final String selection;
443        if (startPoint == endPoint) {
444            selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE;
445        } else {
446            selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<="
447                    + endPointId + " AND " + CLAUSE_ONLY_VISIBLE;
448        }
449
450        if (V) Log.v(TAG, "Query selection is: " + selection);
451
452        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
453    }
454
455    public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
456            final boolean vcardType21, String ownerVCard, int orderByWhat) {
457        if (offset < 1) {
458            Log.e(TAG, "Internal error: offset is not correct.");
459            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
460        }
461        final Uri myUri = Contacts.CONTENT_URI;
462        Cursor contactCursor = null;
463        String selection = null;
464        long contactId = 0;
465        if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
466            try {
467                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
468                        null, Contacts._ID);
469                if (contactCursor != null) {
470                    contactCursor.moveToPosition(offset - 1);
471                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
472                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
473                }
474            } catch (CursorWindowAllocationException e) {
475                Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard order by index");
476            } finally {
477                if (contactCursor != null) {
478                    contactCursor.close();
479                    contactCursor = null;
480                }
481            }
482        } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
483            try {
484                contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
485                        null, Contacts.DISPLAY_NAME);
486                if (contactCursor != null) {
487                    contactCursor.moveToPosition(offset - 1);
488                    contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
489                    if (V) Log.v(TAG, "Query startPointId = " + contactId);
490                }
491            } catch (CursorWindowAllocationException e) {
492                Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard order by alphabetical");
493            } finally {
494                if (contactCursor != null) {
495                    contactCursor.close();
496                    contactCursor = null;
497                }
498            }
499        } else {
500            Log.e(TAG, "Parameter orderByWhat is not supported!");
501            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
502        }
503        selection = Contacts._ID + "=" + contactId;
504
505        if (V) Log.v(TAG, "Query selection is: " + selection);
506
507        return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
508    }
509
510    public final int composeAndSendVCards(Operation op, final String selection,
511            final boolean vcardType21, String ownerVCard, boolean isContacts) {
512        long timestamp = 0;
513        if (V) timestamp = System.currentTimeMillis();
514
515        if (isContacts) {
516            VCardComposer composer = null;
517            HandlerForStringBuffer buffer = null;
518            try {
519                // Currently only support Generic Vcard 2.1 and 3.0
520                int vcardType;
521                if (vcardType21) {
522                    vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
523                } else {
524                    vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
525                }
526
527                if (!BluetoothPbapConfig.includePhotosInVcard()) {
528                    vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
529                }
530
531                //Enhancement: customize Vcard based on preferences/settings and input from caller
532                composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType,null);
533                //End enhancement
534
535                // BT does want PAUSE/WAIT conversion while it doesn't want the other formatting
536                // done by vCard library by default.
537                composer.setPhoneNumberTranslationCallback(
538                        new VCardPhoneNumberTranslationCallback() {
539                            public String onValueReceived(
540                                    String rawValue, int type, String label, boolean isPrimary) {
541                                // 'p' and 'w' are the standard characters for pause and wait
542                                // (see RFC 3601)
543                                // so use those when exporting phone numbers via vCard.
544                                String numberWithControlSequence = rawValue
545                                        .replace(PhoneNumberUtils.PAUSE, 'p')
546                                        .replace(PhoneNumberUtils.WAIT, 'w');
547                                return numberWithControlSequence;
548                            }
549                        });
550                buffer = new HandlerForStringBuffer(op, ownerVCard);
551                if (!composer.init(Contacts.CONTENT_URI, selection, null, Contacts._ID) ||
552                        !buffer.onInit(mContext)) {
553                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
554                }
555
556                while (!composer.isAfterLast()) {
557                    if (BluetoothPbapObexServer.sIsAborted) {
558                        ((ServerOperation)op).isAborted = true;
559                        BluetoothPbapObexServer.sIsAborted = false;
560                        break;
561                    }
562                    String vcard = composer.createOneEntry();
563                    if (vcard == null) {
564                        Log.e(TAG, "Failed to read a contact. Error reason: "
565                                + composer.getErrorReason());
566                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
567                    }
568                    if (V) {
569                        Log.v(TAG, "Vcard Entry:");
570                        Log.v(TAG,vcard);
571                    }
572
573                    if (!buffer.onEntryCreated(vcard)) {
574                        // onEntryCreate() already emits error.
575                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
576                    }
577                }
578            } finally {
579                if (composer != null) {
580                    composer.terminate();
581                }
582                if (buffer != null) {
583                    buffer.onTerminate();
584                }
585            }
586        } else { // CallLog
587            BluetoothPbapCallLogComposer composer = null;
588            HandlerForStringBuffer buffer = null;
589            try {
590
591                composer = new BluetoothPbapCallLogComposer(mContext);
592                buffer = new HandlerForStringBuffer(op, ownerVCard);
593                if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null,
594                                   CALLLOG_SORT_ORDER) ||
595                                   !buffer.onInit(mContext)) {
596                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
597                }
598
599                while (!composer.isAfterLast()) {
600                    if (BluetoothPbapObexServer.sIsAborted) {
601                        ((ServerOperation)op).isAborted = true;
602                        BluetoothPbapObexServer.sIsAborted = false;
603                        break;
604                    }
605                    String vcard = composer.createOneEntry(vcardType21);
606                    if (vcard == null) {
607                        Log.e(TAG, "Failed to read a contact. Error reason: "
608                                + composer.getErrorReason());
609                        return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
610                    }
611                    if (V) {
612                        Log.v(TAG, "Vcard Entry:");
613                        Log.v(TAG,vcard);
614                    }
615
616                    buffer.onEntryCreated(vcard);
617                }
618            } finally {
619                if (composer != null) {
620                    composer.terminate();
621                }
622                if (buffer != null) {
623                    buffer.onTerminate();
624                }
625            }
626        }
627
628        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
629                    + (System.currentTimeMillis() - timestamp) + " ms");
630
631        return ResponseCodes.OBEX_HTTP_OK;
632    }
633
634    /**
635     * Handler to emit vCards to PCE.
636     */
637    public class HandlerForStringBuffer {
638        private Operation operation;
639
640        private OutputStream outputStream;
641
642        private String phoneOwnVCard = null;
643
644        public HandlerForStringBuffer(Operation op, String ownerVCard) {
645            operation = op;
646            if (ownerVCard != null) {
647                phoneOwnVCard = ownerVCard;
648                if (V) Log.v(TAG, "phone own number vcard:");
649                if (V) Log.v(TAG, phoneOwnVCard);
650            }
651        }
652
653        private boolean write(String vCard) {
654            try {
655                if (vCard != null) {
656                    outputStream.write(vCard.getBytes());
657                    return true;
658                }
659            } catch (IOException e) {
660                Log.e(TAG, "write outputstrem failed" + e.toString());
661            }
662            return false;
663        }
664
665        public boolean onInit(Context context) {
666            try {
667                outputStream = operation.openOutputStream();
668                if (phoneOwnVCard != null) {
669                    return write(phoneOwnVCard);
670                }
671                return true;
672            } catch (IOException e) {
673                Log.e(TAG, "open outputstrem failed" + e.toString());
674            }
675            return false;
676        }
677
678        public boolean onEntryCreated(String vcard) {
679            return write(vcard);
680        }
681
682        public void onTerminate() {
683            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
684                if (V) Log.v(TAG, "CloseStream failed!");
685            } else {
686                if (V) Log.v(TAG, "CloseStream ok!");
687            }
688        }
689    }
690}
691