/* * Copyright (C) 2015 Samsung System LSI * 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.sdp; import android.bluetooth.BluetoothDevice; import android.bluetooth.SdpMasRecord; import android.bluetooth.SdpMnsRecord; import android.bluetooth.SdpOppOpsRecord; import android.bluetooth.SdpPseRecord; import android.bluetooth.SdpRecord; import android.bluetooth.SdpSapsRecord; import android.content.Intent; import android.os.Handler; import android.os.Message; import android.os.ParcelUuid; import android.os.Parcelable; import android.util.Log; import com.android.bluetooth.Utils; import com.android.bluetooth.btservice.AbstractionLayer; import com.android.bluetooth.btservice.AdapterService; import java.util.ArrayList; import java.util.Arrays; public class SdpManager { private static final boolean D = true; private static final boolean V = false; private static final String TAG = "SdpManager"; // TODO: When changing PBAP to use this new API. // Move the defines to the profile (PBAP already have the feature bits) /* PBAP repositories */ public static final byte PBAP_REPO_LOCAL = 0x01 << 0; public static final byte PBAP_REPO_SIM = 0x01 << 1; public static final byte PBAP_REPO_SPEED_DAIL = 0x01 << 2; public static final byte PBAP_REPO_FAVORITES = 0x01 << 3; /* Variables to keep track of ongoing and queued search requests. * mTrackerLock must be held, when using/changing sSdpSearchTracker * and mSearchInProgress. */ static SdpSearchTracker sSdpSearchTracker; static boolean sSearchInProgress = false; static final Object TRACKER_LOCK = new Object(); /* The timeout to wait for reply from native. Should never fire. */ private static final int SDP_INTENT_DELAY = 11000; private static final int MESSAGE_SDP_INTENT = 2; // We need a reference to the adapter service, to be able to send intents private static AdapterService sAdapterService; private static boolean sNativeAvailable; // This object is a singleton private static SdpManager sSdpManager = null; static { classInitNative(); } private static native void classInitNative(); private native void initializeNative(); private native void cleanupNative(); private native boolean sdpSearchNative(byte[] address, byte[] uuid); private native int sdpCreateMapMasRecordNative(String serviceName, int masId, int rfcommChannel, int l2capPsm, int version, int msgTypes, int features); private native int sdpCreateMapMnsRecordNative(String serviceName, int rfcommChannel, int l2capPsm, int version, int features); private native int sdpCreatePbapPseRecordNative(String serviceName, int rfcommChannel, int l2capPsm, int version, int repositories, int features); private native int sdpCreateOppOpsRecordNative(String serviceName, int rfcommChannel, int l2capPsm, int version, byte[] formatsList); private native int sdpCreateSapsRecordNative(String serviceName, int rfcommChannel, int version); private native boolean sdpRemoveSdpRecordNative(int recordId); /* Inner class used for wrapping sdp search instance data */ private class SdpSearchInstance { private final BluetoothDevice mDevice; private final ParcelUuid mUuid; private int mStatus = 0; private boolean mSearching; /* TODO: If we change the API to use another mechanism than intents for * delivering the results, this would be the place to keep a list * of the objects to deliver the results to. */ SdpSearchInstance(int status, BluetoothDevice device, ParcelUuid uuid) { this.mDevice = device; this.mUuid = uuid; this.mStatus = status; mSearching = true; } public BluetoothDevice getDevice() { return mDevice; } public ParcelUuid getUuid() { return mUuid; } public int getStatus() { return mStatus; } public void setStatus(int status) { this.mStatus = status; } public void startSearch() { mSearching = true; Message message = mHandler.obtainMessage(MESSAGE_SDP_INTENT, this); mHandler.sendMessageDelayed(message, SDP_INTENT_DELAY); } public void stopSearch() { if (mSearching) { mHandler.removeMessages(MESSAGE_SDP_INTENT, this); } mSearching = false; } public boolean isSearching() { return mSearching; } } /* We wrap the ArrayList class to decorate with functionality to * find an instance based on UUID AND device address. * As we use a mix of byte[] and object instances, this is more * efficient than implementing comparable. */ class SdpSearchTracker { private final ArrayList mList = new ArrayList(); void clear() { mList.clear(); } boolean add(SdpSearchInstance inst) { return mList.add(inst); } boolean remove(SdpSearchInstance inst) { return mList.remove(inst); } SdpSearchInstance getNext() { if (mList.size() > 0) { return mList.get(0); } return null; } SdpSearchInstance getSearchInstance(byte[] address, byte[] uuidBytes) { String addressString = Utils.getAddressStringFromByte(address); ParcelUuid uuid = Utils.byteArrayToUuid(uuidBytes)[0]; for (SdpSearchInstance inst : mList) { if (inst.getDevice().getAddress().equals(addressString) && inst.getUuid() .equals(uuid)) { return inst; } } return null; } boolean isSearching(BluetoothDevice device, ParcelUuid uuid) { String addressString = device.getAddress(); for (SdpSearchInstance inst : mList) { if (inst.getDevice().getAddress().equals(addressString) && inst.getUuid() .equals(uuid)) { return inst.isSearching(); } } return false; } } private SdpManager(AdapterService adapterService) { sSdpSearchTracker = new SdpSearchTracker(); /* This is only needed until intents are no longer used */ sAdapterService = adapterService; initializeNative(); sNativeAvailable = true; } public static SdpManager init(AdapterService adapterService) { sSdpManager = new SdpManager(adapterService); return sSdpManager; } public static SdpManager getDefaultManager() { return sSdpManager; } public void cleanup() { if (sSdpSearchTracker != null) { synchronized (TRACKER_LOCK) { sSdpSearchTracker.clear(); } } if (sNativeAvailable) { cleanupNative(); sNativeAvailable = false; } sSdpManager = null; } void sdpMasRecordFoundCallback(int status, byte[] address, byte[] uuid, int masInstanceId, int l2capPsm, int rfcommCannelNumber, int profileVersion, int supportedFeatures, int supportedMessageTypes, String serviceName, boolean moreResults) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpMasRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { sdpRecord = new SdpMasRecord(masInstanceId, l2capPsm, rfcommCannelNumber, profileVersion, supportedFeatures, supportedMessageTypes, serviceName); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, moreResults); } } void sdpMnsRecordFoundCallback(int status, byte[] address, byte[] uuid, int l2capPsm, int rfcommCannelNumber, int profileVersion, int supportedFeatures, String serviceName, boolean moreResults) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpMnsRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { sdpRecord = new SdpMnsRecord(l2capPsm, rfcommCannelNumber, profileVersion, supportedFeatures, serviceName); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, moreResults); } } void sdpPseRecordFoundCallback(int status, byte[] address, byte[] uuid, int l2capPsm, int rfcommCannelNumber, int profileVersion, int supportedFeatures, int supportedRepositories, String serviceName, boolean moreResults) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpPseRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { sdpRecord = new SdpPseRecord(l2capPsm, rfcommCannelNumber, profileVersion, supportedFeatures, supportedRepositories, serviceName); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, moreResults); } } void sdpOppOpsRecordFoundCallback(int status, byte[] address, byte[] uuid, int l2capPsm, int rfcommCannelNumber, int profileVersion, String serviceName, byte[] formatsList, boolean moreResults) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpOppOpsRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpOppOpsRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { sdpRecord = new SdpOppOpsRecord(serviceName, rfcommCannelNumber, l2capPsm, profileVersion, formatsList); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, moreResults); } } void sdpSapsRecordFoundCallback(int status, byte[] address, byte[] uuid, int rfcommCannelNumber, int profileVersion, String serviceName, boolean moreResults) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpSapsRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpSapsRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { sdpRecord = new SdpSapsRecord(rfcommCannelNumber, profileVersion, serviceName); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, moreResults); } } /* TODO: Test or remove! */ void sdpRecordFoundCallback(int status, byte[] address, byte[] uuid, int sizeRecord, byte[] record) { synchronized (TRACKER_LOCK) { SdpSearchInstance inst = sSdpSearchTracker.getSearchInstance(address, uuid); SdpRecord sdpRecord = null; if (inst == null) { Log.e(TAG, "sdpRecordFoundCallback: Search instance is NULL"); return; } inst.setStatus(status); if (status == AbstractionLayer.BT_STATUS_SUCCESS) { if (D) { Log.d(TAG, "sdpRecordFoundCallback: found a sdp record of size " + sizeRecord); } if (D) { Log.d(TAG, "Record:" + Arrays.toString(record)); } sdpRecord = new SdpRecord(sizeRecord, record); } if (D) { Log.d(TAG, "UUID: " + Arrays.toString(uuid)); } if (D) { Log.d(TAG, "UUID in parcel: " + ((Utils.byteArrayToUuid(uuid))[0]).toString()); } sendSdpIntent(inst, sdpRecord, false); } } public void sdpSearch(BluetoothDevice device, ParcelUuid uuid) { if (!sNativeAvailable) { Log.e(TAG, "Native not initialized!"); return; } synchronized (TRACKER_LOCK) { if (sSdpSearchTracker.isSearching(device, uuid)) { /* Search already in progress */ return; } SdpSearchInstance inst = new SdpSearchInstance(0, device, uuid); sSdpSearchTracker.add(inst); // Queue the request startSearch(); // Start search if not busy } } /* Caller must hold the mTrackerLock */ private void startSearch() { SdpSearchInstance inst = sSdpSearchTracker.getNext(); if ((inst != null) && (!sSearchInProgress)) { if (D) { Log.d(TAG, "Starting search for UUID: " + inst.getUuid()); } sSearchInProgress = true; inst.startSearch(); // Trigger timeout message sdpSearchNative(Utils.getBytesFromAddress(inst.getDevice().getAddress()), Utils.uuidToByteArray(inst.getUuid())); } else { // Else queue is empty. if (D) { Log.d(TAG, "startSearch(): nextInst = " + inst + " mSearchInProgress = " + sSearchInProgress + " - search busy or queue empty."); } } } /* Caller must hold the mTrackerLock */ private void sendSdpIntent(SdpSearchInstance inst, Parcelable record, boolean moreResults) { inst.stopSearch(); Intent intent = new Intent(BluetoothDevice.ACTION_SDP_RECORD); intent.putExtra(BluetoothDevice.EXTRA_DEVICE, inst.getDevice()); intent.putExtra(BluetoothDevice.EXTRA_SDP_SEARCH_STATUS, inst.getStatus()); if (record != null) { intent.putExtra(BluetoothDevice.EXTRA_SDP_RECORD, record); } intent.putExtra(BluetoothDevice.EXTRA_UUID, inst.getUuid()); /* TODO: BLUETOOTH_ADMIN_PERM was private... change to callback interface. * Keep in mind that the MAP client needs to use this as well, * hence to make it call-backs, the MAP client profile needs to be * part of the Bluetooth APK. */ sAdapterService.sendBroadcast(intent, AdapterService.BLUETOOTH_ADMIN_PERM); if (!moreResults) { //Remove the outstanding UUID request sSdpSearchTracker.remove(inst); sSearchInProgress = false; startSearch(); } } private final Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_SDP_INTENT: SdpSearchInstance msgObj = (SdpSearchInstance) msg.obj; Log.w(TAG, "Search timedout for UUID " + msgObj.getUuid()); synchronized (TRACKER_LOCK) { sendSdpIntent(msgObj, null, false); } break; } } }; /** * Create a server side Message Access Profile Service Record. * Create the record once, and reuse it for all connections. * If changes to a record is needed remove the old record using {@link removeSdpRecord} * and then create a new one. * @param serviceName The textual name of the service * @param masId The MAS ID to associate with this SDP record * @param rfcommChannel The RFCOMM channel that clients can connect to * (obtain from BluetoothServerSocket) * @param l2capPsm The L2CAP PSM channel that clients can connect to * (obtain from BluetoothServerSocket) * Supply -1 to omit the L2CAP PSM from the record. * @param version The Profile version number (As specified in the Bluetooth * MAP specification) * @param msgTypes The supported message types bit mask (As specified in * the Bluetooth MAP specification) * @param features The feature bit mask (As specified in the Bluetooth * MAP specification) * @return a handle to the record created. The record can be removed again * using {@link removeSdpRecord}(). The record is not linked to the * creation/destruction of BluetoothSockets, hence SDP record cleanup * is a separate process. */ public int createMapMasRecord(String serviceName, int masId, int rfcommChannel, int l2capPsm, int version, int msgTypes, int features) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpCreateMapMasRecordNative(serviceName, masId, rfcommChannel, l2capPsm, version, msgTypes, features); } /** * Create a client side Message Access Profile Service Record. * Create the record once, and reuse it for all connections. * If changes to a record is needed remove the old record using {@link removeSdpRecord} * and then create a new one. * @param serviceName The textual name of the service * @param rfcommChannel The RFCOMM channel that clients can connect to * (obtain from BluetoothServerSocket) * @param l2capPsm The L2CAP PSM channel that clients can connect to * (obtain from BluetoothServerSocket) * Supply -1 to omit the L2CAP PSM from the record. * @param version The Profile version number (As specified in the Bluetooth * MAP specification) * @param features The feature bit mask (As specified in the Bluetooth * MAP specification) * @return a handle to the record created. The record can be removed again * using {@link removeSdpRecord}(). The record is not linked to the * creation/destruction of BluetoothSockets, hence SDP record cleanup * is a separate process. */ public int createMapMnsRecord(String serviceName, int rfcommChannel, int l2capPsm, int version, int features) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpCreateMapMnsRecordNative(serviceName, rfcommChannel, l2capPsm, version, features); } /** * Create a Server side Phone Book Access Profile Service Record. * Create the record once, and reuse it for all connections. * If changes to a record is needed remove the old record using {@link removeSdpRecord} * and then create a new one. * @param serviceName The textual name of the service * @param rfcommChannel The RFCOMM channel that clients can connect to * (obtain from BluetoothServerSocket) * @param l2capPsm The L2CAP PSM channel that clients can connect to * (obtain from BluetoothServerSocket) * Supply -1 to omit the L2CAP PSM from the record. * @param version The Profile version number (As specified in the Bluetooth * PBAP specification) * @param repositories The supported repositories bit mask (As specified in * the Bluetooth PBAP specification) * @param features The feature bit mask (As specified in the Bluetooth * PBAP specification) * @return a handle to the record created. The record can be removed again * using {@link removeSdpRecord}(). The record is not linked to the * creation/destruction of BluetoothSockets, hence SDP record cleanup * is a separate process. */ public int createPbapPseRecord(String serviceName, int rfcommChannel, int l2capPsm, int version, int repositories, int features) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpCreatePbapPseRecordNative(serviceName, rfcommChannel, l2capPsm, version, repositories, features); } /** * Create a Server side Object Push Profile Service Record. * Create the record once, and reuse it for all connections. * If changes to a record is needed remove the old record using {@link removeSdpRecord} * and then create a new one. * @param serviceName The textual name of the service * @param rfcommChannel The RFCOMM channel that clients can connect to * (obtain from BluetoothServerSocket) * @param l2capPsm The L2CAP PSM channel that clients can connect to * (obtain from BluetoothServerSocket) * Supply -1 to omit the L2CAP PSM from the record. * @param version The Profile version number (As specified in the Bluetooth * OPP specification) * @param formatsList A list of the supported formats (As specified in * the Bluetooth OPP specification) * @return a handle to the record created. The record can be removed again * using {@link removeSdpRecord}(). The record is not linked to the * creation/destruction of BluetoothSockets, hence SDP record cleanup * is a separate process. */ public int createOppOpsRecord(String serviceName, int rfcommChannel, int l2capPsm, int version, byte[] formatsList) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpCreateOppOpsRecordNative(serviceName, rfcommChannel, l2capPsm, version, formatsList); } /** * Create a server side Sim Access Profile Service Record. * Create the record once, and reuse it for all connections. * If changes to a record is needed remove the old record using {@link removeSdpRecord} * and then create a new one. * @param serviceName The textual name of the service * @param rfcommChannel The RFCOMM channel that clients can connect to * (obtain from BluetoothServerSocket) * @param version The Profile version number (As specified in the Bluetooth * SAP specification) * @return a handle to the record created. The record can be removed again * using {@link removeSdpRecord}(). The record is not linked to the * creation/destruction of BluetoothSockets, hence SDP record cleanup * is a separate process. */ public int createSapsRecord(String serviceName, int rfcommChannel, int version) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpCreateSapsRecordNative(serviceName, rfcommChannel, version); } /** * Remove a SDP record. * When Bluetooth is disabled all records will be deleted, hence there * is no need to call this function when bluetooth is disabled. * @param recordId The Id returned by on of the createXxxXxxRecord() functions. * @return TRUE if the record removal was initiated successfully. FALSE if the record * handle is not known/have already been removed. */ public boolean removeSdpRecord(int recordId) { if (!sNativeAvailable) { throw new RuntimeException(TAG + " sNativeAvailable == false - native not initialized"); } return sdpRemoveSdpRecordNative(recordId); } }