Loading android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java +58 −41 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.accounts.AccountManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpPseRecord; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; Loading Loading @@ -184,6 +185,48 @@ public class PbapClientService extends ProfileService { setComponentAvailable(AUTHENTICATOR_SERVICE, false); } /** * Add our PBAP Client SDP record to the device SDP database * * <p>This allows our client to be recognized by the remove device. The record must be cleaned * up when we shutdown. */ private void registerSdpRecord() { SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e(TAG, "SdpManagerNativeInterface is not available"); return; } mSdpHandle = nativeInterface.createPbapPceRecord(SERVICE_NAME, PbapSdpRecord.VERSION_1_2); } /** * Remove our PBAP Client SDP record from the device SDP database * * <p>Gracefully removes PBAP Client support from our SDP records. Called when shutting down. */ private void cleanUpSdpRecord() { if (mSdpHandle < 0) { Log.e(TAG, "cleanUpSdpRecord, SDP record never created"); return; } int sdpHandle = mSdpHandle; mSdpHandle = -1; SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e( TAG, "cleanUpSdpRecord failed, SdpManagerNativeInterface is not available," + " sdpHandle=" + sdpHandle); return; } Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); if (!nativeInterface.removeSdpRecord(sdpHandle)) { Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); } } void cleanupDevice(BluetoothDevice device) { Log.d(TAG, "Cleanup device: " + device); synchronized (mPbapClientStateMachineMap) { Loading Loading @@ -277,39 +320,6 @@ public class PbapClientService extends ProfileService { } } private void registerSdpRecord() { SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e(TAG, "SdpManagerNativeInterface is not available"); return; } mSdpHandle = nativeInterface.createPbapPceRecord( SERVICE_NAME, PbapClientConnectionHandler.PBAP_V1_2); } private void cleanUpSdpRecord() { if (mSdpHandle < 0) { Log.e(TAG, "cleanUpSdpRecord, SDP record never created"); return; } int sdpHandle = mSdpHandle; mSdpHandle = -1; SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e( TAG, "cleanUpSdpRecord failed, SdpManagerNativeInterface is not available," + " sdpHandle=" + sdpHandle); return; } Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); if (!nativeInterface.removeSdpRecord(sdpHandle)) { Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); } } @VisibleForTesting class PbapBroadcastReceiver extends BroadcastReceiver { @Override Loading Loading @@ -399,11 +409,6 @@ public class PbapClientService extends ProfileService { */ public void receiveSdpSearchRecord( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) { PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device=" + device.toString()); return; } Log.v( TAG, "Received SDP record for UUID=" Loading @@ -412,9 +417,21 @@ public class PbapClientService extends ProfileService { + BluetoothUuid.PBAP_PSE.toString() + ")"); if (uuid.equals(BluetoothUuid.PBAP_PSE)) { PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device=" + device.toString()); return; } SdpPseRecord pseRecord = (SdpPseRecord) record; if (pseRecord != null) { stateMachine .obtainMessage(PbapClientStateMachine.MSG_SDP_COMPLETE, record) .obtainMessage( PbapClientStateMachine.MSG_SDP_COMPLETE, new PbapSdpRecord(device, pseRecord)) .sendToTarget(); } else { Log.w(TAG, "Received null PSE record for device=" + device); } } } Loading android/app/src/com/android/bluetooth/pbapclient/PbapSdpRecord.java 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.pbapclient; import android.bluetooth.BluetoothDevice; import android.bluetooth.SdpPseRecord; import java.util.Objects; /** * This object represents an SDP Record for the PBAP profile. It extends the framework class by * housing all the supported feature masks. */ public class PbapSdpRecord { public static final int VERSION_1_0 = 0x0100; public static final int VERSION_1_1 = 0x0101; public static final int VERSION_1_2 = 0x0102; public static final int FEATURES_EXCLUDED = -1; public static final int FEATURE_DOWNLOADING = 1 << 0; public static final int FEATURE_BROWSING = 1 << 1; public static final int FEATURE_DATABASE_IDENTIFIER = 1 << 2; public static final int FEATURE_FOLDER_VERSION_COUNTERS = 1 << 3; public static final int FEATURE_VCARD_SELECTING = 1 << 4; public static final int FEATURE_ENHANCED_MISSED_CALLS = 1 << 5; public static final int FEATURE_XBT_UCI_VCARD_PROPERTY = 1 << 6; public static final int FEATURE_XBT_UID_VCARD_PROPERTY = 1 << 7; public static final int FEATURE_CONTACT_REFERENCING = 1 << 8; public static final int FEATURE_DEFAULT_IMAGE_FORMAT = 1 << 9; // PBAP v1.2.3 Sec. 7.1.2 public static final int REPOSITORY_LOCAL_PHONEBOOK = 1 << 0; public static final int REPOSITORY_SIM_CARD = 1 << 1; public static final int REPOSITORY_SPEED_DIAL = 1 << 2; public static final int REPOSITORY_FAVORITES = 1 << 3; public static final int FIELD_MISSING = -1; private final BluetoothDevice mDevice; private final SdpPseRecord mSdpRecord; PbapSdpRecord(BluetoothDevice device, SdpPseRecord record) { mDevice = Objects.requireNonNull(device); mSdpRecord = Objects.requireNonNull(record); } /** Get the device associated with this SDP record */ public BluetoothDevice getDevice() { return mDevice; } /** Get the profile version associated with this SDP record */ public int getProfileVersion() { return mSdpRecord.getProfileVersion(); } /** Get the service name associated with this SDP record */ public String getServiceName() { return mSdpRecord.getServiceName(); } /** Get the L2CAP PSM associated with this SDP record */ public int getL2capPsm() { return mSdpRecord.getL2capPsm(); } /** Get the RFCOMM channel number associated with this SDP record */ public int getRfcommChannelNumber() { return mSdpRecord.getRfcommChannelNumber(); } /** Get the supported features associated with this SDP record */ public int getSupportedFeatures() { return mSdpRecord.getSupportedFeatures(); } /** Returns true if this SDP record supports a given feature */ public boolean isFeatureSupported(int feature) { int remoteFeatures = mSdpRecord.getSupportedFeatures(); if (remoteFeatures != FIELD_MISSING) { return (feature & remoteFeatures) != 0; } return false; } /** Git the supported repositories bitmask associated with this SDP record */ public int getSupportedRepositories() { return mSdpRecord.getSupportedRepositories(); } /** Returns true if this SDP record supports a given repository */ public boolean isRepositorySupported(int repository) { int remoteRepositories = mSdpRecord.getSupportedRepositories(); if (remoteRepositories != FIELD_MISSING) { return (repository & remoteRepositories) != 0; } return false; } /** Get a string representation of this SDP record */ @Override public String toString() { return mSdpRecord.toString(); } /** * Get a string representation of any of the SDP PBAP version constants * * <p>Version is represented as a series of specification defined constants, in the form: * 0x[Major 2 bytes][Minor 2 bytes] -> [Major].[Minor] * * <p>For example, 0x0102 is 1.2. */ public static String versionToString(int version) { switch (version) { case FIELD_MISSING: return "VERSION_UNKNOWN"; case VERSION_1_0: return "VERSION_1_0"; case VERSION_1_1: return "VERSION_1_1"; case VERSION_1_2: return "VERSION_1_2"; default: return "VERSION_UNRECOGNIZED_" + String.format("%04X", version); } } /** Get a string representation of any of the SDP feature constants */ public static String featureToString(int feature) { switch (feature) { case FEATURE_DOWNLOADING: return "FEATURE_DOWNLOADING"; case FEATURE_BROWSING: return "FEATURE_BROWSING"; case FEATURE_DATABASE_IDENTIFIER: return "FEATURE_DATABASE_IDENTIFIER"; case FEATURE_FOLDER_VERSION_COUNTERS: return "FEATURE_FOLDER_VERSION_COUNTERS"; case FEATURE_VCARD_SELECTING: return "FEATURE_VCARD_SELECTING"; case FEATURE_ENHANCED_MISSED_CALLS: return "FEATURE_ENHANCED_MISSED_CALLS"; case FEATURE_XBT_UCI_VCARD_PROPERTY: return "FEATURE_XBT_UCI_VCARD_PROPERTY"; case FEATURE_XBT_UID_VCARD_PROPERTY: return "FEATURE_XBT_UID_VCARD_PROPERTY"; case FEATURE_CONTACT_REFERENCING: return "FEATURE_CONTACT_REFERENCING"; case FEATURE_DEFAULT_IMAGE_FORMAT: return "FEATURE_DEFAULT_IMAGE_FORMAT"; default: return "FEATURE_RESERVED_BIT_" + feature; } } /** Get a string representation of any of the SDP repository constants */ public static String repositoryToString(int repository) { switch (repository) { case REPOSITORY_LOCAL_PHONEBOOK: return "REPOSITORY_LOCAL_PHONEBOOK"; case REPOSITORY_SIM_CARD: return "REPOSITORY_SIM_CARD"; case REPOSITORY_SPEED_DIAL: return "REPOSITORY_SPEED_DIAL"; case REPOSITORY_FAVORITES: return "REPOSITORY_FAVORITES"; default: return "REPOSITORY_RESERVED_BIT_" + repository; } } } android/app/src/com/android/bluetooth/pbapclient/obex/PbapClientConnectionHandler.java +18 −33 Original line number Diff line number Diff line Loading @@ -21,7 +21,6 @@ import android.annotation.SuppressLint; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpPseRecord; import android.content.Context; import android.os.Handler; import android.os.Looper; Loading Loading @@ -61,6 +60,9 @@ class PbapClientConnectionHandler extends Handler { static final int MSG_DISCONNECT = 2; static final int MSG_DOWNLOAD = 3; static final int L2CAP_INVALID_PSM = -1; static final int RFCOMM_INVALID_CHANNEL_ID = -1; // The following constants are pulled from the Bluetooth Phone Book Access Profile specification // 1.1 private static final byte[] PBAP_TARGET = Loading @@ -83,27 +85,15 @@ class PbapClientConnectionHandler extends Handler { 0x66 }; private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200; private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001; private static final int PBAP_SUPPORTED_FEATURE = PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_DOWNLOADING; @VisibleForTesting static final int L2CAP_INVALID_PSM = -1; // PBAP v1.2.3 Sec. 7.1.2 private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0; private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1; private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3; public static final int PBAP_V1_2 = 0x0102; private static final int PBAP_SUPPORTED_FEATURES = PbapSdpRecord.FEATURE_DEFAULT_IMAGE_FORMAT | PbapSdpRecord.FEATURE_DOWNLOADING; private Account mAccount; private AccountManager mAccountManager; private BluetoothSocket mSocket; private final BluetoothDevice mDevice; // PSE SDP Record for current device. private SdpPseRecord mPseRec = null; private PbapSdpRecord mPseRec = null; private ClientSession mObexSession; private Context mContext; private PbapClientObexAuthenticator mAuth = null; Loading Loading @@ -166,7 +156,8 @@ class PbapClientConnectionHandler extends Handler { Log.d(TAG, "Handling Message = " + msg.what); switch (msg.what) { case MSG_CONNECT: mPseRec = (SdpPseRecord) msg.obj; mPseRec = (PbapSdpRecord) msg.obj; /* To establish a connection, first open a socket and then create an OBEX session */ if (connectSocket()) { Log.d(TAG, "Socket connected"); Loading Loading @@ -215,13 +206,13 @@ class PbapClientConnectionHandler extends Handler { Log.e(TAG, "Account creation failed."); return; } if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_FAVORITES)) { downloadContacts(PbapPhonebook.FAVORITES_PATH); } if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_LOCAL_PHONEBOOK)) { downloadContacts(PbapPhonebook.LOCAL_PHONEBOOK_PATH); } if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_SIM_CARD)) { downloadContacts(PbapPhonebook.SIM_PHONEBOOK_PATH); } Loading @@ -237,7 +228,7 @@ class PbapClientConnectionHandler extends Handler { } @VisibleForTesting synchronized void setPseRecord(SdpPseRecord record) { synchronized void setPseRecord(PbapSdpRecord record) { mPseRec = record; } Loading @@ -261,9 +252,12 @@ class PbapClientConnectionHandler extends Handler { } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) { Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm()); mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm()); } else { } else if (mPseRec.getRfcommChannelNumber() != RFCOMM_INVALID_CHANNEL_ID) { Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber()); mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber()); } else { Log.w(TAG, "connectSocket: transport PSM or channel ID not specified"); return false; } if (mSocket != null) { Loading Loading @@ -298,10 +292,10 @@ class PbapClientConnectionHandler extends Handler { ObexAppParameters oap = new ObexAppParameters(); if (mPseRec.getProfileVersion() >= PBAP_V1_2) { if (mPseRec.getProfileVersion() >= PbapSdpRecord.VERSION_1_2) { oap.add( PbapApplicationParameters.OAP_PBAP_SUPPORTED_FEATURES, PBAP_SUPPORTED_FEATURE); PBAP_SUPPORTED_FEATURES); } oap.addToHeaderSet(connectionRequest); Loading Loading @@ -469,13 +463,4 @@ class PbapClientConnectionHandler extends Handler { Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); } } @VisibleForTesting boolean isRepositorySupported(int mask) { if (mPseRec == null) { Log.v(TAG, "No PBAP Server SDP Record"); return false; } return (mask & mPseRec.getSupportedRepositories()) != 0; } } android/app/src/com/android/bluetooth/pbapclient/obex/PbapClientRequest.java +56 −50 Original line number Diff line number Diff line Loading @@ -29,91 +29,97 @@ import java.io.InputStream; abstract class PbapClientRequest { static final String TAG = PbapClientRequest.class.getSimpleName(); protected HeaderSet mHeaderSet; // Request Types public static final int TYPE_PULL_PHONEBOOK_METADATA = 0; public static final int TYPE_PULL_PHONEBOOK = 1; protected int mResponseCode; private boolean mAborted = false; private ClientOperation mOp = null; protected HeaderSet mHeaderSet = new HeaderSet(); private int mResponseCode = -1; PbapClientRequest() { mHeaderSet = new HeaderSet(); mResponseCode = -1; } public final boolean isSuccess() { return (mResponseCode == ResponseCodes.OBEX_HTTP_OK); /** * A function that returns the type of the request. * * <p>Used to determine type instead of using 'instanceof' */ public abstract int getType(); /** * Get the actual response code associated with the request * * @return The response code as in integer */ public final int getResponseCode() { return mResponseCode; } /** * A generica operation, providing overridable hooks to read response headers and content. * * <p>All PBAP Client operations are GET OBEX operations, so that is what this is. */ public void execute(ClientSession session) throws IOException { Log.v(TAG, "execute"); /* in case request is aborted before can be executed */ if (mAborted) { mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; return; } ClientOperation operation = null; try { mOp = (ClientOperation) session.get(mHeaderSet); operation = (ClientOperation) session.get(mHeaderSet); /* make sure final flag for GET is used (PBAP spec 6.2.2) */ mOp.setGetFinalFlag(true); operation.setGetFinalFlag(true); /* * this will trigger ClientOperation to use non-buffered stream so * we can abort operation */ mOp.continueOperation(true, false); readResponseHeaders(mOp.getReceivedHeader()); InputStream is = mOp.openInputStream(); readResponse(is); is.close(); mOp.close(); operation.continueOperation(true, false); mResponseCode = mOp.getResponseCode(); Log.d(TAG, "mResponseCode=" + mResponseCode); checkResponseCode(mResponseCode); readResponseHeaders(operation.getReceivedHeader()); InputStream inputStream = operation.openInputStream(); readResponse(inputStream); inputStream.close(); mResponseCode = operation.getResponseCode(); } catch (IOException e) { Log.e(TAG, "IOException occurred when processing request", e); mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; Log.e(TAG, "IOException occurred when processing request", e); throw e; } } public void abort() { mAborted = true; if (mOp != null) { try { mOp.abort(); } catch (IOException e) { Log.e(TAG, "Exception occurred when trying to abort", e); } finally { // Always close the operation so the next operation can successfully complete if (operation != null) { operation.close(); } } } protected void readResponse(InputStream stream) throws IOException { Log.v(TAG, "readResponse"); /* nothing here by default */ } protected void readResponseHeaders(HeaderSet headerset) { Log.v(TAG, "readResponseHeaders"); /* nothing here by default */ } protected void checkResponseCode(int responseCode) throws IOException { Log.v(TAG, "checkResponseCode"); public static String typeToString(int type) { switch (type) { case TYPE_PULL_PHONEBOOK_METADATA: return "TYPE_PULL_PHONEBOOK_METADATA"; case TYPE_PULL_PHONEBOOK: return "TYPE_PULL_PHONEBOOK"; default: return "TYPE_RESERVED (" + type + ")"; } } /* nothing here by default */ @Override public String toString() { return "<" + TAG + (" type=" + typeToString(getType())) + (", responseCode=" + getResponseCode()) + ">"; } } android/app/src/com/android/bluetooth/pbapclient/obex/RequestPullPhonebook.java +5 −4 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.bluetooth.pbapclient; import android.accounts.Account; import android.util.Log; import com.android.bluetooth.ObexAppParameters; import com.android.obex.HeaderSet; Loading @@ -40,6 +39,11 @@ final class RequestPullPhonebook extends PbapClientRequest { private PbapPhonebook mResponse; @Override public int getType() { return TYPE_PULL_PHONEBOOK; } RequestPullPhonebook(String phonebook, PbapApplicationParameters params, Account account) { mPhonebook = phonebook; mFormat = params.getVcardFormat(); Loading Loading @@ -80,10 +84,7 @@ final class RequestPullPhonebook extends PbapClientRequest { @Override protected void readResponse(InputStream stream) throws IOException { Log.v(TAG, "readResponse"); mResponse = new PbapPhonebook(mPhonebook, mFormat, mListStartOffset, mAccount, stream); Log.d(TAG, "Read " + mResponse.getCount() + " entries"); } public String getPhonebook() { Loading Loading
android/app/src/com/android/bluetooth/pbapclient/PbapClientService.java +58 −41 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.accounts.AccountManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpPseRecord; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; Loading Loading @@ -184,6 +185,48 @@ public class PbapClientService extends ProfileService { setComponentAvailable(AUTHENTICATOR_SERVICE, false); } /** * Add our PBAP Client SDP record to the device SDP database * * <p>This allows our client to be recognized by the remove device. The record must be cleaned * up when we shutdown. */ private void registerSdpRecord() { SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e(TAG, "SdpManagerNativeInterface is not available"); return; } mSdpHandle = nativeInterface.createPbapPceRecord(SERVICE_NAME, PbapSdpRecord.VERSION_1_2); } /** * Remove our PBAP Client SDP record from the device SDP database * * <p>Gracefully removes PBAP Client support from our SDP records. Called when shutting down. */ private void cleanUpSdpRecord() { if (mSdpHandle < 0) { Log.e(TAG, "cleanUpSdpRecord, SDP record never created"); return; } int sdpHandle = mSdpHandle; mSdpHandle = -1; SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e( TAG, "cleanUpSdpRecord failed, SdpManagerNativeInterface is not available," + " sdpHandle=" + sdpHandle); return; } Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); if (!nativeInterface.removeSdpRecord(sdpHandle)) { Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); } } void cleanupDevice(BluetoothDevice device) { Log.d(TAG, "Cleanup device: " + device); synchronized (mPbapClientStateMachineMap) { Loading Loading @@ -277,39 +320,6 @@ public class PbapClientService extends ProfileService { } } private void registerSdpRecord() { SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e(TAG, "SdpManagerNativeInterface is not available"); return; } mSdpHandle = nativeInterface.createPbapPceRecord( SERVICE_NAME, PbapClientConnectionHandler.PBAP_V1_2); } private void cleanUpSdpRecord() { if (mSdpHandle < 0) { Log.e(TAG, "cleanUpSdpRecord, SDP record never created"); return; } int sdpHandle = mSdpHandle; mSdpHandle = -1; SdpManagerNativeInterface nativeInterface = SdpManagerNativeInterface.getInstance(); if (!nativeInterface.isAvailable()) { Log.e( TAG, "cleanUpSdpRecord failed, SdpManagerNativeInterface is not available," + " sdpHandle=" + sdpHandle); return; } Log.i(TAG, "cleanUpSdpRecord, mSdpHandle=" + sdpHandle); if (!nativeInterface.removeSdpRecord(sdpHandle)) { Log.e(TAG, "cleanUpSdpRecord, removeSdpRecord failed, sdpHandle=" + sdpHandle); } } @VisibleForTesting class PbapBroadcastReceiver extends BroadcastReceiver { @Override Loading Loading @@ -399,11 +409,6 @@ public class PbapClientService extends ProfileService { */ public void receiveSdpSearchRecord( BluetoothDevice device, int status, Parcelable record, ParcelUuid uuid) { PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device=" + device.toString()); return; } Log.v( TAG, "Received SDP record for UUID=" Loading @@ -412,9 +417,21 @@ public class PbapClientService extends ProfileService { + BluetoothUuid.PBAP_PSE.toString() + ")"); if (uuid.equals(BluetoothUuid.PBAP_PSE)) { PbapClientStateMachine stateMachine = mPbapClientStateMachineMap.get(device); if (stateMachine == null) { Log.e(TAG, "No Statemachine found for the device=" + device.toString()); return; } SdpPseRecord pseRecord = (SdpPseRecord) record; if (pseRecord != null) { stateMachine .obtainMessage(PbapClientStateMachine.MSG_SDP_COMPLETE, record) .obtainMessage( PbapClientStateMachine.MSG_SDP_COMPLETE, new PbapSdpRecord(device, pseRecord)) .sendToTarget(); } else { Log.w(TAG, "Received null PSE record for device=" + device); } } } Loading
android/app/src/com/android/bluetooth/pbapclient/PbapSdpRecord.java 0 → 100644 +186 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.bluetooth.pbapclient; import android.bluetooth.BluetoothDevice; import android.bluetooth.SdpPseRecord; import java.util.Objects; /** * This object represents an SDP Record for the PBAP profile. It extends the framework class by * housing all the supported feature masks. */ public class PbapSdpRecord { public static final int VERSION_1_0 = 0x0100; public static final int VERSION_1_1 = 0x0101; public static final int VERSION_1_2 = 0x0102; public static final int FEATURES_EXCLUDED = -1; public static final int FEATURE_DOWNLOADING = 1 << 0; public static final int FEATURE_BROWSING = 1 << 1; public static final int FEATURE_DATABASE_IDENTIFIER = 1 << 2; public static final int FEATURE_FOLDER_VERSION_COUNTERS = 1 << 3; public static final int FEATURE_VCARD_SELECTING = 1 << 4; public static final int FEATURE_ENHANCED_MISSED_CALLS = 1 << 5; public static final int FEATURE_XBT_UCI_VCARD_PROPERTY = 1 << 6; public static final int FEATURE_XBT_UID_VCARD_PROPERTY = 1 << 7; public static final int FEATURE_CONTACT_REFERENCING = 1 << 8; public static final int FEATURE_DEFAULT_IMAGE_FORMAT = 1 << 9; // PBAP v1.2.3 Sec. 7.1.2 public static final int REPOSITORY_LOCAL_PHONEBOOK = 1 << 0; public static final int REPOSITORY_SIM_CARD = 1 << 1; public static final int REPOSITORY_SPEED_DIAL = 1 << 2; public static final int REPOSITORY_FAVORITES = 1 << 3; public static final int FIELD_MISSING = -1; private final BluetoothDevice mDevice; private final SdpPseRecord mSdpRecord; PbapSdpRecord(BluetoothDevice device, SdpPseRecord record) { mDevice = Objects.requireNonNull(device); mSdpRecord = Objects.requireNonNull(record); } /** Get the device associated with this SDP record */ public BluetoothDevice getDevice() { return mDevice; } /** Get the profile version associated with this SDP record */ public int getProfileVersion() { return mSdpRecord.getProfileVersion(); } /** Get the service name associated with this SDP record */ public String getServiceName() { return mSdpRecord.getServiceName(); } /** Get the L2CAP PSM associated with this SDP record */ public int getL2capPsm() { return mSdpRecord.getL2capPsm(); } /** Get the RFCOMM channel number associated with this SDP record */ public int getRfcommChannelNumber() { return mSdpRecord.getRfcommChannelNumber(); } /** Get the supported features associated with this SDP record */ public int getSupportedFeatures() { return mSdpRecord.getSupportedFeatures(); } /** Returns true if this SDP record supports a given feature */ public boolean isFeatureSupported(int feature) { int remoteFeatures = mSdpRecord.getSupportedFeatures(); if (remoteFeatures != FIELD_MISSING) { return (feature & remoteFeatures) != 0; } return false; } /** Git the supported repositories bitmask associated with this SDP record */ public int getSupportedRepositories() { return mSdpRecord.getSupportedRepositories(); } /** Returns true if this SDP record supports a given repository */ public boolean isRepositorySupported(int repository) { int remoteRepositories = mSdpRecord.getSupportedRepositories(); if (remoteRepositories != FIELD_MISSING) { return (repository & remoteRepositories) != 0; } return false; } /** Get a string representation of this SDP record */ @Override public String toString() { return mSdpRecord.toString(); } /** * Get a string representation of any of the SDP PBAP version constants * * <p>Version is represented as a series of specification defined constants, in the form: * 0x[Major 2 bytes][Minor 2 bytes] -> [Major].[Minor] * * <p>For example, 0x0102 is 1.2. */ public static String versionToString(int version) { switch (version) { case FIELD_MISSING: return "VERSION_UNKNOWN"; case VERSION_1_0: return "VERSION_1_0"; case VERSION_1_1: return "VERSION_1_1"; case VERSION_1_2: return "VERSION_1_2"; default: return "VERSION_UNRECOGNIZED_" + String.format("%04X", version); } } /** Get a string representation of any of the SDP feature constants */ public static String featureToString(int feature) { switch (feature) { case FEATURE_DOWNLOADING: return "FEATURE_DOWNLOADING"; case FEATURE_BROWSING: return "FEATURE_BROWSING"; case FEATURE_DATABASE_IDENTIFIER: return "FEATURE_DATABASE_IDENTIFIER"; case FEATURE_FOLDER_VERSION_COUNTERS: return "FEATURE_FOLDER_VERSION_COUNTERS"; case FEATURE_VCARD_SELECTING: return "FEATURE_VCARD_SELECTING"; case FEATURE_ENHANCED_MISSED_CALLS: return "FEATURE_ENHANCED_MISSED_CALLS"; case FEATURE_XBT_UCI_VCARD_PROPERTY: return "FEATURE_XBT_UCI_VCARD_PROPERTY"; case FEATURE_XBT_UID_VCARD_PROPERTY: return "FEATURE_XBT_UID_VCARD_PROPERTY"; case FEATURE_CONTACT_REFERENCING: return "FEATURE_CONTACT_REFERENCING"; case FEATURE_DEFAULT_IMAGE_FORMAT: return "FEATURE_DEFAULT_IMAGE_FORMAT"; default: return "FEATURE_RESERVED_BIT_" + feature; } } /** Get a string representation of any of the SDP repository constants */ public static String repositoryToString(int repository) { switch (repository) { case REPOSITORY_LOCAL_PHONEBOOK: return "REPOSITORY_LOCAL_PHONEBOOK"; case REPOSITORY_SIM_CARD: return "REPOSITORY_SIM_CARD"; case REPOSITORY_SPEED_DIAL: return "REPOSITORY_SPEED_DIAL"; case REPOSITORY_FAVORITES: return "REPOSITORY_FAVORITES"; default: return "REPOSITORY_RESERVED_BIT_" + repository; } } }
android/app/src/com/android/bluetooth/pbapclient/obex/PbapClientConnectionHandler.java +18 −33 Original line number Diff line number Diff line Loading @@ -21,7 +21,6 @@ import android.annotation.SuppressLint; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothSocket; import android.bluetooth.BluetoothUuid; import android.bluetooth.SdpPseRecord; import android.content.Context; import android.os.Handler; import android.os.Looper; Loading Loading @@ -61,6 +60,9 @@ class PbapClientConnectionHandler extends Handler { static final int MSG_DISCONNECT = 2; static final int MSG_DOWNLOAD = 3; static final int L2CAP_INVALID_PSM = -1; static final int RFCOMM_INVALID_CHANNEL_ID = -1; // The following constants are pulled from the Bluetooth Phone Book Access Profile specification // 1.1 private static final byte[] PBAP_TARGET = Loading @@ -83,27 +85,15 @@ class PbapClientConnectionHandler extends Handler { 0x66 }; private static final int PBAP_FEATURE_DEFAULT_IMAGE_FORMAT = 0x00000200; private static final int PBAP_FEATURE_DOWNLOADING = 0x00000001; private static final int PBAP_SUPPORTED_FEATURE = PBAP_FEATURE_DEFAULT_IMAGE_FORMAT | PBAP_FEATURE_DOWNLOADING; @VisibleForTesting static final int L2CAP_INVALID_PSM = -1; // PBAP v1.2.3 Sec. 7.1.2 private static final int SUPPORTED_REPOSITORIES_LOCALPHONEBOOK = 1 << 0; private static final int SUPPORTED_REPOSITORIES_SIMCARD = 1 << 1; private static final int SUPPORTED_REPOSITORIES_FAVORITES = 1 << 3; public static final int PBAP_V1_2 = 0x0102; private static final int PBAP_SUPPORTED_FEATURES = PbapSdpRecord.FEATURE_DEFAULT_IMAGE_FORMAT | PbapSdpRecord.FEATURE_DOWNLOADING; private Account mAccount; private AccountManager mAccountManager; private BluetoothSocket mSocket; private final BluetoothDevice mDevice; // PSE SDP Record for current device. private SdpPseRecord mPseRec = null; private PbapSdpRecord mPseRec = null; private ClientSession mObexSession; private Context mContext; private PbapClientObexAuthenticator mAuth = null; Loading Loading @@ -166,7 +156,8 @@ class PbapClientConnectionHandler extends Handler { Log.d(TAG, "Handling Message = " + msg.what); switch (msg.what) { case MSG_CONNECT: mPseRec = (SdpPseRecord) msg.obj; mPseRec = (PbapSdpRecord) msg.obj; /* To establish a connection, first open a socket and then create an OBEX session */ if (connectSocket()) { Log.d(TAG, "Socket connected"); Loading Loading @@ -215,13 +206,13 @@ class PbapClientConnectionHandler extends Handler { Log.e(TAG, "Account creation failed."); return; } if (isRepositorySupported(SUPPORTED_REPOSITORIES_FAVORITES)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_FAVORITES)) { downloadContacts(PbapPhonebook.FAVORITES_PATH); } if (isRepositorySupported(SUPPORTED_REPOSITORIES_LOCALPHONEBOOK)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_LOCAL_PHONEBOOK)) { downloadContacts(PbapPhonebook.LOCAL_PHONEBOOK_PATH); } if (isRepositorySupported(SUPPORTED_REPOSITORIES_SIMCARD)) { if (mPseRec.isRepositorySupported(PbapSdpRecord.REPOSITORY_SIM_CARD)) { downloadContacts(PbapPhonebook.SIM_PHONEBOOK_PATH); } Loading @@ -237,7 +228,7 @@ class PbapClientConnectionHandler extends Handler { } @VisibleForTesting synchronized void setPseRecord(SdpPseRecord record) { synchronized void setPseRecord(PbapSdpRecord record) { mPseRec = record; } Loading @@ -261,9 +252,12 @@ class PbapClientConnectionHandler extends Handler { } else if (mPseRec.getL2capPsm() != L2CAP_INVALID_PSM) { Log.v(TAG, "connectSocket: PSM: " + mPseRec.getL2capPsm()); mSocket = mDevice.createL2capSocket(mPseRec.getL2capPsm()); } else { } else if (mPseRec.getRfcommChannelNumber() != RFCOMM_INVALID_CHANNEL_ID) { Log.v(TAG, "connectSocket: channel: " + mPseRec.getRfcommChannelNumber()); mSocket = mDevice.createRfcommSocket(mPseRec.getRfcommChannelNumber()); } else { Log.w(TAG, "connectSocket: transport PSM or channel ID not specified"); return false; } if (mSocket != null) { Loading Loading @@ -298,10 +292,10 @@ class PbapClientConnectionHandler extends Handler { ObexAppParameters oap = new ObexAppParameters(); if (mPseRec.getProfileVersion() >= PBAP_V1_2) { if (mPseRec.getProfileVersion() >= PbapSdpRecord.VERSION_1_2) { oap.add( PbapApplicationParameters.OAP_PBAP_SUPPORTED_FEATURES, PBAP_SUPPORTED_FEATURE); PBAP_SUPPORTED_FEATURES); } oap.addToHeaderSet(connectionRequest); Loading Loading @@ -469,13 +463,4 @@ class PbapClientConnectionHandler extends Handler { Log.d(TAG, "Call Logs could not be deleted, they may not exist yet."); } } @VisibleForTesting boolean isRepositorySupported(int mask) { if (mPseRec == null) { Log.v(TAG, "No PBAP Server SDP Record"); return false; } return (mask & mPseRec.getSupportedRepositories()) != 0; } }
android/app/src/com/android/bluetooth/pbapclient/obex/PbapClientRequest.java +56 −50 Original line number Diff line number Diff line Loading @@ -29,91 +29,97 @@ import java.io.InputStream; abstract class PbapClientRequest { static final String TAG = PbapClientRequest.class.getSimpleName(); protected HeaderSet mHeaderSet; // Request Types public static final int TYPE_PULL_PHONEBOOK_METADATA = 0; public static final int TYPE_PULL_PHONEBOOK = 1; protected int mResponseCode; private boolean mAborted = false; private ClientOperation mOp = null; protected HeaderSet mHeaderSet = new HeaderSet(); private int mResponseCode = -1; PbapClientRequest() { mHeaderSet = new HeaderSet(); mResponseCode = -1; } public final boolean isSuccess() { return (mResponseCode == ResponseCodes.OBEX_HTTP_OK); /** * A function that returns the type of the request. * * <p>Used to determine type instead of using 'instanceof' */ public abstract int getType(); /** * Get the actual response code associated with the request * * @return The response code as in integer */ public final int getResponseCode() { return mResponseCode; } /** * A generica operation, providing overridable hooks to read response headers and content. * * <p>All PBAP Client operations are GET OBEX operations, so that is what this is. */ public void execute(ClientSession session) throws IOException { Log.v(TAG, "execute"); /* in case request is aborted before can be executed */ if (mAborted) { mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; return; } ClientOperation operation = null; try { mOp = (ClientOperation) session.get(mHeaderSet); operation = (ClientOperation) session.get(mHeaderSet); /* make sure final flag for GET is used (PBAP spec 6.2.2) */ mOp.setGetFinalFlag(true); operation.setGetFinalFlag(true); /* * this will trigger ClientOperation to use non-buffered stream so * we can abort operation */ mOp.continueOperation(true, false); readResponseHeaders(mOp.getReceivedHeader()); InputStream is = mOp.openInputStream(); readResponse(is); is.close(); mOp.close(); operation.continueOperation(true, false); mResponseCode = mOp.getResponseCode(); Log.d(TAG, "mResponseCode=" + mResponseCode); checkResponseCode(mResponseCode); readResponseHeaders(operation.getReceivedHeader()); InputStream inputStream = operation.openInputStream(); readResponse(inputStream); inputStream.close(); mResponseCode = operation.getResponseCode(); } catch (IOException e) { Log.e(TAG, "IOException occurred when processing request", e); mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR; Log.e(TAG, "IOException occurred when processing request", e); throw e; } } public void abort() { mAborted = true; if (mOp != null) { try { mOp.abort(); } catch (IOException e) { Log.e(TAG, "Exception occurred when trying to abort", e); } finally { // Always close the operation so the next operation can successfully complete if (operation != null) { operation.close(); } } } protected void readResponse(InputStream stream) throws IOException { Log.v(TAG, "readResponse"); /* nothing here by default */ } protected void readResponseHeaders(HeaderSet headerset) { Log.v(TAG, "readResponseHeaders"); /* nothing here by default */ } protected void checkResponseCode(int responseCode) throws IOException { Log.v(TAG, "checkResponseCode"); public static String typeToString(int type) { switch (type) { case TYPE_PULL_PHONEBOOK_METADATA: return "TYPE_PULL_PHONEBOOK_METADATA"; case TYPE_PULL_PHONEBOOK: return "TYPE_PULL_PHONEBOOK"; default: return "TYPE_RESERVED (" + type + ")"; } } /* nothing here by default */ @Override public String toString() { return "<" + TAG + (" type=" + typeToString(getType())) + (", responseCode=" + getResponseCode()) + ">"; } }
android/app/src/com/android/bluetooth/pbapclient/obex/RequestPullPhonebook.java +5 −4 Original line number Diff line number Diff line Loading @@ -17,7 +17,6 @@ package com.android.bluetooth.pbapclient; import android.accounts.Account; import android.util.Log; import com.android.bluetooth.ObexAppParameters; import com.android.obex.HeaderSet; Loading @@ -40,6 +39,11 @@ final class RequestPullPhonebook extends PbapClientRequest { private PbapPhonebook mResponse; @Override public int getType() { return TYPE_PULL_PHONEBOOK; } RequestPullPhonebook(String phonebook, PbapApplicationParameters params, Account account) { mPhonebook = phonebook; mFormat = params.getVcardFormat(); Loading Loading @@ -80,10 +84,7 @@ final class RequestPullPhonebook extends PbapClientRequest { @Override protected void readResponse(InputStream stream) throws IOException { Log.v(TAG, "readResponse"); mResponse = new PbapPhonebook(mPhonebook, mFormat, mListStartOffset, mAccount, stream); Log.d(TAG, "Read " + mResponse.getCount() + " entries"); } public String getPhonebook() { Loading