OSDN Git Service

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