BluetoothPbapVcardManager.java revision 4ca05d6f02264b0eb2983e43d34ca4051434a4d4
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 com.android.bluetooth.R;
37import com.android.bluetooth.util.DevicePolicyUtils;
38import com.android.vcard.VCardComposer;
39import com.android.vcard.VCardConfig;
40import com.android.vcard.VCardPhoneNumberTranslationCallback;
41
42import android.content.ContentResolver;
43import android.content.Context;
44import android.database.Cursor;
45import android.database.CursorWindowAllocationException;
46import android.database.MatrixCursor;
47import android.net.Uri;
48import android.provider.CallLog;
49import android.provider.CallLog.Calls;
50import android.provider.ContactsContract.CommonDataKinds;
51import android.provider.ContactsContract.CommonDataKinds.Phone;
52import android.provider.ContactsContract.Contacts;
53import android.provider.ContactsContract.Data;
54import android.provider.ContactsContract.PhoneLookup;
55import android.provider.ContactsContract.RawContactsEntity;
56import android.telephony.PhoneNumberUtils;
57import android.text.TextUtils;
58import android.util.Log;
59
60import java.io.IOException;
61import java.io.OutputStream;
62import java.util.ArrayList;
63
64import javax.obex.Operation;
65import javax.obex.ResponseCodes;
66import javax.obex.ServerOperation;
67
68public class BluetoothPbapVcardManager {
69    private static final String TAG = "BluetoothPbapVcardManager";
70
71    private static final boolean V = BluetoothPbapService.VERBOSE;
72
73    private ContentResolver mResolver;
74
75    private Context mContext;
76
77    private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
78
79    static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
80
81    static final String[] PHONES_CONTACTS_PROJECTION = new String[] {
82            Phone.CONTACT_ID, // 0
83            Phone.DISPLAY_NAME, // 1
84    };
85
86    static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
87            PhoneLookup._ID, PhoneLookup.DISPLAY_NAME
88    };
89
90    static final int CONTACTS_ID_COLUMN_INDEX = 0;
91
92    static final int CONTACTS_NAME_COLUMN_INDEX = 1;
93
94    // call histories use dynamic handles, and handles should order by date; the
95    // most recently one should be the first handle. In table "calls", _id and
96    // date are consistent in ordering, to implement simply, we sort by _id
97    // here.
98    static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
99
100    public BluetoothPbapVcardManager(final Context context) {
101        mContext = context;
102        mResolver = mContext.getContentResolver();
103    }
104
105    /**
106     * Create an owner vcard from the configured profile
107     * @param vcardType21
108     * @return
109     */
110    private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) {
111        // Currently only support Generic Vcard 2.1 and 3.0
112        int vcardType;
113        if (vcardType21) {
114            vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
115        } else {
116            vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
117        }
118
119        if (!BluetoothPbapConfig.includePhotosInVcard()) {
120            vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
121        }
122
123        return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter);
124    }
125
126    public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) {
127        //Owner vCard enhancement: Use "ME" profile if configured
128        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
129            String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter);
130            if (vcard != null && vcard.length() != 0) {
131                return vcard;
132            }
133        }
134        //End enhancement
135
136        BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext);
137        String name = BluetoothPbapService.getLocalPhoneName();
138        String number = BluetoothPbapService.getLocalPhoneNum();
139        String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
140                vcardType21);
141        return vcard;
142    }
143
144    public final int getPhonebookSize(final int type) {
145        int size;
146        switch (type) {
147            case BluetoothPbapObexServer.ContentType.PHONEBOOK:
148                size = getContactsSize();
149                break;
150            default:
151                size = getCallHistorySize(type);
152                break;
153        }
154        if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type);
155        return size;
156    }
157
158    public final int getContactsSize() {
159        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
160        Cursor contactCursor = null;
161        try {
162            contactCursor = mResolver.query(
163                    myUri, new String[] {Phone.CONTACT_ID}, null, null, Phone.CONTACT_ID);
164            if (contactCursor == null) {
165                return 0;
166            }
167            return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf
168        } catch (CursorWindowAllocationException e) {
169            Log.e(TAG, "CursorWindowAllocationException while getting Contacts size");
170        } finally {
171            if (contactCursor != null) {
172                contactCursor.close();
173            }
174        }
175        return 0;
176    }
177
178    public final int getCallHistorySize(final int type) {
179        final Uri myUri = CallLog.Calls.CONTENT_URI;
180        String selection = BluetoothPbapObexServer.createSelectionPara(type);
181        int size = 0;
182        Cursor callCursor = null;
183        try {
184            callCursor = mResolver.query(myUri, null, selection, null,
185                    CallLog.Calls.DEFAULT_SORT_ORDER);
186            if (callCursor != null) {
187                size = callCursor.getCount();
188            }
189        } catch (CursorWindowAllocationException e) {
190            Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size");
191        } finally {
192            if (callCursor != null) {
193                callCursor.close();
194                callCursor = null;
195            }
196        }
197        return size;
198    }
199
200    public final ArrayList<String> loadCallHistoryList(final int type) {
201        final Uri myUri = CallLog.Calls.CONTENT_URI;
202        String selection = BluetoothPbapObexServer.createSelectionPara(type);
203        String[] projection = new String[] {
204                Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION
205        };
206        final int CALLS_NUMBER_COLUMN_INDEX = 0;
207        final int CALLS_NAME_COLUMN_INDEX = 1;
208        final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2;
209
210        Cursor callCursor = null;
211        ArrayList<String> list = new ArrayList<String>();
212        try {
213            callCursor = mResolver.query(myUri, projection, selection, null,
214                    CALLLOG_SORT_ORDER);
215            if (callCursor != null) {
216                for (callCursor.moveToFirst(); !callCursor.isAfterLast();
217                        callCursor.moveToNext()) {
218                    String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
219                    if (TextUtils.isEmpty(name)) {
220                        // name not found, use number instead
221                        final int numberPresentation = callCursor.getInt(
222                                CALLS_NUMBER_PRESENTATION_COLUMN_INDEX);
223                        if (numberPresentation != Calls.PRESENTATION_ALLOWED) {
224                            name = mContext.getString(R.string.unknownNumber);
225                        } else {
226                            name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
227                        }
228                    }
229                    list.add(name);
230                }
231            }
232        } catch (CursorWindowAllocationException e) {
233            Log.e(TAG, "CursorWindowAllocationException while loading CallHistory");
234        } finally {
235            if (callCursor != null) {
236                callCursor.close();
237                callCursor = null;
238            }
239        }
240        return list;
241    }
242
243    public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
244        ArrayList<String> nameList = new ArrayList<String>();
245        //Owner vCard enhancement. Use "ME" profile if configured
246        String ownerName = null;
247        if (BluetoothPbapConfig.useProfileForOwnerVcard()) {
248            ownerName = BluetoothPbapUtils.getProfileName(mContext);
249        }
250        if (ownerName == null || ownerName.length()==0) {
251            ownerName = BluetoothPbapService.getLocalPhoneName();
252        }
253        nameList.add(ownerName);
254        //End enhancement
255
256        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
257        Cursor contactCursor = null;
258        // By default order is indexed
259        String orderBy = Phone.CONTACT_ID;
260        try {
261            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
262                orderBy = Phone.DISPLAY_NAME;
263            }
264            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
265            if (contactCursor != null) {
266                appendDistinctNameIdList(nameList,
267                        mContext.getString(android.R.string.unknownName),
268                        contactCursor);
269            }
270        } catch (CursorWindowAllocationException e) {
271            Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list");
272        } catch (Exception e) {
273            Log.e(TAG, "Exception while getting phonebook name list", e);
274        } finally {
275            if (contactCursor != null) {
276                contactCursor.close();
277                contactCursor = null;
278            }
279        }
280        return nameList;
281    }
282
283    public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
284        ArrayList<String> nameList = new ArrayList<String>();
285        ArrayList<String> tempNameList = new ArrayList<String>();
286
287        Cursor contactCursor = null;
288        Uri uri = null;
289        String[] projection = null;
290
291        if (TextUtils.isEmpty(phoneNumber)) {
292            uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
293            projection = PHONES_CONTACTS_PROJECTION;
294        } else {
295            uri = Uri.withAppendedPath(getPhoneLookupFilterUri(),
296                Uri.encode(phoneNumber));
297            projection = PHONE_LOOKUP_PROJECTION;
298        }
299
300        try {
301            contactCursor = mResolver.query(uri, projection, null, null, Phone.CONTACT_ID);
302
303            if (contactCursor != null) {
304                appendDistinctNameIdList(nameList,
305                        mContext.getString(android.R.string.unknownName),
306                        contactCursor);
307                if (V) {
308                    for (String nameIdStr : nameList) {
309                        Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber);
310                    }
311                }
312            }
313        } catch (CursorWindowAllocationException e) {
314            Log.e(TAG, "CursorWindowAllocationException while getting contact names");
315        } finally {
316            if (contactCursor != null) {
317                contactCursor.close();
318                contactCursor = null;
319            }
320        }
321        int tempListSize = tempNameList.size();
322        for (int index = 0; index < tempListSize; index++) {
323            String object = tempNameList.get(index);
324            if (!nameList.contains(object))
325                nameList.add(object);
326        }
327
328        return nameList;
329    }
330
331    public final int composeAndSendCallLogVcards(final int type, Operation op,
332            final int startPoint, final int endPoint, final boolean vcardType21,
333            boolean ignorefilter, byte[] filter) {
334        if (startPoint < 1 || startPoint > endPoint) {
335            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
336            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
337        }
338        String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
339
340        final Uri myUri = CallLog.Calls.CONTENT_URI;
341        final String[] CALLLOG_PROJECTION = new String[] {
342            CallLog.Calls._ID, // 0
343        };
344        final int ID_COLUMN_INDEX = 0;
345
346        Cursor callsCursor = null;
347        long startPointId = 0;
348        long endPointId = 0;
349        try {
350            // Need test to see if order by _ID is ok here, or by date?
351            callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
352                    CALLLOG_SORT_ORDER);
353            if (callsCursor != null) {
354                callsCursor.moveToPosition(startPoint - 1);
355                startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
356                if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
357                if (startPoint == endPoint) {
358                    endPointId = startPointId;
359                } else {
360                    callsCursor.moveToPosition(endPoint - 1);
361                    endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
362                }
363                if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
364            }
365        } catch (CursorWindowAllocationException e) {
366            Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards");
367        } finally {
368            if (callsCursor != null) {
369                callsCursor.close();
370                callsCursor = null;
371            }
372        }
373
374        String recordSelection;
375        if (startPoint == endPoint) {
376            recordSelection = Calls._ID + "=" + startPointId;
377        } else {
378            // The query to call table is by "_id DESC" order, so change
379            // correspondingly.
380            recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
381                    + startPointId;
382        }
383
384        String selection;
385        if (typeSelection == null) {
386            selection = recordSelection;
387        } else {
388            selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
389        }
390
391        if (V) Log.v(TAG, "Call log query selection is: " + selection);
392
393        return composeCallLogsAndSendVCards(op, selection, vcardType21, null, ignorefilter, filter);
394    }
395
396    public final int composeAndSendPhonebookVcards(Operation op, final int startPoint,
397            final int endPoint, final boolean vcardType21, String ownerVCard,
398            boolean ignorefilter, byte[] filter) {
399        if (startPoint < 1 || startPoint > endPoint) {
400            Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
401            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
402        }
403
404        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
405        Cursor contactCursor = null;
406        Cursor contactIdCursor = new MatrixCursor(new String[] {
407            Phone.CONTACT_ID
408        });
409        try {
410            contactCursor = mResolver.query(
411                    myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID);
412            if (contactCursor != null) {
413                contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint,
414                        endPoint);
415            }
416        } catch (CursorWindowAllocationException e) {
417            Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards");
418        } finally {
419            if (contactCursor != null) {
420                contactCursor.close();
421            }
422        }
423        return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
424                ignorefilter, filter);
425    }
426
427    public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
428            final boolean vcardType21, String ownerVCard, int orderByWhat,
429            boolean ignorefilter, byte[] filter) {
430        if (offset < 1) {
431            Log.e(TAG, "Internal error: offset is not correct.");
432            return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
433        }
434        final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
435
436        Cursor contactCursor = null;
437        Cursor contactIdCursor = new MatrixCursor(new String[] {
438            Phone.CONTACT_ID
439        });
440        // By default order is indexed
441        String orderBy = Phone.CONTACT_ID;
442        try {
443            if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
444                orderBy = Phone.DISPLAY_NAME;
445            }
446            contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy);
447        } catch (CursorWindowAllocationException e) {
448            Log.e(TAG,
449                "CursorWindowAllocationException while composing phonebook one vcard");
450        } finally {
451            if (contactCursor != null) {
452                contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset);
453                contactCursor.close();
454                contactCursor = null;
455            }
456        }
457        return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
458                ignorefilter, filter);
459    }
460
461    /**
462     * Filter contact cursor by certain condition.
463     */
464    public static final class ContactCursorFilter {
465        /**
466         *
467         * @param contactCursor
468         * @param offset
469         * @return a cursor containing contact id of {@code offset} contact.
470         */
471        public static Cursor filterByOffset(Cursor contactCursor, int offset) {
472            return filterByRange(contactCursor, offset, offset);
473        }
474
475        /**
476         *
477         * @param contactCursor
478         * @param startPoint
479         * @param endPoint
480         * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th
481         * contact.
482         */
483        public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) {
484            final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID);
485            long previousContactId = -1;
486            // As startPoint, endOffset index starts from 1 to n, we set
487            // currentPoint base as 1 not 0
488            int currentOffset = 1;
489            final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{
490                    Phone.CONTACT_ID
491            });
492            while (contactCursor.moveToNext() && currentOffset <= endPoint) {
493                long currentContactId = contactCursor.getLong(contactIdColumn);
494                if (previousContactId != currentContactId) {
495                    previousContactId = currentContactId;
496                    if (currentOffset >= startPoint) {
497                        contactIdsCursor.addRow(new Long[]{currentContactId});
498                        if (V) Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId);
499                    }
500                    currentOffset++;
501                }
502            }
503            return contactIdsCursor;
504        }
505    }
506
507    /**
508     * Handler enterprise contact id in VCardComposer
509     */
510    private static class EnterpriseRawContactEntitlesInfoCallback implements
511            VCardComposer.RawContactEntitlesInfoCallback {
512        @Override
513        public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) {
514            if (Contacts.isEnterpriseContactId(contactId)) {
515                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI,
516                        contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE);
517            } else {
518                return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId);
519            }
520        }
521    }
522
523    public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
524            final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
525        long timestamp = 0;
526        if (V) timestamp = System.currentTimeMillis();
527
528        VCardComposer composer = null;
529        VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
530
531        HandlerForStringBuffer buffer = null;
532        try {
533            // Currently only support Generic Vcard 2.1 and 3.0
534            int vcardType;
535            if (vcardType21) {
536                vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
537            } else {
538                vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
539            }
540            if (!vcardfilter.isPhotoEnabled()) {
541                vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
542            }
543
544            // Enhancement: customize Vcard based on preferences/settings and
545            // input from caller
546            composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
547            // End enhancement
548
549            // BT does want PAUSE/WAIT conversion while it doesn't want the
550            // other formatting
551            // done by vCard library by default.
552            composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
553                public String onValueReceived(String rawValue, int type, String label,
554                        boolean isPrimary) {
555                    // 'p' and 'w' are the standard characters for pause and
556                    // wait
557                    // (see RFC 3601)
558                    // so use those when exporting phone numbers via vCard.
559                    String numberWithControlSequence = rawValue
560                            .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT,
561                                    'w');
562                    return numberWithControlSequence;
563                }
564            });
565            buffer = new HandlerForStringBuffer(op, ownerVCard);
566            Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount());
567            if (!composer.initWithCallback(contactIdCursor,
568                    new EnterpriseRawContactEntitlesInfoCallback())
569                    || !buffer.onInit(mContext)) {
570                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
571            }
572
573            while (!composer.isAfterLast()) {
574                if (BluetoothPbapObexServer.sIsAborted) {
575                    ((ServerOperation) op).isAborted = true;
576                    BluetoothPbapObexServer.sIsAborted = false;
577                    break;
578                }
579                String vcard = composer.createOneEntry();
580                if (vcard == null) {
581                    Log.e(TAG,
582                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
583                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
584                }
585                if (V) Log.v(TAG, "vCard from composer: " + vcard);
586
587                vcard = vcardfilter.apply(vcard, vcardType21);
588                vcard = StripTelephoneNumber(vcard);
589
590                if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
591
592                if (!buffer.onEntryCreated(vcard)) {
593                    // onEntryCreate() already emits error.
594                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
595                }
596            }
597        } finally {
598            if (composer != null) {
599                composer.terminate();
600            }
601            if (buffer != null) {
602                buffer.onTerminate();
603            }
604        }
605
606        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
607                    + (System.currentTimeMillis() - timestamp) + " ms");
608
609        return ResponseCodes.OBEX_HTTP_OK;
610    }
611
612    public final int composeCallLogsAndSendVCards(Operation op, final String selection,
613            final boolean vcardType21, String ownerVCard, boolean ignorefilter,
614            byte[] filter) {
615        long timestamp = 0;
616        if (V) timestamp = System.currentTimeMillis();
617
618        BluetoothPbapCallLogComposer composer = null;
619        HandlerForStringBuffer buffer = null;
620        try {
621
622            composer = new BluetoothPbapCallLogComposer(mContext);
623            buffer = new HandlerForStringBuffer(op, ownerVCard);
624            if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER)
625                    || !buffer.onInit(mContext)) {
626                return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
627            }
628
629            while (!composer.isAfterLast()) {
630                if (BluetoothPbapObexServer.sIsAborted) {
631                    ((ServerOperation) op).isAborted = true;
632                    BluetoothPbapObexServer.sIsAborted = false;
633                    break;
634                }
635                String vcard = composer.createOneEntry(vcardType21);
636                if (vcard == null) {
637                    Log.e(TAG,
638                            "Failed to read a contact. Error reason: " + composer.getErrorReason());
639                    return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
640                }
641                if (V) {
642                    Log.v(TAG, "Vcard Entry:");
643                    Log.v(TAG, vcard);
644                }
645
646                buffer.onEntryCreated(vcard);
647            }
648        } finally {
649            if (composer != null) {
650                composer.terminate();
651            }
652            if (buffer != null) {
653                buffer.onTerminate();
654            }
655        }
656
657        if (V) Log.v(TAG, "Total vcard composing and sending out takes "
658                + (System.currentTimeMillis() - timestamp) + " ms");
659        return ResponseCodes.OBEX_HTTP_OK;
660    }
661
662    public String StripTelephoneNumber (String vCard){
663        String attr [] = vCard.split(System.getProperty("line.separator"));
664        String Vcard = "";
665            for (int i=0; i < attr.length; i++) {
666                if(attr[i].startsWith("TEL")) {
667                    attr[i] = attr[i].replace("(", "");
668                    attr[i] = attr[i].replace(")", "");
669                    attr[i] = attr[i].replace("-", "");
670                    attr[i] = attr[i].replace(" ", "");
671                }
672            }
673
674            for (int i=0; i < attr.length; i++) {
675                if(!attr[i].equals("")){
676                    Vcard = Vcard.concat(attr[i] + "\n");
677                }
678            }
679        if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
680        return Vcard;
681    }
682
683    /**
684     * Handler to emit vCards to PCE.
685     */
686    public class HandlerForStringBuffer {
687        private Operation operation;
688
689        private OutputStream outputStream;
690
691        private String phoneOwnVCard = null;
692
693        public HandlerForStringBuffer(Operation op, String ownerVCard) {
694            operation = op;
695            if (ownerVCard != null) {
696                phoneOwnVCard = ownerVCard;
697                if (V) Log.v(TAG, "phone own number vcard:");
698                if (V) Log.v(TAG, phoneOwnVCard);
699            }
700        }
701
702        private boolean write(String vCard) {
703            try {
704                if (vCard != null) {
705                    outputStream.write(vCard.getBytes());
706                    return true;
707                }
708            } catch (IOException e) {
709                Log.e(TAG, "write outputstrem failed" + e.toString());
710            }
711            return false;
712        }
713
714        public boolean onInit(Context context) {
715            try {
716                outputStream = operation.openOutputStream();
717                if (phoneOwnVCard != null) {
718                    return write(phoneOwnVCard);
719                }
720                return true;
721            } catch (IOException e) {
722                Log.e(TAG, "open outputstrem failed" + e.toString());
723            }
724            return false;
725        }
726
727        public boolean onEntryCreated(String vcard) {
728            return write(vcard);
729        }
730
731        public void onTerminate() {
732            if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
733                if (V) Log.v(TAG, "CloseStream failed!");
734            } else {
735                if (V) Log.v(TAG, "CloseStream ok!");
736            }
737        }
738    }
739
740    public static class VCardFilter {
741        private static enum FilterBit {
742            //       bit  property                  onlyCheckV21  excludeForV21
743            FN (       1, "FN",                       true,         false),
744            PHOTO(     3, "PHOTO",                    false,        false),
745            BDAY(      4, "BDAY",                     false,        false),
746            ADR(       5, "ADR",                      false,        false),
747            EMAIL(     8, "EMAIL",                    false,        false),
748            TITLE(    12, "TITLE",                    false,        false),
749            ORG(      16, "ORG",                      false,        false),
750            NOTE(     17, "NOTE",                     false,        false),
751            URL(      20, "URL",                      false,        false),
752            NICKNAME( 23, "NICKNAME",                 false,        true),
753            DATETIME( 28, "X-IRMC-CALL-DATETIME",     false,        false);
754
755            public final int pos;
756            public final String prop;
757            public final boolean onlyCheckV21;
758            public final boolean excludeForV21;
759
760            FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
761                this.pos = pos;
762                this.prop = prop;
763                this.onlyCheckV21 = onlyCheckV21;
764                this.excludeForV21 = excludeForV21;
765            }
766        }
767
768        private static final String SEPARATOR = System.getProperty("line.separator");
769        private final byte[] filter;
770
771        //This function returns true if the attributes needs to be included in the filtered vcard.
772        private boolean isFilteredIn(FilterBit bit, boolean vCardType21) {
773            final int offset = (bit.pos / 8) + 1;
774            final int bit_pos = bit.pos % 8;
775            if (!vCardType21 && bit.onlyCheckV21) return true;
776            if (vCardType21 && bit.excludeForV21) return false;
777            if (filter == null || offset >= filter.length) return true;
778            return ((filter[filter.length - offset] >> bit_pos) & 0x01) != 0;
779        }
780
781        VCardFilter(byte[] filter) {
782            this.filter = filter;
783        }
784
785        public boolean isPhotoEnabled() {
786            return isFilteredIn(FilterBit.PHOTO, false);
787        }
788
789        public String apply(String vCard, boolean vCardType21){
790            if (filter == null) return vCard;
791            String lines[] = vCard.split(SEPARATOR);
792            StringBuilder filteredVCard = new StringBuilder();
793            boolean filteredIn = false;
794
795            for (String line : lines) {
796                // Check whether the current property is changing (ignoring multi-line properties)
797                // and determine if the current property is filtered in.
798                if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) {
799                    String currentProp = line.split("[;:]")[0];
800                    filteredIn = true;
801
802                    for (FilterBit bit : FilterBit.values()) {
803                        if (bit.prop.equals(currentProp)) {
804                            filteredIn = isFilteredIn(bit, vCardType21);
805                            break;
806                        }
807                    }
808
809                    // Since PBAP does not have filter bits for IM and SIP,
810                    // exclude them by default. Easiest way is to exclude all
811                    // X- fields, except date time....
812                    if (currentProp.startsWith("X-")) {
813                        filteredIn = false;
814                        if (currentProp.equals("X-IRMC-CALL-DATETIME")) {
815                            filteredIn = true;
816                        }
817                    }
818                }
819
820                // Build filtered vCard
821                if (filteredIn) {
822                    filteredVCard.append(line + SEPARATOR);
823                }
824            }
825
826            return filteredVCard.toString();
827        }
828    }
829
830    private static final Uri getPhoneLookupFilterUri() {
831        return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
832    }
833
834    /**
835     * Get size of the cursor without duplicated contact id. This assumes the
836     * given cursor is sorted by CONTACT_ID.
837     */
838    private static final int getDistinctContactIdSize(Cursor cursor) {
839        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
840        final int idColumn = cursor.getColumnIndex(Data._ID);
841        long previousContactId = -1;
842        int count = 0;
843        cursor.moveToPosition(-1);
844        while (cursor.moveToNext()) {
845            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
846            if (previousContactId != contactId) {
847                count++;
848                previousContactId = contactId;
849            }
850        }
851        if (V) {
852            Log.i(TAG, "getDistinctContactIdSize result: " + count);
853        }
854        return count;
855    }
856
857    /**
858     * Append "display_name,contact_id" string array from cursor to ArrayList.
859     * This assumes the given cursor is sorted by CONTACT_ID.
860     */
861    private static void appendDistinctNameIdList(ArrayList<String> resultList,
862            String defaultName, Cursor cursor) {
863        final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
864        final int idColumn = cursor.getColumnIndex(Data._ID);
865        final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME);
866        cursor.moveToPosition(-1);
867        while (cursor.moveToNext()) {
868            final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn);
869            String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName;
870            if (TextUtils.isEmpty(displayName)) {
871                displayName = defaultName;
872            }
873
874            String newString = displayName + "," + contactId;
875            if (!resultList.contains(newString)) {
876                resultList.add(newString);
877            }
878        }
879        if (V) {
880            for (String nameId : resultList) {
881                Log.i(TAG, "appendDistinctNameIdList result: " + nameId);
882            }
883        }
884    }
885}
886