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