2 * Copyright (c) 2008-2009, Motorola, Inc.
3 * Copyright (C) 2009-2012, Broadcom Corporation
7 * Redistribution and use in source and binary forms, with or without
8 * modification, are permitted provided that the following conditions are met:
10 * - Redistributions of source code must retain the above copyright notice,
11 * this list of conditions and the following disclaimer.
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.
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.
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.
34 package com.android.bluetooth.pbap;
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;
55 import com.android.bluetooth.R;
56 import com.android.vcard.VCardComposer;
57 import com.android.vcard.VCardConfig;
58 import com.android.vcard.VCardPhoneNumberTranslationCallback;
60 import java.io.IOException;
61 import java.io.OutputStream;
62 import java.util.ArrayList;
63 import java.util.Collections;
65 import javax.obex.ServerOperation;
66 import javax.obex.Operation;
67 import javax.obex.ResponseCodes;
69 import com.android.bluetooth.Utils;
70 import com.android.bluetooth.util.DevicePolicyUtils;
72 public class BluetoothPbapVcardManager {
73 private static final String TAG = "BluetoothPbapVcardManager";
75 private static final boolean V = BluetoothPbapService.VERBOSE;
77 private ContentResolver mResolver;
79 private Context mContext;
81 private static final int PHONE_NUMBER_COLUMN_INDEX = 3;
83 static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC";
85 static final String[] PHONES_CONTACTS_PROJECTION = new String[] {
86 Phone.CONTACT_ID, // 0
87 Phone.DISPLAY_NAME, // 1
90 static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
91 PhoneLookup._ID, PhoneLookup.DISPLAY_NAME
94 static final int CONTACTS_ID_COLUMN_INDEX = 0;
96 static final int CONTACTS_NAME_COLUMN_INDEX = 1;
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
102 static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC";
104 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
106 public BluetoothPbapVcardManager(final Context context) {
108 mResolver = mContext.getContentResolver();
112 * Create an owner vcard from the configured profile
116 private final String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) {
117 // Currently only support Generic Vcard 2.1 and 3.0
120 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
122 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
125 if (!BluetoothPbapConfig.includePhotosInVcard()) {
126 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
129 return BluetoothPbapUtils.createProfileVCard(mContext, vcardType,filter);
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) {
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,
150 public final int getPhonebookSize(final int type) {
153 case BluetoothPbapObexServer.ContentType.PHONEBOOK:
154 size = getContactsSize();
157 size = getCallHistorySize(type);
160 if (V) Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type);
164 public final int getContactsSize() {
165 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
166 Cursor contactCursor = null;
168 contactCursor = mResolver.query(myUri, new String[] {Phone.CONTACT_ID},
169 CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID);
170 if (contactCursor == null) {
173 return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf
174 } catch (CursorWindowAllocationException e) {
175 Log.e(TAG, "CursorWindowAllocationException while getting Contacts size");
177 if (contactCursor != null) {
178 contactCursor.close();
184 public final int getCallHistorySize(final int type) {
185 final Uri myUri = CallLog.Calls.CONTENT_URI;
186 String selection = BluetoothPbapObexServer.createSelectionPara(type);
188 Cursor callCursor = null;
190 callCursor = mResolver.query(myUri, null, selection, null,
191 CallLog.Calls.DEFAULT_SORT_ORDER);
192 if (callCursor != null) {
193 size = callCursor.getCount();
195 } catch (CursorWindowAllocationException e) {
196 Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size");
198 if (callCursor != null) {
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
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;
216 Cursor callCursor = null;
217 ArrayList<String> list = new ArrayList<String>();
219 callCursor = mResolver.query(myUri, projection, selection, null,
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);
232 name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX);
238 } catch (CursorWindowAllocationException e) {
239 Log.e(TAG, "CursorWindowAllocationException while loading CallHistory");
241 if (callCursor != null) {
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);
256 if (ownerName == null || ownerName.length()==0) {
257 ownerName = BluetoothPbapService.getLocalPhoneName();
259 nameList.add(ownerName);
262 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
263 Cursor contactCursor = null;
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),
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);
279 } catch (CursorWindowAllocationException e) {
280 Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list");
282 if (contactCursor != null) {
283 contactCursor.close();
284 contactCursor = null;
290 public final ArrayList<String> getContactNamesByNumber(final String phoneNumber) {
291 ArrayList<String> nameList = new ArrayList<String>();
292 ArrayList<String> tempNameList = new ArrayList<String>();
294 Cursor contactCursor = null;
296 String[] projection = null;
298 if (TextUtils.isEmpty(phoneNumber)) {
299 uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
300 projection = PHONES_CONTACTS_PROJECTION;
302 uri = Uri.withAppendedPath(getPhoneLookupFilterUri(),
303 Uri.encode(phoneNumber));
304 projection = PHONE_LOOKUP_PROJECTION;
308 contactCursor = mResolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null,
311 if (contactCursor != null) {
312 appendDistinctNameIdList(nameList,
313 mContext.getString(android.R.string.unknownName),
316 for (String nameIdStr : nameList) {
317 Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber);
321 } catch (CursorWindowAllocationException e) {
322 Log.e(TAG, "CursorWindowAllocationException while getting contact names");
324 if (contactCursor != null) {
325 contactCursor.close();
326 contactCursor = null;
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);
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;
346 String typeSelection = BluetoothPbapObexServer.createSelectionPara(type);
348 final Uri myUri = CallLog.Calls.CONTENT_URI;
349 final String[] CALLLOG_PROJECTION = new String[] {
350 CallLog.Calls._ID, // 0
352 final int ID_COLUMN_INDEX = 0;
354 Cursor callsCursor = null;
355 long startPointId = 0;
358 // Need test to see if order by _ID is ok here, or by date?
359 callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null,
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;
368 callsCursor.moveToPosition(endPoint - 1);
369 endPointId = callsCursor.getLong(ID_COLUMN_INDEX);
371 if (V) Log.v(TAG, "Call log query endPointId = " + endPointId);
373 } catch (CursorWindowAllocationException e) {
374 Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards");
376 if (callsCursor != null) {
382 String recordSelection;
383 if (startPoint == endPoint) {
384 recordSelection = Calls._ID + "=" + startPointId;
386 // The query to call table is by "_id DESC" order, so change
388 recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<="
393 if (typeSelection == null) {
394 selection = recordSelection;
396 selection = "(" + typeSelection + ") AND (" + recordSelection + ")";
399 if (V) Log.v(TAG, "Call log query selection is: " + selection);
401 return composeCallLogsAndSendVCards(op, selection, vcardType21, null, ignorefilter, filter);
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;
412 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
413 Cursor contactCursor = null;
414 Cursor contactIdCursor = new MatrixCursor(new String[] {
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,
424 } catch (CursorWindowAllocationException e) {
425 Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards");
427 if (contactCursor != null) {
428 contactCursor.close();
431 return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
432 ignorefilter, filter);
435 public final int composeAndSendPhonebookOneVcard(Operation op, final int offset,
436 final boolean vcardType21, String ownerVCard, int orderByWhat,
437 boolean ignorefilter, byte[] filter) {
439 Log.e(TAG, "Internal error: offset is not correct.");
440 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
442 final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext);
444 Cursor contactCursor = null;
445 Cursor contactIdCursor = new MatrixCursor(new String[] {
449 contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION,
450 CLAUSE_ONLY_VISIBLE, null, Phone.CONTACT_ID);
451 contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset);
453 } catch (CursorWindowAllocationException e) {
455 "CursorWindowAllocationException while composing phonebook one vcard");
457 if (contactCursor != null) {
458 contactCursor.close();
461 return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard,
462 ignorefilter, filter);
466 * Filter contact cursor by certain condition.
468 public static final class ContactCursorFilter {
471 * @param contactCursor
473 * @return a cursor containing contact id of {@code offset} contact.
475 public static Cursor filterByOffset(Cursor contactCursor, int offset) {
476 return filterByRange(contactCursor, offset, offset);
481 * @param contactCursor
484 * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th
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[]{
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);
507 return contactIdsCursor;
512 * Handler enterprise contact id in VCardComposer
514 private static class EnterpriseRawContactEntitlesInfoCallback implements
515 VCardComposer.RawContactEntitlesInfoCallback {
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);
522 return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId);
527 public final int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor,
528 final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) {
530 if (V) timestamp = System.currentTimeMillis();
532 VCardComposer composer = null;
533 VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter);
535 HandlerForStringBuffer buffer = null;
537 // Currently only support Generic Vcard 2.1 and 3.0
540 vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC;
542 vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC;
544 if (!vcardfilter.isPhotoEnabled()) {
545 vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
548 // Enhancement: customize Vcard based on preferences/settings and
550 composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null);
553 // BT does want PAUSE/WAIT conversion while it doesn't want the
555 // done by vCard library by default.
556 composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() {
557 public String onValueReceived(String rawValue, int type, String label,
559 // 'p' and 'w' are the standard characters for pause and
562 // so use those when exporting phone numbers via vCard.
563 String numberWithControlSequence = rawValue
564 .replace(PhoneNumberUtils.PAUSE, 'p').replace(PhoneNumberUtils.WAIT,
566 return numberWithControlSequence;
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;
577 while (!composer.isAfterLast()) {
578 if (BluetoothPbapObexServer.sIsAborted) {
579 ((ServerOperation) op).isAborted = true;
580 BluetoothPbapObexServer.sIsAborted = false;
583 String vcard = composer.createOneEntry();
586 "Failed to read a contact. Error reason: " + composer.getErrorReason());
587 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
589 if (V) Log.v(TAG, "vCard from composer: " + vcard);
591 vcard = vcardfilter.apply(vcard, vcardType21);
592 vcard = StripTelephoneNumber(vcard);
594 if (V) Log.v(TAG, "vCard after cleanup: " + vcard);
596 if (!buffer.onEntryCreated(vcard)) {
597 // onEntryCreate() already emits error.
598 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
602 if (composer != null) {
603 composer.terminate();
605 if (buffer != null) {
606 buffer.onTerminate();
610 if (V) Log.v(TAG, "Total vcard composing and sending out takes "
611 + (System.currentTimeMillis() - timestamp) + " ms");
613 return ResponseCodes.OBEX_HTTP_OK;
616 public final int composeCallLogsAndSendVCards(Operation op, final String selection,
617 final boolean vcardType21, String ownerVCard, boolean ignorefilter,
620 if (V) timestamp = System.currentTimeMillis();
622 BluetoothPbapCallLogComposer composer = null;
623 HandlerForStringBuffer buffer = null;
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;
633 while (!composer.isAfterLast()) {
634 if (BluetoothPbapObexServer.sIsAborted) {
635 ((ServerOperation) op).isAborted = true;
636 BluetoothPbapObexServer.sIsAborted = false;
639 String vcard = composer.createOneEntry(vcardType21);
642 "Failed to read a contact. Error reason: " + composer.getErrorReason());
643 return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
646 Log.v(TAG, "Vcard Entry:");
650 buffer.onEntryCreated(vcard);
653 if (composer != null) {
654 composer.terminate();
656 if (buffer != null) {
657 buffer.onTerminate();
661 if (V) Log.v(TAG, "Total vcard composing and sending out takes "
662 + (System.currentTimeMillis() - timestamp) + " ms");
663 return ResponseCodes.OBEX_HTTP_OK;
666 public String StripTelephoneNumber (String vCard){
667 String attr [] = vCard.split(System.getProperty("line.separator"));
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(" ", "");
678 for (int i=0; i < attr.length; i++) {
679 if(!attr[i].equals("")){
680 Vcard = Vcard.concat(attr[i] + "\n");
683 if (V) Log.v(TAG, "Vcard with stripped telephone no.: " + Vcard);
688 * Handler to emit vCards to PCE.
690 public class HandlerForStringBuffer {
691 private Operation operation;
693 private OutputStream outputStream;
695 private String phoneOwnVCard = null;
697 public HandlerForStringBuffer(Operation op, String ownerVCard) {
699 if (ownerVCard != null) {
700 phoneOwnVCard = ownerVCard;
701 if (V) Log.v(TAG, "phone own number vcard:");
702 if (V) Log.v(TAG, phoneOwnVCard);
706 private boolean write(String vCard) {
709 outputStream.write(vCard.getBytes());
712 } catch (IOException e) {
713 Log.e(TAG, "write outputstrem failed" + e.toString());
718 public boolean onInit(Context context) {
720 outputStream = operation.openOutputStream();
721 if (phoneOwnVCard != null) {
722 return write(phoneOwnVCard);
725 } catch (IOException e) {
726 Log.e(TAG, "open outputstrem failed" + e.toString());
731 public boolean onEntryCreated(String vcard) {
735 public void onTerminate() {
736 if (!BluetoothPbapObexServer.closeStream(outputStream, operation)) {
737 if (V) Log.v(TAG, "CloseStream failed!");
739 if (V) Log.v(TAG, "CloseStream ok!");
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);
758 public final int pos;
759 public final String prop;
760 public final boolean onlyCheckV21;
761 public final boolean excludeForV21;
763 FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) {
766 this.onlyCheckV21 = onlyCheckV21;
767 this.excludeForV21 = excludeForV21;
771 private static final String SEPARATOR = System.getProperty("line.separator");
772 private final byte[] filter;
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;
783 VCardFilter(byte[] filter) {
784 this.filter = filter;
787 public boolean isPhotoEnabled() {
788 return !isFilteredOut(FilterBit.PHOTO, false);
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;
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];
804 for (FilterBit bit : FilterBit.values()) {
805 if (bit.prop.equals(currentProp)) {
806 filteredOut = isFilteredOut(bit, vCardType21);
811 // Since PBAP does not have filter bits for IM and SIP,
812 // exclude them by default. Easiest way is to exclude all
814 if (currentProp.startsWith("X-")) filteredOut = true;
817 // Build filtered vCard
818 if (!filteredOut) filteredVCard.append(line + SEPARATOR);
821 return filteredVCard.toString();
825 private static final Uri getPhoneLookupFilterUri() {
826 return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI;
830 * Get size of the cursor without duplicated contact id. This assumes the
831 * given cursor is sorted by CONATCT_ID.
833 private static final int getDistinctContactIdSize(Cursor cursor) {
834 final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID);
835 long previousContactId = -1;
837 cursor.moveToPosition(-1);
838 while (cursor.moveToNext()) {
839 final long contactId = cursor.getLong(contactIdColumn);
840 if (previousContactId != contactId) {
842 previousContactId = contactId;
846 Log.i(TAG, "getDistinctContactIdSize result: " + count);
852 * Append "display_name,contact_id" string array from cursor to ArrayList.
853 * This assumes the given cursor is sorted by CONATCT_ID.
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;
868 if (previousContactId != contactId) {
869 previousContactId = contactId;
870 resultList.add(displayName + "," + contactId);
874 for (String nameId : resultList) {
875 Log.i(TAG, "appendDistinctNameIdList result: " + nameId);