Home | History | Annotate | Download | only in pbap
      1 /*
      2  * Copyright (c) 2008-2009, Motorola, Inc.
      3  *
      4  * All rights reserved.
      5  *
      6  * Redistribution and use in source and binary forms, with or without
      7  * modification, are permitted provided that the following conditions are met:
      8  *
      9  * - Redistributions of source code must retain the above copyright notice,
     10  * this list of conditions and the following disclaimer.
     11  *
     12  * - Redistributions in binary form must reproduce the above copyright notice,
     13  * this list of conditions and the following disclaimer in the documentation
     14  * and/or other materials provided with the distribution.
     15  *
     16  * - Neither the name of the Motorola, Inc. nor the names of its contributors
     17  * may be used to endorse or promote products derived from this software
     18  * without specific prior written permission.
     19  *
     20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     21  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     22  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     23  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
     24  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     25  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     26  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     27  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     28  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     29  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     30  * POSSIBILITY OF SUCH DAMAGE.
     31  */
     32 
     33 package com.android.bluetooth.pbap;
     34 
     35 import android.content.ContentResolver;
     36 import android.content.Context;
     37 import android.database.Cursor;
     38 import android.net.Uri;
     39 import android.pim.vcard.VCardComposer;
     40 import android.pim.vcard.VCardConfig;
     41 import android.pim.vcard.VCardComposer.OneEntryHandler;
     42 import android.provider.CallLog;
     43 import android.provider.CallLog.Calls;
     44 import android.provider.ContactsContract.CommonDataKinds;
     45 import android.provider.ContactsContract.Contacts;
     46 import android.provider.ContactsContract.Data;
     47 import android.provider.ContactsContract.CommonDataKinds.Phone;
     48 import android.text.TextUtils;
     49 import android.util.Log;
     50 
     51 import com.android.bluetooth.R;
     52 
     53 import java.io.IOException;
     54 import java.io.OutputStream;
     55 import java.util.ArrayList;
     56 
     57 import javax.obex.ServerOperation;
     58 import javax.obex.Operation;
     59 import javax.obex.ResponseCodes;
     60 
     61 public class BluetoothPbapVcardManager {
     62     private static final String TAG = "BluetoothPbapVcardManager";
     63 
     64     private static final boolean V = BluetoothPbapService.VERBOSE;
     65 
     66     private ContentResolver mResolver;
     67 
     68     private Context mContext;
     69 
     70     private StringBuilder mVcardResults = null;
     71 
     72     static final String[] PHONES_PROJECTION = new String[] {
     73             Data._ID, // 0
     74             CommonDataKinds.Phone.TYPE, // 1
     75             CommonDataKinds.Phone.LABEL, // 2
     76             CommonDataKinds.Phone.NUMBER, // 3
     77             Contacts.DISPLAY_NAME, // 4
     78     };
     79 
     80     private static final int ID_COLUMN_INDEX = 0;
     81 
     82     private static final int PHONE_TYPE_COLUMN_INDEX = 1;
     83 
     84     private static final int PHONE_LABEL_COLUMN_INDEX = 2;
     85 
     86     private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
     87 
     88     private static final int CONTACTS_DISPLAY_NAME_COLUMN_INDEX = 4;
     89 
     90     static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
     91 
     92     static final String[] CONTACTS_PROJECTION = new String[] {
     93             Contacts._ID, // 0
     94             Contacts.DISPLAY_NAME, // 1
     95     };
     96 
     97     static final int CONTACTS_ID_COLUMN_INDEX = 0;
     98 
     99     static final int CONTACTS_NAME_COLUMN_INDEX = 1;
    100 
    101     // call histories use dynamic handles, and handles should order by date; the
    102     // most recently one should be the first handle. In table "calls", _id and
    103     // date are consistent in ordering, to implement simply, we sort by _id
    104     // here.
    105     static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
    106 
    107     private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
    108 
    109     public BluetoothPbapVcardManager(final Context context) {
    110         mContext = context;
    111         mResolver = mContext.getContentResolver();
    112     }
    113 
    114     public final String getOwnerPhoneNumberVcard(final boolean vcardType21) {
    115         BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext, false);
    116         String name = BluetoothPbapService.getLocalPhoneName();
    117         String number = BluetoothPbapService.getLocalPhoneNum();
    118         String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number,
    119                 vcardType21);
    120         return vcard;
    121     }
    122 
    123     public final int getPhonebookSize(final int type) {
    124         int size;
    125         switch (type) {
    126             case BluetoothPbapObexServer.ContentType.PHONEBOOK:
    127                 size = getContactsSize();
    128                 break;
    129             default:
    130                 size = getCallHistorySize(type);
    131                 break;
    132         }
    133         if (V) Log.v(TAG, "getPhonebookSzie size = " + size + " type = " + type);
    134         return size;
    135     }
    136 
    137     public final int getContactsSize() {
    138         final Uri myUri = Contacts.CONTENT_URI;
    139         int size = 0;
    140         Cursor contactCursor = null;
    141         try {
    142             contactCursor = mResolver.query(myUri, null, CLAUSE_ONLY_VISIBLE, null, null);
    143             if (contactCursor != null) {
    144                 size = contactCursor.getCount() + 1; // always has the 0.vcf
    145             }
    146         } finally {
    147             if (contactCursor != null) {
    148                 contactCursor.close();
    149             }
    150         }
    151         return size;
    152     }
    153 
    154     public final int getCallHistorySize(final int type) {
    155         final Uri myUri = CallLog.Calls.CONTENT_URI;
    156         String selection = BluetoothPbapObexServer.createSelectionPara(type);
    157         int size = 0;
    158         Cursor callCursor = null;
    159         try {
    160             callCursor = mResolver.query(myUri, null, selection, null,
    161                     CallLog.Calls.DEFAULT_SORT_ORDER);
    162             if (callCursor != null) {
    163                 size = callCursor.getCount();
    164             }
    165         } finally {
    166             if (callCursor != null) {
    167                 callCursor.close();
    168             }
    169         }
    170         return size;
    171     }
    172 
    173     public final ArrayList<String> loadCallHistoryList(final int type) {
    174         final Uri myUri = CallLog.Calls.CONTENT_URI;
    175         String selection = BluetoothPbapObexServer.createSelectionPara(type);
    176         String[] projection = new String[] {
    177                 Calls.NUMBER, Calls.CACHED_NAME
    178         };
    179         final int CALLS_NUMBER_COLUMN_INDEX = 0;
    180         final int CALLS_NAME_COLUMN_INDEX = 1;
    181 
    182         Cursor callCursor = null;
    183         ArrayList<String> list = new ArrayList<String>();
    184         try {
    185             callCursor = mResolver.query(myUri, projection, selection, null,
    186                     CALLLOG_SORT_ORDER);
    187             if (callCursor != null) {
    188                 for (callCursor.moveToFirst(); !callCursor.isAfterLast();
    189                         callCursor.moveToNext()) {
    190                     String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX);
    191                     if (TextUtils.isEmpty(name)) {
    192                         // name not found,use number instead
    193                         name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
    194                     }
    195                     list.add(name);
    196                 }
    197             }
    198         } finally {
    199             if (callCursor != null) {
    200                 callCursor.close();
    201             }
    202         }
    203         return list;
    204     }
    205 
    206     public final ArrayList<String> getPhonebookNameList(final int orderByWhat) {
    207         ArrayList<String> nameList = new ArrayList<String>();
    208         nameList.add(BluetoothPbapService.getLocalPhoneName());
    209 
    210         final Uri myUri = Contacts.CONTENT_URI;
    211         Cursor contactCursor = null;
    212         try {
    213             if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
    214                 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
    215                         null, Contacts._ID);
    216             } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
    217                 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
    218                         null, Contacts.DISPLAY_NAME);
    219             }
    220             if (contactCursor != null) {
    221                 for (contactCursor.moveToFirst(); !contactCursor.isAfterLast(); contactCursor
    222                         .moveToNext()) {
    223                     String name = contactCursor.getString(CONTACTS_NAME_COLUMN_INDEX);
    224                     if (TextUtils.isEmpty(name)) {
    225                         name = mContext.getString(android.R.string.unknownName);
    226                     }
    227                     nameList.add(name);
    228                 }
    229             }
    230         } finally {
    231             if (contactCursor != null) {
    232                 contactCursor.close();
    233             }
    234         }
    235         return nameList;
    236     }
    237 
    238     public final ArrayList<String> getPhonebookNumberList() {
    239         ArrayList<String> numberList = new ArrayList<String>();
    240         numberList.add(BluetoothPbapService.getLocalPhoneNum());
    241 
    242         final Uri myUri = Phone.CONTENT_URI;
    243         Cursor phoneCursor = null;
    244         try {
    245             phoneCursor = mResolver.query(myUri, PHONES_PROJECTION, CLAUSE_ONLY_VISIBLE, null,
    246                     SORT_ORDER_PHONE_NUMBER);
    247             if (phoneCursor != null) {
    248                 for (phoneCursor.moveToFirst(); !phoneCursor.isAfterLast(); phoneCursor
    249                         .moveToNext()) {
    250                     String number = phoneCursor.getString(PHONE_NUMBER_COLUMN_INDEX);
    251                     if (TextUtils.isEmpty(number)) {
    252                         number = mContext.getString(R.string.defaultnumber);
    253                     }
    254                     numberList.add(number);
    255                 }
    256             }
    257         } finally {
    258             if (phoneCursor != null) {
    259                 phoneCursor.close();
    260             }
    261         }
    262         return numberList;
    263     }
    264 
    265     public final int composeAndSendCallLogVcards(final int type, Operation op,
    266             final int startPoint, final int endPoint, final boolean vcardType21) {
    267         if (startPoint < 1 || startPoint > endPoint) {
    268             Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
    269             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    270         }
    271         String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
    272 
    273         final Uri myUri = CallLog.Calls.CONTENT_URI;
    274         final String[] CALLLOG_PROJECTION = new String[] {
    275             CallLog.Calls._ID, // 0
    276         };
    277         final int ID_COLUMN_INDEX = 0;
    278 
    279         Cursor callsCursor = null;
    280         long startPointId = 0;
    281         long endPointId = 0;
    282         try {
    283             // Need test to see if order by _ID is ok here, or by date?
    284             callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
    285                     CALLLOG_SORT_ORDER);
    286             if (callsCursor != null) {
    287                 callsCursor.moveToPosition(startPoint - 1);
    288                 startPointId = callsCursor.getLong(ID_COLUMN_INDEX);
    289                 if (V) Log.v(TAG, "Call Log query startPointId = " + startPointId);
    290                 if (startPoint == endPoint) {
    291                     endPointId = startPointId;
    292                 } else {
    293                     callsCursor.moveToPosition(endPoint - 1);
    294                     endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
    295                 }
    296                 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
    297             }
    298         } finally {
    299             if (callsCursor != null) {
    300                 callsCursor.close();
    301             }
    302         }
    303 
    304         String recordSelection;
    305         if (startPoint == endPoint) {
    306             recordSelection = Calls._ID + "=" + startPointId;
    307         } else {
    308             // The query to call table is by "_id DESC" order, so change
    309             // correspondingly.
    310             recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
    311                     + startPointId;
    312         }
    313 
    314         String selection;
    315         if (typeSelection == null) {
    316             selection = recordSelection;
    317         } else {
    318             selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
    319         }
    320 
    321         if (V) Log.v(TAG, "Call log query selection is: " + selection);
    322 
    323         return composeAndSendVCards(op, selection, vcardType21, null, false);
    324     }
    325 
    326     public final int composeAndSendPhonebookVcards(Operation op, final int startPoint,
    327             final int endPoint, final boolean vcardType21, String ownerVCard) {
    328         if (startPoint < 1 || startPoint > endPoint) {
    329             Log.e(TAG, "internal error: startPoint or endPoint is not correct.");
    330             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    331         }
    332         final Uri myUri = Contacts.CONTENT_URI;
    333 
    334         Cursor contactCursor = null;
    335         long startPointId = 0;
    336         long endPointId = 0;
    337         try {
    338             contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE, null,
    339                     Contacts._ID);
    340             if (contactCursor != null) {
    341                 contactCursor.moveToPosition(startPoint - 1);
    342                 startPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
    343                 if (V) Log.v(TAG, "Query startPointId = " + startPointId);
    344                 if (startPoint == endPoint) {
    345                     endPointId = startPointId;
    346                 } else {
    347                     contactCursor.moveToPosition(endPoint - 1);
    348                     endPointId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
    349                 }
    350                 if (V) Log.v(TAG, "Query endPointId = " + endPointId);
    351             }
    352         } finally {
    353             if (contactCursor != null) {
    354                 contactCursor.close();
    355             }
    356         }
    357 
    358         final String selection;
    359         if (startPoint == endPoint) {
    360             selection = Contacts._ID + "=" + startPointId + " AND " + CLAUSE_ONLY_VISIBLE;
    361         } else {
    362             selection = Contacts._ID + ">=" + startPointId + " AND " + Contacts._ID + "<="
    363                     + endPointId + " AND " + CLAUSE_ONLY_VISIBLE;
    364         }
    365 
    366         if (V) Log.v(TAG, "Query selection is: " + selection);
    367 
    368         return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
    369     }
    370 
    371     public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
    372             final boolean vcardType21, String ownerVCard, int orderByWhat) {
    373         if (offset < 1) {
    374             Log.e(TAG, "Internal error: offset is not correct.");
    375             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    376         }
    377         final Uri myUri = Contacts.CONTENT_URI;
    378         Cursor contactCursor = null;
    379         String selection = null;
    380         long contactId = 0;
    381         if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) {
    382             try {
    383                 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
    384                         null, Contacts._ID);
    385                 if (contactCursor != null) {
    386                     contactCursor.moveToPosition(offset - 1);
    387                     contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
    388                     if (V) Log.v(TAG, "Query startPointId = " + contactId);
    389                 }
    390             } finally {
    391                 if (contactCursor != null) {
    392                     contactCursor.close();
    393                 }
    394             }
    395         } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) {
    396             try {
    397                 contactCursor = mResolver.query(myUri, CONTACTS_PROJECTION, CLAUSE_ONLY_VISIBLE,
    398                         null, Contacts.DISPLAY_NAME);
    399                 if (contactCursor != null) {
    400                     contactCursor.moveToPosition(offset - 1);
    401                     contactId = contactCursor.getLong(CONTACTS_ID_COLUMN_INDEX);
    402                     if (V) Log.v(TAG, "Query startPointId = " + contactId);
    403                 }
    404             } finally {
    405                 if (contactCursor != null) {
    406                     contactCursor.close();
    407                 }
    408             }
    409         } else {
    410             Log.e(TAG, "Parameter orderByWhat is not supported!");
    411             return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    412         }
    413         selection = Contacts._ID + "=" + contactId;
    414 
    415         if (V) Log.v(TAG, "Query selection is: " + selection);
    416 
    417         return composeAndSendVCards(op, selection, vcardType21, ownerVCard, true);
    418     }
    419 
    420     public final int composeAndSendVCards(Operation op, final String selection,
    421             final boolean vcardType21, String ownerVCard, boolean isContacts) {
    422         long timestamp = 0;
    423         if (V) timestamp = System.currentTimeMillis();
    424 
    425         if (isContacts) {
    426             VCardComposer composer = null;
    427             try {
    428                 // Currently only support Generic Vcard 2.1 and 3.0
    429                 int vcardType;
    430                 if (vcardType21) {
    431                     vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC_UTF8;
    432                 } else {
    433                     vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC_UTF8;
    434                 }
    435                 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
    436 
    437                 composer = new VCardComposer(mContext, vcardType, true);
    438                 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
    439                 if (!composer.init(Contacts.CONTENT_URI, selection, null, null)) {
    440                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    441                 }
    442 
    443                 while (!composer.isAfterLast()) {
    444                     if (BluetoothPbapObexServer.sIsAborted) {
    445                         ((ServerOperation)op).isAborted = true;
    446                         BluetoothPbapObexServer.sIsAborted = false;
    447                         break;
    448                     }
    449                     if (!composer.createOneEntry()) {
    450                         Log.e(TAG, "Failed to read a contact. Error reason: "
    451                                 + composer.getErrorReason());
    452                         return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    453                     }
    454                 }
    455             } finally {
    456                 if (composer != null) {
    457                     composer.terminate();
    458                 }
    459             }
    460         } else { // CallLog
    461             BluetoothPbapCallLogComposer composer = null;
    462             try {
    463                 composer = new BluetoothPbapCallLogComposer(mContext, true);
    464                 composer.addHandler(new HandlerForStringBuffer(op, ownerVCard));
    465                 if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, null)) {
    466                     return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    467                 }
    468 
    469                 while (!composer.isAfterLast()) {
    470                     if (BluetoothPbapObexServer.sIsAborted) {
    471                         ((ServerOperation)op).isAborted = true;
    472                         BluetoothPbapObexServer.sIsAborted = false;
    473                         break;
    474                     }
    475                     if (!composer.createOneEntry()) {
    476                         Log.e(TAG, "Failed to read a contact. Error reason: "
    477                                 + composer.getErrorReason());
    478                         return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
    479                     }
    480                 }
    481             } finally {
    482                 if (composer != null) {
    483                     composer.terminate();
    484                 }
    485             }
    486         }
    487 
    488         if (V) Log.v(TAG, "Total vcard composing and sending out takes "
    489                     + (System.currentTimeMillis() - timestamp) + " ms");
    490 
    491         return ResponseCodes.OBEX_HTTP_OK;
    492     }
    493 
    494     /**
    495      * Handler to emit VCard String to PCE once size grow to maxPacketSize.
    496      */
    497     public class HandlerForStringBuffer implements OneEntryHandler {
    498         @SuppressWarnings("hiding")
    499         private Operation operation;
    500 
    501         private OutputStream outputStream;
    502 
    503         private int maxPacketSize;
    504 
    505         private String phoneOwnVCard = null;
    506 
    507         public HandlerForStringBuffer(Operation op, String ownerVCard) {
    508             operation = op;
    509             maxPacketSize = operation.getMaxPacketSize();
    510             if (V) Log.v(TAG, "getMaxPacketSize() = " + maxPacketSize);
    511             if (ownerVCard != null) {
    512                 phoneOwnVCard = ownerVCard;
    513                 if (V) Log.v(TAG, "phone own number vcard:");
    514                 if (V) Log.v(TAG, phoneOwnVCard);
    515             }
    516         }
    517 
    518         public boolean onInit(Context context) {
    519             try {
    520                 outputStream = operation.openOutputStream();
    521                 mVcardResults = new StringBuilder();
    522                 if (phoneOwnVCard != null) {
    523                     mVcardResults.append(phoneOwnVCard);
    524                 }
    525             } catch (IOException e) {
    526                 Log.e(TAG, "open outputstrem failed" + e.toString());
    527                 return false;
    528             }
    529             if (V) Log.v(TAG, "openOutputStream() ok.");
    530             return true;
    531         }
    532 
    533         public boolean onEntryCreated(String vcard) {
    534             int vcardLen = vcard.length();
    535             if (V) Log.v(TAG, "The length of this vcard is: " + vcardLen);
    536 
    537             mVcardResults.append(vcard);
    538             int vcardStringLen = mVcardResults.toString().length();
    539             if (V) Log.v(TAG, "The length of this vcardResults is: " + vcardStringLen);
    540 
    541             if (vcardStringLen >= maxPacketSize) {
    542                 long timestamp = 0;
    543                 int position = 0;
    544 
    545                 // Need while loop to handle the big vcard case
    546                 while (!BluetoothPbapObexServer.sIsAborted
    547                         && position < (vcardStringLen - maxPacketSize)) {
    548                     if (V) timestamp = System.currentTimeMillis();
    549 
    550                     String subStr = mVcardResults.toString().substring(position,
    551                             position + maxPacketSize);
    552                     try {
    553                         outputStream.write(subStr.getBytes(), 0, maxPacketSize);
    554                     } catch (IOException e) {
    555                         Log.e(TAG, "write outputstrem failed" + e.toString());
    556                         return false;
    557                     }
    558                     if (V) Log.v(TAG, "Sending vcard String " + maxPacketSize + " bytes took "
    559                             + (System.currentTimeMillis() - timestamp) + " ms");
    560 
    561                     position += maxPacketSize;
    562                 }
    563                 mVcardResults.delete(0, position);
    564             }
    565             return true;
    566         }
    567 
    568         public void onTerminate() {
    569             // Send out last packet
    570             String lastStr = mVcardResults.toString();
    571             try {
    572                 outputStream.write(lastStr.getBytes(), 0, lastStr.length());
    573             } catch (IOException e) {
    574                 Log.e(TAG, "write outputstrem failed" + e.toString());
    575             }
    576             if (V) Log.v(TAG, "Last packet sent out, sending process complete!");
    577 
    578             if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
    579                 if (V) Log.v(TAG, "CloseStream failed!");
    580             } else {
    581                 if (V) Log.v(TAG, "CloseStream ok!");
    582             }
    583         }
    584     }
    585 }
    586