/* * Copyright (c) 2008-2009, Motorola, Inc. * Copyright (C) 2009-2012, Broadcom Corporation * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * - Neither the name of the Motorola, Inc. nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package com.android.bluetooth.pbap; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.database.CursorWindowAllocationException; import android.database.MatrixCursor; import android.net.Uri; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.RawContactsEntity; import android.telephony.PhoneNumberUtils; import android.text.TextUtils; import android.util.Log; import com.android.bluetooth.R; import com.android.bluetooth.util.DevicePolicyUtils; import com.android.vcard.VCardComposer; import com.android.vcard.VCardConfig; import com.android.vcard.VCardPhoneNumberTranslationCallback; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import javax.obex.Operation; import javax.obex.ResponseCodes; import javax.obex.ServerOperation; public class BluetoothPbapVcardManager { private static final String TAG = "BluetoothPbapVcardManager"; private static final boolean V = BluetoothPbapService.VERBOSE; private ContentResolver mResolver; private Context mContext; private static final int PHONE_NUMBER_COLUMN_INDEX = 3; static final String SORT_ORDER_PHONE_NUMBER = CommonDataKinds.Phone.NUMBER + " ASC"; static final String[] PHONES_CONTACTS_PROJECTION = new String[]{ Phone.CONTACT_ID, // 0 Phone.DISPLAY_NAME, // 1 }; static final String[] PHONE_LOOKUP_PROJECTION = new String[]{ PhoneLookup._ID, PhoneLookup.DISPLAY_NAME }; static final int CONTACTS_ID_COLUMN_INDEX = 0; static final int CONTACTS_NAME_COLUMN_INDEX = 1; static long sLastFetchedTimeStamp; // call histories use dynamic handles, and handles should order by date; the // most recently one should be the first handle. In table "calls", _id and // date are consistent in ordering, to implement simply, we sort by _id // here. static final String CALLLOG_SORT_ORDER = Calls._ID + " DESC"; private static final int NEED_SEND_BODY = -1; public BluetoothPbapVcardManager(final Context context) { mContext = context; mResolver = mContext.getContentResolver(); sLastFetchedTimeStamp = System.currentTimeMillis(); } /** * Create an owner vcard from the configured profile * @param vcardType21 * @return */ private String getOwnerPhoneNumberVcardFromProfile(final boolean vcardType21, final byte[] filter) { // Currently only support Generic Vcard 2.1 and 3.0 int vcardType; if (vcardType21) { vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; } else { vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; } if (!BluetoothPbapConfig.includePhotosInVcard()) { vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; } return BluetoothPbapUtils.createProfileVCard(mContext, vcardType, filter); } public final String getOwnerPhoneNumberVcard(final boolean vcardType21, final byte[] filter) { //Owner vCard enhancement: Use "ME" profile if configured if (BluetoothPbapConfig.useProfileForOwnerVcard()) { String vcard = getOwnerPhoneNumberVcardFromProfile(vcardType21, filter); if (vcard != null && vcard.length() != 0) { return vcard; } } //End enhancement BluetoothPbapCallLogComposer composer = new BluetoothPbapCallLogComposer(mContext); String name = BluetoothPbapService.getLocalPhoneName(); String number = BluetoothPbapService.getLocalPhoneNum(); String vcard = composer.composeVCardForPhoneOwnNumber(Phone.TYPE_MOBILE, name, number, vcardType21); return vcard; } public final int getPhonebookSize(final int type) { int size; switch (type) { case BluetoothPbapObexServer.ContentType.PHONEBOOK: size = getContactsSize(); break; default: size = getCallHistorySize(type); break; } if (V) { Log.v(TAG, "getPhonebookSize size = " + size + " type = " + type); } return size; } public final int getContactsSize() { final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; try { contactCursor = mResolver.query(myUri, new String[]{Phone.CONTACT_ID}, null, null, Phone.CONTACT_ID); if (contactCursor == null) { return 0; } return getDistinctContactIdSize(contactCursor) + 1; // always has the 0.vcf } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while getting Contacts size"); } finally { if (contactCursor != null) { contactCursor.close(); } } return 0; } public final int getCallHistorySize(final int type) { final Uri myUri = CallLog.Calls.CONTENT_URI; String selection = BluetoothPbapObexServer.createSelectionPara(type); int size = 0; Cursor callCursor = null; try { callCursor = mResolver.query(myUri, null, selection, null, CallLog.Calls.DEFAULT_SORT_ORDER); if (callCursor != null) { size = callCursor.getCount(); } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while getting CallHistory size"); } finally { if (callCursor != null) { callCursor.close(); callCursor = null; } } return size; } private static final int CALLS_NUMBER_COLUMN_INDEX = 0; private static final int CALLS_NAME_COLUMN_INDEX = 1; private static final int CALLS_NUMBER_PRESENTATION_COLUMN_INDEX = 2; public final ArrayList loadCallHistoryList(final int type) { final Uri myUri = CallLog.Calls.CONTENT_URI; String selection = BluetoothPbapObexServer.createSelectionPara(type); String[] projection = new String[]{ Calls.NUMBER, Calls.CACHED_NAME, Calls.NUMBER_PRESENTATION }; Cursor callCursor = null; ArrayList list = new ArrayList(); try { callCursor = mResolver.query(myUri, projection, selection, null, CALLLOG_SORT_ORDER); if (callCursor != null) { for (callCursor.moveToFirst(); !callCursor.isAfterLast(); callCursor.moveToNext()) { String name = callCursor.getString(CALLS_NAME_COLUMN_INDEX); if (TextUtils.isEmpty(name)) { // name not found, use number instead final int numberPresentation = callCursor.getInt(CALLS_NUMBER_PRESENTATION_COLUMN_INDEX); if (numberPresentation != Calls.PRESENTATION_ALLOWED) { name = mContext.getString(R.string.unknownNumber); } else { name = callCursor.getString(CALLS_NUMBER_COLUMN_INDEX); } } list.add(name); } } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while loading CallHistory"); } finally { if (callCursor != null) { callCursor.close(); callCursor = null; } } return list; } public final ArrayList getPhonebookNameList(final int orderByWhat) { ArrayList nameList = new ArrayList(); //Owner vCard enhancement. Use "ME" profile if configured String ownerName = null; if (BluetoothPbapConfig.useProfileForOwnerVcard()) { ownerName = BluetoothPbapUtils.getProfileName(mContext); } if (ownerName == null || ownerName.length() == 0) { ownerName = BluetoothPbapService.getLocalPhoneName(); } nameList.add(ownerName); //End enhancement final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; // By default order is indexed String orderBy = Phone.CONTACT_ID; try { if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { orderBy = Phone.DISPLAY_NAME; } contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); if (contactCursor != null) { appendDistinctNameIdList(nameList, mContext.getString(android.R.string.unknownName), contactCursor); } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while getting phonebook name list"); } catch (Exception e) { Log.e(TAG, "Exception while getting phonebook name list", e); } finally { if (contactCursor != null) { contactCursor.close(); contactCursor = null; } } return nameList; } final ArrayList getSelectedPhonebookNameList(final int orderByWhat, final boolean vcardType21, int needSendBody, int pbSize, byte[] selector, String vcardselectorop) { ArrayList nameList = new ArrayList(); PropertySelector vcardselector = new PropertySelector(selector); VCardComposer composer = null; int vcardType; if (vcardType21) { vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; } else { vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; } composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { @Override public String onValueReceived(String rawValue, int type, String label, boolean isPrimary) { String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p') .replace(PhoneNumberUtils.WAIT, 'w'); return numberWithControlSequence; } }); // Owner vCard enhancement. Use "ME" profile if configured String ownerName = null; if (BluetoothPbapConfig.useProfileForOwnerVcard()) { ownerName = BluetoothPbapUtils.getProfileName(mContext); } if (ownerName == null || ownerName.length() == 0) { ownerName = BluetoothPbapService.getLocalPhoneName(); } nameList.add(ownerName); // End enhancement final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; try { contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID); ArrayList contactNameIdList = new ArrayList(); appendDistinctNameIdList(contactNameIdList, mContext.getString(android.R.string.unknownName), contactCursor); if (contactCursor != null) { if (!composer.initWithCallback(contactCursor, new EnterpriseRawContactEntitlesInfoCallback())) { return nameList; } int i = 0; contactCursor.moveToFirst(); while (!composer.isAfterLast()) { String vcard = composer.createOneEntry(); if (vcard == null) { Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason()); return nameList; } else if (vcard.isEmpty()) { Log.i(TAG, "Contact may have been deleted during operation"); continue; } if (V) { Log.v(TAG, "Checking selected bits in the vcard composer" + vcard); } if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) { Log.e(TAG, "vcard selector check fail"); vcard = null; pbSize--; continue; } else { String name = vcardselector.getName(vcard); if (TextUtils.isEmpty(name)) { name = mContext.getString(android.R.string.unknownName); } nameList.add(contactNameIdList.get(i)); } i++; } if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_INDEXED) { if (V) { Log.v(TAG, "getPhonebookNameList, order by index"); } // Do not need to do anything, as we sort it by index already } else if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { if (V) { Log.v(TAG, "getPhonebookNameList, order by alpha"); } Collections.sort(nameList); } } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while getting Phonebook name list"); } finally { if (contactCursor != null) { contactCursor.close(); contactCursor = null; } } return nameList; } public final ArrayList getContactNamesByNumber(final String phoneNumber) { ArrayList nameList = new ArrayList(); ArrayList tempNameList = new ArrayList(); Cursor contactCursor = null; Uri uri = null; String[] projection = null; if (TextUtils.isEmpty(phoneNumber)) { uri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); projection = PHONES_CONTACTS_PROJECTION; } else { uri = Uri.withAppendedPath(getPhoneLookupFilterUri(), Uri.encode(phoneNumber)); projection = PHONE_LOOKUP_PROJECTION; } try { contactCursor = mResolver.query(uri, projection, null, null, Phone.CONTACT_ID); if (contactCursor != null) { appendDistinctNameIdList(nameList, mContext.getString(android.R.string.unknownName), contactCursor); if (V) { for (String nameIdStr : nameList) { Log.v(TAG, "got name " + nameIdStr + " by number " + phoneNumber); } } } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while getting contact names"); } finally { if (contactCursor != null) { contactCursor.close(); contactCursor = null; } } int tempListSize = tempNameList.size(); for (int index = 0; index < tempListSize; index++) { String object = tempNameList.get(index); if (!nameList.contains(object)) { nameList.add(object); } } return nameList; } byte[] getCallHistoryPrimaryFolderVersion(final int type) { final Uri myUri = CallLog.Calls.CONTENT_URI; String selection = BluetoothPbapObexServer.createSelectionPara(type); selection = selection + " AND date >= " + sLastFetchedTimeStamp; Log.d(TAG, "LAST_FETCHED_TIME_STAMP is " + sLastFetchedTimeStamp); Cursor callCursor = null; long count = 0; long primaryVcMsb = 0; ArrayList list = new ArrayList(); try { callCursor = mResolver.query(myUri, null, selection, null, null); while (callCursor != null && callCursor.moveToNext()) { count = count + 1; } } catch (Exception e) { Log.e(TAG, "exception while fetching callHistory pvc"); } finally { if (callCursor != null) { callCursor.close(); callCursor = null; } } sLastFetchedTimeStamp = System.currentTimeMillis(); Log.d(TAG, "getCallHistoryPrimaryFolderVersion count is " + count + " type is " + type); ByteBuffer pvc = ByteBuffer.allocate(16); pvc.putLong(primaryVcMsb); Log.d(TAG, "primaryVersionCounter is " + BluetoothPbapUtils.sPrimaryVersionCounter); pvc.putLong(count); return pvc.array(); } private static final String[] CALLLOG_PROJECTION = new String[]{ CallLog.Calls._ID, // 0 }; private static final int ID_COLUMN_INDEX = 0; final int composeAndSendSelectedCallLogVcards(final int type, Operation op, final int startPoint, final int endPoint, final boolean vcardType21, int needSendBody, int pbSize, boolean ignorefilter, byte[] filter, byte[] vcardselector, String vcardselectorop, boolean vcardselect) { if (startPoint < 1 || startPoint > endPoint) { Log.e(TAG, "internal error: startPoint or endPoint is not correct."); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } String typeSelection = BluetoothPbapObexServer.createSelectionPara(type); final Uri myUri = CallLog.Calls.CONTENT_URI; Cursor callsCursor = null; long startPointId = 0; long endPointId = 0; try { // Need test to see if order by _ID is ok here, or by date? callsCursor = mResolver.query(myUri, CALLLOG_PROJECTION, typeSelection, null, CALLLOG_SORT_ORDER); if (callsCursor != null) { callsCursor.moveToPosition(startPoint - 1); startPointId = callsCursor.getLong(ID_COLUMN_INDEX); if (V) { Log.v(TAG, "Call Log query startPointId = " + startPointId); } if (startPoint == endPoint) { endPointId = startPointId; } else { callsCursor.moveToPosition(endPoint - 1); endPointId = callsCursor.getLong(ID_COLUMN_INDEX); } if (V) { Log.v(TAG, "Call log query endPointId = " + endPointId); } } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while composing calllog vcards"); } finally { if (callsCursor != null) { callsCursor.close(); callsCursor = null; } } String recordSelection; if (startPoint == endPoint) { recordSelection = Calls._ID + "=" + startPointId; } else { // The query to call table is by "_id DESC" order, so change // correspondingly. recordSelection = Calls._ID + ">=" + endPointId + " AND " + Calls._ID + "<=" + startPointId; } String selection; if (typeSelection == null) { selection = recordSelection; } else { selection = "(" + typeSelection + ") AND (" + recordSelection + ")"; } if (V) { Log.v(TAG, "Call log query selection is: " + selection); } return composeCallLogsAndSendSelectedVCards(op, selection, vcardType21, needSendBody, pbSize, null, ignorefilter, filter, vcardselector, vcardselectorop, vcardselect); } final int composeAndSendPhonebookVcards(Operation op, final int startPoint, final int endPoint, final boolean vcardType21, String ownerVCard, int needSendBody, int pbSize, boolean ignorefilter, byte[] filter, byte[] vcardselector, String vcardselectorop, boolean vcardselect) { if (startPoint < 1 || startPoint > endPoint) { Log.e(TAG, "internal error: startPoint or endPoint is not correct."); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; Cursor contactIdCursor = new MatrixCursor(new String[]{ Phone.CONTACT_ID }); try { contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, Phone.CONTACT_ID); if (contactCursor != null) { contactIdCursor = ContactCursorFilter.filterByRange(contactCursor, startPoint, endPoint); } } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while composing phonebook vcards"); } finally { if (contactCursor != null) { contactCursor.close(); } } if (vcardselect) { return composeContactsAndSendSelectedVCards(op, contactIdCursor, vcardType21, ownerVCard, needSendBody, pbSize, ignorefilter, filter, vcardselector, vcardselectorop); } else { return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter); } } final int composeAndSendPhonebookOneVcard(Operation op, final int offset, final boolean vcardType21, String ownerVCard, int orderByWhat, boolean ignorefilter, byte[] filter) { if (offset < 1) { Log.e(TAG, "Internal error: offset is not correct."); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } final Uri myUri = DevicePolicyUtils.getEnterprisePhoneUri(mContext); Cursor contactCursor = null; Cursor contactIdCursor = new MatrixCursor(new String[]{ Phone.CONTACT_ID }); // By default order is indexed String orderBy = Phone.CONTACT_ID; try { if (orderByWhat == BluetoothPbapObexServer.ORDER_BY_ALPHABETICAL) { orderBy = Phone.DISPLAY_NAME; } contactCursor = mResolver.query(myUri, PHONES_CONTACTS_PROJECTION, null, null, orderBy); } catch (CursorWindowAllocationException e) { Log.e(TAG, "CursorWindowAllocationException while composing phonebook one vcard"); } finally { if (contactCursor != null) { contactIdCursor = ContactCursorFilter.filterByOffset(contactCursor, offset); contactCursor.close(); contactCursor = null; } } return composeContactsAndSendVCards(op, contactIdCursor, vcardType21, ownerVCard, ignorefilter, filter); } /** * Filter contact cursor by certain condition. */ private static final class ContactCursorFilter { /** * * @param contactCursor * @param offset * @return a cursor containing contact id of {@code offset} contact. */ public static Cursor filterByOffset(Cursor contactCursor, int offset) { return filterByRange(contactCursor, offset, offset); } /** * * @param contactCursor * @param startPoint * @param endPoint * @return a cursor containing contact ids of {@code startPoint}th to {@code endPoint}th * contact. */ public static Cursor filterByRange(Cursor contactCursor, int startPoint, int endPoint) { final int contactIdColumn = contactCursor.getColumnIndex(Data.CONTACT_ID); long previousContactId = -1; // As startPoint, endOffset index starts from 1 to n, we set // currentPoint base as 1 not 0 int currentOffset = 1; final MatrixCursor contactIdsCursor = new MatrixCursor(new String[]{ Phone.CONTACT_ID }); while (contactCursor.moveToNext() && currentOffset <= endPoint) { long currentContactId = contactCursor.getLong(contactIdColumn); if (previousContactId != currentContactId) { previousContactId = currentContactId; if (currentOffset >= startPoint) { contactIdsCursor.addRow(new Long[]{currentContactId}); if (V) { Log.v(TAG, "contactIdsCursor.addRow: " + currentContactId); } } currentOffset++; } } return contactIdsCursor; } } /** * Handler enterprise contact id in VCardComposer */ private static class EnterpriseRawContactEntitlesInfoCallback implements VCardComposer.RawContactEntitlesInfoCallback { @Override public VCardComposer.RawContactEntitlesInfo getRawContactEntitlesInfo(long contactId) { if (Contacts.isEnterpriseContactId(contactId)) { return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CORP_CONTENT_URI, contactId - Contacts.ENTERPRISE_CONTACT_ID_BASE); } else { return new VCardComposer.RawContactEntitlesInfo(RawContactsEntity.CONTENT_URI, contactId); } } } private int composeContactsAndSendVCards(Operation op, final Cursor contactIdCursor, final boolean vcardType21, String ownerVCard, boolean ignorefilter, byte[] filter) { long timestamp = 0; if (V) { timestamp = System.currentTimeMillis(); } VCardComposer composer = null; VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); HandlerForStringBuffer buffer = null; try { // Currently only support Generic Vcard 2.1 and 3.0 int vcardType; if (vcardType21) { vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; vcardType |= VCardConfig.FLAG_CONVERT_PHONETIC_NAME_STRINGS; vcardType |= VCardConfig.FLAG_REFRAIN_QP_TO_NAME_PROPERTIES; } else { vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; } if (!vcardfilter.isPhotoEnabled()) { vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; } // Enhancement: customize Vcard based on preferences/settings and // input from caller composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); // End enhancement // BT does want PAUSE/WAIT conversion while it doesn't want the // other formatting // done by vCard library by default. composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { @Override public String onValueReceived(String rawValue, int type, String label, boolean isPrimary) { // 'p' and 'w' are the standard characters for pause and // wait // (see RFC 3601) // so use those when exporting phone numbers via vCard. String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p') .replace(PhoneNumberUtils.WAIT, 'w'); return numberWithControlSequence; } }); buffer = new HandlerForStringBuffer(op, ownerVCard); Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); if (!composer.initWithCallback(contactIdCursor, new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) { return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } while (!composer.isAfterLast()) { if (BluetoothPbapObexServer.sIsAborted) { ((ServerOperation) op).isAborted = true; BluetoothPbapObexServer.sIsAborted = false; break; } String vcard = composer.createOneEntry(); if (vcard == null) { Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } else if (vcard.isEmpty()) { Log.i(TAG, "Contact may have been deleted during operation"); continue; } if (V) { Log.v(TAG, "vCard from composer: " + vcard); } vcard = vcardfilter.apply(vcard, vcardType21); vcard = stripTelephoneNumber(vcard); if (V) { Log.v(TAG, "vCard after cleanup: " + vcard); } if (!buffer.onEntryCreated(vcard)) { // onEntryCreate() already emits error. return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } } finally { if (composer != null) { composer.terminate(); } if (buffer != null) { buffer.onTerminate(); } } if (V) { Log.v(TAG, "Total vcard composing and sending out takes " + (System.currentTimeMillis() - timestamp) + " ms"); } return ResponseCodes.OBEX_HTTP_OK; } private int composeContactsAndSendSelectedVCards(Operation op, final Cursor contactIdCursor, final boolean vcardType21, String ownerVCard, int needSendBody, int pbSize, boolean ignorefilter, byte[] filter, byte[] selector, String vcardselectorop) { long timestamp = 0; if (V) { timestamp = System.currentTimeMillis(); } VCardComposer composer = null; VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); PropertySelector vcardselector = new PropertySelector(selector); HandlerForStringBuffer buffer = null; try { // Currently only support Generic Vcard 2.1 and 3.0 int vcardType; if (vcardType21) { vcardType = VCardConfig.VCARD_TYPE_V21_GENERIC; } else { vcardType = VCardConfig.VCARD_TYPE_V30_GENERIC; } if (!vcardfilter.isPhotoEnabled()) { vcardType |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT; } // Enhancement: customize Vcard based on preferences/settings and // input from caller composer = BluetoothPbapUtils.createFilteredVCardComposer(mContext, vcardType, null); // End enhancement /* BT does want PAUSE/WAIT conversion while it doesn't want the * other formatting done by vCard library by default. */ composer.setPhoneNumberTranslationCallback(new VCardPhoneNumberTranslationCallback() { @Override public String onValueReceived(String rawValue, int type, String label, boolean isPrimary) { /* 'p' and 'w' are the standard characters for pause and wait * (see RFC 3601) so use those when exporting phone numbers via vCard.*/ String numberWithControlSequence = rawValue.replace(PhoneNumberUtils.PAUSE, 'p') .replace(PhoneNumberUtils.WAIT, 'w'); return numberWithControlSequence; } }); buffer = new HandlerForStringBuffer(op, ownerVCard); Log.v(TAG, "contactIdCursor size: " + contactIdCursor.getCount()); if (!composer.initWithCallback(contactIdCursor, new EnterpriseRawContactEntitlesInfoCallback()) || !buffer.onInit(mContext)) { return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } while (!composer.isAfterLast()) { if (BluetoothPbapObexServer.sIsAborted) { ((ServerOperation) op).isAborted = true; BluetoothPbapObexServer.sIsAborted = false; break; } String vcard = composer.createOneEntry(); if (vcard == null) { Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } else if (vcard.isEmpty()) { Log.i(TAG, "Contact may have been deleted during operation"); continue; } if (V) { Log.v(TAG, "Checking selected bits in the vcard composer" + vcard); } if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) { Log.e(TAG, "vcard selector check fail"); vcard = null; pbSize--; continue; } Log.e(TAG, "vcard selector check pass"); if (needSendBody == NEED_SEND_BODY) { vcard = vcardfilter.apply(vcard, vcardType21); vcard = stripTelephoneNumber(vcard); if (V) { Log.v(TAG, "vCard after cleanup: " + vcard); } if (!buffer.onEntryCreated(vcard)) { // onEntryCreate() already emits error. return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } } } if (needSendBody != NEED_SEND_BODY) { return pbSize; } } finally { if (composer != null) { composer.terminate(); } if (buffer != null) { buffer.onTerminate(); } } if (V) { Log.v(TAG, "Total vcard composing and sending out takes " + (System.currentTimeMillis() - timestamp) + " ms"); } return ResponseCodes.OBEX_HTTP_OK; } private int composeCallLogsAndSendSelectedVCards(Operation op, final String selection, final boolean vcardType21, int needSendBody, int pbSize, String ownerVCard, boolean ignorefilter, byte[] filter, byte[] selector, String vcardselectorop, boolean vCardSelct) { long timestamp = 0; if (V) { timestamp = System.currentTimeMillis(); } BluetoothPbapCallLogComposer composer = null; HandlerForStringBuffer buffer = null; try { VCardFilter vcardfilter = new VCardFilter(ignorefilter ? null : filter); PropertySelector vcardselector = new PropertySelector(selector); composer = new BluetoothPbapCallLogComposer(mContext); buffer = new HandlerForStringBuffer(op, ownerVCard); if (!composer.init(CallLog.Calls.CONTENT_URI, selection, null, CALLLOG_SORT_ORDER) || !buffer.onInit(mContext)) { return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } while (!composer.isAfterLast()) { if (BluetoothPbapObexServer.sIsAborted) { ((ServerOperation) op).isAborted = true; BluetoothPbapObexServer.sIsAborted = false; break; } String vcard = composer.createOneEntry(vcardType21); if (vCardSelct) { if (!vcardselector.checkVCardSelector(vcard, vcardselectorop)) { Log.e(TAG, "Checking vcard selector for call log"); vcard = null; pbSize--; continue; } if (needSendBody == NEED_SEND_BODY) { if (vcard == null) { Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } else if (vcard.isEmpty()) { Log.i(TAG, "Call Log may have been deleted during operation"); continue; } vcard = vcardfilter.apply(vcard, vcardType21); if (V) { Log.v(TAG, "Vcard Entry:"); Log.v(TAG, vcard); } buffer.onEntryCreated(vcard); } } else { if (vcard == null) { Log.e(TAG, "Failed to read a contact. Error reason: " + composer.getErrorReason()); return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; } if (V) { Log.v(TAG, "Vcard Entry:"); Log.v(TAG, vcard); } buffer.onEntryCreated(vcard); } } if (needSendBody != NEED_SEND_BODY && vCardSelct) { return pbSize; } } finally { if (composer != null) { composer.terminate(); } if (buffer != null) { buffer.onTerminate(); } } if (V) { Log.v(TAG, "Total vcard composing and sending out takes " + (System.currentTimeMillis() - timestamp) + " ms"); } return ResponseCodes.OBEX_HTTP_OK; } public String stripTelephoneNumber(String vCard) { String[] attr = vCard.split(System.getProperty("line.separator")); String stripedVCard = ""; for (int i = 0; i < attr.length; i++) { if (attr[i].startsWith("TEL")) { String[] vTagAndTel = attr[i].split(":", 2); int telLenBefore = vTagAndTel[1].length(); // Remove '-', '(', ')' or ' ' from TEL number vTagAndTel[1] = vTagAndTel[1].replace("-", "") .replace("(", "") .replace(")", "") .replace(" ", ""); if (vTagAndTel[1].length() < telLenBefore) { if (V) { Log.v(TAG, "Fixing vCard TEL to " + vTagAndTel[1]); } attr[i] = new StringBuilder().append(vTagAndTel[0]).append(":") .append(vTagAndTel[1]).toString(); } } } for (int i = 0; i < attr.length; i++) { if (!attr[i].isEmpty()) { stripedVCard = stripedVCard.concat(attr[i] + "\n"); } } if (V) { Log.v(TAG, "vCard with stripped telephone no.: " + stripedVCard); } return stripedVCard; } /** * Handler to emit vCards to PCE. */ public class HandlerForStringBuffer { private Operation mOperation; private OutputStream mOutputStream; private String mPhoneOwnVCard = null; public HandlerForStringBuffer(Operation op, String ownerVCard) { mOperation = op; if (ownerVCard != null) { mPhoneOwnVCard = ownerVCard; if (V) { Log.v(TAG, "phone own number vcard:"); } if (V) { Log.v(TAG, mPhoneOwnVCard); } } } private boolean write(String vCard) { try { if (vCard != null) { mOutputStream.write(vCard.getBytes()); return true; } } catch (IOException e) { Log.e(TAG, "write outputstrem failed" + e.toString()); } return false; } public boolean onInit(Context context) { try { mOutputStream = mOperation.openOutputStream(); if (mPhoneOwnVCard != null) { return write(mPhoneOwnVCard); } return true; } catch (IOException e) { Log.e(TAG, "open outputstrem failed" + e.toString()); } return false; } public boolean onEntryCreated(String vcard) { return write(vcard); } public void onTerminate() { if (!BluetoothPbapObexServer.closeStream(mOutputStream, mOperation)) { if (V) { Log.v(TAG, "CloseStream failed!"); } } else { if (V) { Log.v(TAG, "CloseStream ok!"); } } } } public static class VCardFilter { private enum FilterBit { // bit property onlyCheckV21 excludeForV21 FN(1, "FN", true, false), PHOTO(3, "PHOTO", false, false), BDAY(4, "BDAY", false, false), ADR(5, "ADR", false, false), EMAIL(8, "EMAIL", false, false), TITLE(12, "TITLE", false, false), ORG(16, "ORG", false, false), NOTE(17, "NOTE", false, false), SOUND(19, "SOUND", false, false), URL(20, "URL", false, false), NICKNAME(23, "NICKNAME", false, true), DATETIME(28, "X-IRMC-CALL-DATETIME", false, false); public final int pos; public final String prop; public final boolean onlyCheckV21; public final boolean excludeForV21; FilterBit(int pos, String prop, boolean onlyCheckV21, boolean excludeForV21) { this.pos = pos; this.prop = prop; this.onlyCheckV21 = onlyCheckV21; this.excludeForV21 = excludeForV21; } } private static final String SEPARATOR = System.getProperty("line.separator"); private final byte[] mFilter; //This function returns true if the attributes needs to be included in the filtered vcard. private boolean isFilteredIn(FilterBit bit, boolean vCardType21) { final int offset = (bit.pos / 8) + 1; final int bitPos = bit.pos % 8; if (!vCardType21 && bit.onlyCheckV21) { return true; } if (vCardType21 && bit.excludeForV21) { return false; } if (mFilter == null || offset >= mFilter.length) { return true; } return ((mFilter[mFilter.length - offset] >> bitPos) & 0x01) != 0; } VCardFilter(byte[] filter) { this.mFilter = filter; } public boolean isPhotoEnabled() { return isFilteredIn(FilterBit.PHOTO, false); } public String apply(String vCard, boolean vCardType21) { if (mFilter == null) { return vCard; } String[] lines = vCard.split(SEPARATOR); StringBuilder filteredVCard = new StringBuilder(); boolean filteredIn = false; for (String line : lines) { // Check whether the current property is changing (ignoring multi-line properties) // and determine if the current property is filtered in. if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { String currentProp = line.split("[;:]")[0]; filteredIn = true; for (FilterBit bit : FilterBit.values()) { if (bit.prop.equals(currentProp)) { filteredIn = isFilteredIn(bit, vCardType21); break; } } // Since PBAP does not have filter bits for IM and SIP, // exclude them by default. Easiest way is to exclude all // X- fields, except date time.... if (currentProp.startsWith("X-")) { filteredIn = false; if (currentProp.equals("X-IRMC-CALL-DATETIME")) { filteredIn = true; } } } // Build filtered vCard if (filteredIn) { filteredVCard.append(line + SEPARATOR); } } return filteredVCard.toString(); } } private static class PropertySelector { private enum PropertyMask { // bit property VERSION(0, "VERSION"), FN(1, "FN"), NAME(2, "N"), PHOTO(3, "PHOTO"), BDAY(4, "BDAY"), ADR(5, "ADR"), LABEL(6, "LABEL"), TEL(7, "TEL"), EMAIL(8, "EMAIL"), TITLE(12, "TITLE"), ORG(16, "ORG"), NOTE(17, "NOTE"), URL(20, "URL"), NICKNAME(23, "NICKNAME"), DATETIME(28, "DATETIME"); public final int pos; public final String prop; PropertyMask(int pos, String prop) { this.pos = pos; this.prop = prop; } } private static final String SEPARATOR = System.getProperty("line.separator"); private final byte[] mSelector; PropertySelector(byte[] selector) { this.mSelector = selector; } private boolean checkbit(int attrBit, byte[] selector) { int selectorlen = selector.length; if (((selector[selectorlen - 1 - ((int) attrBit / 8)] >> (attrBit % 8)) & 0x01) == 0) { return false; } return true; } private boolean checkprop(String vcard, String prop) { String[] lines = vcard.split(SEPARATOR); boolean isPresent = false; for (String line : lines) { if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { String currentProp = line.split("[;:]")[0]; if (prop.equals(currentProp)) { Log.d(TAG, "bit.prop.equals current prop :" + prop); isPresent = true; return isPresent; } } } return isPresent; } private boolean checkVCardSelector(String vcard, String vcardselectorop) { boolean selectedIn = true; for (PropertyMask bit : PropertyMask.values()) { if (checkbit(bit.pos, mSelector)) { Log.d(TAG, "checking for prop :" + bit.prop); if (vcardselectorop.equals("0")) { if (checkprop(vcard, bit.prop)) { Log.d(TAG, "bit.prop.equals current prop :" + bit.prop); selectedIn = true; break; } else { selectedIn = false; } } else if (vcardselectorop.equals("1")) { if (!checkprop(vcard, bit.prop)) { Log.d(TAG, "bit.prop.notequals current prop" + bit.prop); selectedIn = false; return selectedIn; } else { selectedIn = true; } } } } return selectedIn; } private String getName(String vcard) { String[] lines = vcard.split(SEPARATOR); String name = ""; for (String line : lines) { if (!Character.isWhitespace(line.charAt(0)) && !line.startsWith("=")) { if (line.startsWith("N:")) { name = line.substring(line.lastIndexOf(':'), line.length()); } } } Log.d(TAG, "returning name: " + name); return name; } } private static Uri getPhoneLookupFilterUri() { return PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI; } /** * Get size of the cursor without duplicated contact id. This assumes the * given cursor is sorted by CONTACT_ID. */ private static int getDistinctContactIdSize(Cursor cursor) { final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); final int idColumn = cursor.getColumnIndex(Data._ID); long previousContactId = -1; int count = 0; cursor.moveToPosition(-1); while (cursor.moveToNext()) { final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); if (previousContactId != contactId) { count++; previousContactId = contactId; } } if (V) { Log.i(TAG, "getDistinctContactIdSize result: " + count); } return count; } /** * Append "display_name,contact_id" string array from cursor to ArrayList. * This assumes the given cursor is sorted by CONTACT_ID. */ private static void appendDistinctNameIdList(ArrayList resultList, String defaultName, Cursor cursor) { final int contactIdColumn = cursor.getColumnIndex(Data.CONTACT_ID); final int idColumn = cursor.getColumnIndex(Data._ID); final int nameColumn = cursor.getColumnIndex(Data.DISPLAY_NAME); cursor.moveToPosition(-1); while (cursor.moveToNext()) { final long contactId = cursor.getLong(contactIdColumn != -1 ? contactIdColumn : idColumn); String displayName = nameColumn != -1 ? cursor.getString(nameColumn) : defaultName; if (TextUtils.isEmpty(displayName)) { displayName = defaultName; } String newString = displayName + "," + contactId; if (!resultList.contains(newString)) { resultList.add(newString); } } if (V) { for (String nameId : resultList) { Log.i(TAG, "appendDistinctNameIdList result: " + nameId); } } } }