| /****************************************************************************** |
| * |
| * Copyright 2021 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.bc; |
| |
| import java.util.List; |
| import java.util.Arrays; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.util.Log; |
| import java.util.UUID; |
| import java.util.Collection; |
| import android.os.UserHandle; |
| |
| import com.android.internal.util.State; |
| import com.android.internal.util.StateMachine; |
| import java.nio.charset.StandardCharsets; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.io.StringWriter; |
| import java.util.Scanner; |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.LinkedHashSet; |
| import java.util.Set; |
| import java.lang.String; |
| import java.lang.StringBuffer; |
| import java.lang.Integer; |
| |
| import java.nio.ByteBuffer; |
| import java.lang.Byte; |
| import java.util.stream.IntStream; |
| import java.util.NoSuchElementException; |
| |
| import android.bluetooth.le.ScanResult; |
| import android.bluetooth.le.ScanSettings; |
| import android.bluetooth.le.ScanFilter; |
| import android.bluetooth.le.ScanCallback; |
| import android.bluetooth.le.ScanRecord; |
| import android.bluetooth.le.BluetoothLeScanner; |
| import java.util.UUID; |
| import android.os.Handler; |
| import android.os.ParcelUuid; |
| import android.os.SystemProperties; |
| import android.os.RemoteException; |
| |
| import android.bluetooth.BleBroadcastSourceInfo; |
| import android.bluetooth.BleBroadcastSourceChannel; |
| //import android.bluetooth.BluetoothBroadcast; |
| import android.bluetooth.BluetoothProfile; |
| import android.bluetooth.BluetoothAdapter; |
| import android.bluetooth.BluetoothDevice; |
| import com.android.bluetooth.btservice.ServiceFactory; |
| ///*_BMS |
| import com.android.bluetooth.broadcast.BroadcastService; |
| //_BMS*/ |
| import android.bluetooth.BluetoothCodecConfig; |
| /*_PACS |
| import com.android.bluetooth.pacsclient.PacsClientService; |
| _PACS*/ |
| import android.bluetooth.IBleBroadcastAudioScanAssistCallback; |
| |
| /** |
| * Bass Utility functions |
| */ |
| |
| final class BassUtils { |
| private static final String TAG = "BassUtils"; |
| /*LE Scan related members*/ |
| private boolean mBroadcastersAround = false; |
| private BluetoothAdapter mBluetoothAdapter = null; |
| private BluetoothLeScanner mLeScanner = null; |
| private BCService mBCService = null; |
| |
| ///*_BMS |
| private BroadcastService mBAService = null; |
| //_BMS*/ |
| public static final String BAAS_UUID = "00001852-0000-1000-8000-00805F9B34FB"; |
| private boolean mIsLocalBMSNotified = false; |
| private ServiceFactory mFactory = new ServiceFactory(); |
| //Using ArrayList as KEY to hashmap. May be not risk |
| //in this case as It is used to track the callback to cancel Scanning later |
| private final Map<ArrayList<IBleBroadcastAudioScanAssistCallback>, ScanCallback> mLeAudioSourceScanCallbacks; |
| private final Map<BluetoothDevice, ScanCallback> mBassAutoAssist; |
| |
| private static final int AA_START_SCAN = 1; |
| private static final int AA_SCAN_SUCCESS = 2; |
| private static final int AA_SCAN_FAILURE = 3; |
| private static final int AA_SCAN_TIMEOUT = 4; |
| //timeout for internal scan |
| private static final int AA_SCAN_TIMEOUT_MS = 1000; |
| |
| /** |
| * Stanadard Codec param types |
| */ |
| static final int LOCATION = 3; |
| //sample rate |
| static final int SAMPLE_RATE = 1; |
| //frame duration |
| static final int FRAME_DURATION = 2; |
| //Octets per frame |
| static final int OCTETS_PER_FRAME = 8; |
| /*_PACS |
| private PacsClientService mPacsClientService = PacsClientService.getPacsClientService(); |
| _PACS*/ |
| BassUtils (BCService service) { |
| mBCService = service; |
| mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); |
| mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); |
| mLeAudioSourceScanCallbacks = new HashMap<ArrayList<IBleBroadcastAudioScanAssistCallback>, ScanCallback>(); |
| mBassAutoAssist = new HashMap<BluetoothDevice, ScanCallback>(); |
| ///*_BMS |
| mBAService = BroadcastService.getBroadcastService(); |
| //_BMS*/ |
| } |
| |
| private ScanCallback mPaSyncScanCallback = new ScanCallback() { |
| @Override |
| public void onScanResult(int callbackType, ScanResult result) { |
| log( "onScanResult:" + result); |
| } |
| }; |
| |
| void cleanUp () { |
| |
| if (mLeAudioSourceScanCallbacks != null) { |
| mLeAudioSourceScanCallbacks.clear(); |
| } |
| |
| if (mBassAutoAssist != null) { |
| mBassAutoAssist.clear(); |
| } |
| } |
| |
| boolean leScanControl(boolean on) { |
| log("leScanControl:" + on); |
| mLeScanner = mBluetoothAdapter.getBluetoothLeScanner(); |
| if (mLeScanner == null) { |
| Log.e(TAG, "LeScan handle not available"); |
| return false; |
| } |
| |
| if (on) { |
| mLeScanner.startScan(mPaSyncScanCallback); |
| } else { |
| mLeScanner.stopScan(mPaSyncScanCallback); |
| } |
| |
| return true; |
| } |
| |
| /* private helper to check if the Local BLE Broadcast happening Or not */ |
| public boolean isLocalLEAudioBroadcasting() { |
| boolean ret = false; |
| /*String localLeABroadcast = SystemProperties.get("persist.vendor.btstack.isLocalLeAB"); |
| if (!localLeABroadcast.isEmpty() && "true".equals(localLeABroadcast)) { |
| ret = true; |
| } |
| log("property isLocalLEAudioBroadcasting returning " + ret);*/ |
| ///*_Broadcast |
| if (mBAService == null) { |
| mBAService = BroadcastService.getBroadcastService(); |
| } |
| if (mBAService != null) { |
| ret = mBAService.isBroadcastActive(); |
| //ret = mBAService.isBroadcastStreaming(); |
| log("local broadcast streaming:" + ret); |
| } else { |
| log("BroadcastService is Null"); |
| } |
| //_Broadcast*/ |
| log("isLocalLEAudioBroadcasting returning " + ret); |
| return ret; |
| } |
| |
| Handler mAutoAssistScanHandler = new Handler() { |
| public void handleMessage(Message msg) { |
| super.handleMessage(msg); |
| switch (msg.what) { |
| case AA_START_SCAN: |
| BluetoothDevice dev = (BluetoothDevice) msg.obj; |
| Message m = obtainMessage(AA_SCAN_TIMEOUT); |
| m.obj = dev; |
| sendMessageDelayed(m, AA_SCAN_TIMEOUT_MS); |
| searchforLeAudioBroadcasters(dev, null); |
| break; |
| case AA_SCAN_SUCCESS: |
| //Able to find to desired desired Source Device |
| ScanResult scanRes = (ScanResult) msg.obj; |
| dev = scanRes.getDevice(); |
| stopSearchforLeAudioBroadcasters(dev,null); |
| mBCService.selectBroadcastSource(dev, scanRes, false, true); |
| break; |
| case AA_SCAN_FAILURE: |
| //Not able to find the given source |
| //ignore |
| break; |
| case AA_SCAN_TIMEOUT: |
| dev = (BluetoothDevice)msg.obj; |
| stopSearchforLeAudioBroadcasters(dev, null); |
| break; |
| } |
| } |
| }; |
| private void notifyLocalBroadcastSourceFound(ArrayList<IBleBroadcastAudioScanAssistCallback> cbs) { |
| BluetoothDevice localDev = |
| BluetoothAdapter.getDefaultAdapter().getRemoteDevice(mBluetoothAdapter.getAddress()); |
| String localName = BluetoothAdapter.getDefaultAdapter().getName(); |
| ScanRecord record = null; |
| if (localName != null) { |
| byte name_len = (byte)localName.length(); |
| byte[] bd_name = localName.getBytes(StandardCharsets.US_ASCII); |
| byte[] name_key = new byte[] {++name_len, 0x09 }; //0x09 TYPE:Name |
| byte[] scan_r = new byte[name_key.length + bd_name.length]; |
| System.arraycopy(name_key, 0, scan_r, 0, name_key.length); |
| System.arraycopy(bd_name, 0, scan_r, name_key.length, bd_name.length); |
| record = ScanRecord.parseFromBytes(scan_r); |
| log ("Local name populated in fake Scan res:" + record.getDeviceName()); |
| } |
| ScanResult scanRes = new ScanResult(localDev, |
| 1, 1, 1,2, 0, 0, 0, record, 0); |
| if (cbs != null) { |
| for (IBleBroadcastAudioScanAssistCallback cb : cbs) { |
| try { |
| cb.onBleBroadcastSourceFound(scanRes); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while calling onBleBroadcastSourceFound"); |
| } |
| } |
| } |
| } |
| public boolean searchforLeAudioBroadcasters (BluetoothDevice srcDevice, |
| ArrayList<IBleBroadcastAudioScanAssistCallback> cbs |
| ) { |
| log( "searchforLeAudioBroadcasters: "); |
| BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); |
| mIsLocalBMSNotified = false; |
| if (scanner == null) { |
| Log.e(TAG, "startLeScan: cannot get BluetoothLeScanner"); |
| return false; |
| } |
| synchronized (mLeAudioSourceScanCallbacks) { |
| if (mLeAudioSourceScanCallbacks.containsKey(cbs)) { |
| Log.e(TAG, "LE Scan has already started"); |
| return false; |
| } |
| ScanCallback scanCallback = new ScanCallback() { |
| @Override |
| public void onScanResult(int callbackType, ScanResult result) { |
| log( "onScanResult:" + result); |
| if (callbackType != ScanSettings.CALLBACK_TYPE_ALL_MATCHES) { |
| // Should not happen. |
| Log.e(TAG, "LE Scan has already started"); |
| return; |
| } |
| ScanRecord scanRecord = result.getScanRecord(); |
| //int pInterval = result.getPeriodicAdvertisingInterval(); |
| if (scanRecord != null) { |
| Map<ParcelUuid, byte[]> listOfUuids = scanRecord.getServiceData(); |
| if (listOfUuids != null) { |
| //ParcelUuid bmsUuid = new ParcelUuid(BroadcastService.BAAS_UUID); |
| //boolean isBroadcastSource = listOfUuids.containsKey(bmsUuid); |
| boolean isBroadcastSource = listOfUuids.containsKey(ParcelUuid.fromString(BAAS_UUID)); |
| log( "isBroadcastSource:" + isBroadcastSource); |
| if (isBroadcastSource) { |
| log( "Broadcast Source Found:" + result.getDevice()); |
| if (cbs != null) { |
| for (IBleBroadcastAudioScanAssistCallback cb : cbs) { |
| try { |
| cb.onBleBroadcastSourceFound(result); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Exception while calling onBleBroadcastSourceFound"); |
| } |
| } |
| } else { |
| if (srcDevice.equals(result.getDevice())) { |
| log("matching src Device found"); |
| Message msg = mAutoAssistScanHandler.obtainMessage(AA_SCAN_SUCCESS); |
| msg.obj = result; |
| mAutoAssistScanHandler.sendMessage(msg); |
| } |
| } |
| } else { |
| log( "Broadcast Source UUID not preset, ignore"); |
| } |
| } else { |
| Log.e(TAG, "Ignore no UUID"); |
| return; |
| } |
| } else { |
| Log.e(TAG, "Scan record is null, ignoring this Scan res"); |
| return; |
| } |
| //Before starting LE Scan, Call local APIs to find out if the local device |
| //is Broadcaster, then generate callback for Local device |
| if (!mIsLocalBMSNotified && isLocalLEAudioBroadcasting()) { |
| //Create a DUMMY scan result for colocated case |
| notifyLocalBroadcastSourceFound(cbs); |
| mIsLocalBMSNotified = true; |
| } |
| } |
| |
| public void onScanFailed(int errorCode) { |
| Log.e(TAG, "Scan Failure:" + errorCode); |
| } |
| }; |
| if (mBluetoothAdapter != null) { |
| if (cbs != null) { |
| mLeAudioSourceScanCallbacks.put(cbs, scanCallback); |
| } else { |
| //internal auto assist trigger remember it |
| //based on device |
| mBassAutoAssist.put(srcDevice, scanCallback); |
| } |
| |
| ScanSettings settings = new ScanSettings.Builder().setCallbackType( |
| ScanSettings.CALLBACK_TYPE_ALL_MATCHES) |
| .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) |
| .setLegacy(false) |
| .build(); |
| ScanFilter.Builder filterBuilder = new ScanFilter.Builder(); |
| ScanFilter srcFilter = filterBuilder.setServiceUuid( |
| ParcelUuid.fromString(BAAS_UUID)).build(); |
| List<ScanFilter> filters = new ArrayList<ScanFilter>(); |
| if (!mIsLocalBMSNotified && isLocalLEAudioBroadcasting()) { |
| //Create a DUMMY scan result for colocated case |
| notifyLocalBroadcastSourceFound(cbs); |
| mIsLocalBMSNotified = true; |
| } |
| scanner.startScan(filters, settings, scanCallback); |
| return true; |
| } else { |
| Log.e(TAG, "searchforLeAudioBroadcasters: Adapter is NULL"); |
| return false; |
| } |
| } |
| } |
| public boolean stopSearchforLeAudioBroadcasters(BluetoothDevice srcDev, |
| ArrayList<IBleBroadcastAudioScanAssistCallback> cbs) { |
| log( "stopSearchforLeAudioBroadcasters()"); |
| BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); |
| if (scanner == null) { |
| return false; |
| } |
| ScanCallback scanCallback = null; |
| if (cbs != null) { |
| scanCallback = mLeAudioSourceScanCallbacks.remove(cbs); |
| } else { |
| scanCallback = mLeAudioSourceScanCallbacks.remove(srcDev); |
| } |
| |
| if (scanCallback == null) { |
| log( "scan not started yet"); |
| return false; |
| } |
| scanner.stopScan(scanCallback); |
| return true; |
| } |
| |
| private int convertConfigurationSRToCapabilitySR(byte sampleRate) { |
| int ret = BluetoothCodecConfig.SAMPLE_RATE_NONE; |
| switch (sampleRate) { |
| case 1: |
| ret = BluetoothCodecConfig.SAMPLE_RATE_NONE; break; |
| case 2: |
| ret = BluetoothCodecConfig.SAMPLE_RATE_NONE; break; |
| case 3: |
| ret = BluetoothCodecConfig.SAMPLE_RATE_NONE; break; |
| case 4: |
| //ret = BluetoothCodecConfig.SAMPLE_RATE_32000; break; |
| case 5: |
| ret = BluetoothCodecConfig.SAMPLE_RATE_44100; break; |
| case 6: |
| ret = BluetoothCodecConfig.SAMPLE_RATE_48000; break; |
| } |
| log("convertConfigurationSRToCapabilitySR returns:" + ret); |
| return ret; |
| } |
| |
| private boolean isSampleRateSupported(BluetoothDevice device, byte sampleRate) { |
| boolean ret = false; |
| /*_PACS |
| BluetoothCodecConfig[] supportedConfigs = mPacsClientService.getSinkPacs(device); |
| int actualSampleRate = convertConfigurationSRToCapabilitySR(sampleRate); |
| |
| if (actualSampleRate == BluetoothCodecConfig.SAMPLE_RATE_NONE) { |
| return false; |
| } |
| |
| for (int i=0; i<supportedConfigs.length; i++) { |
| if (actualSampleRate == supportedConfigs[i].getSampleRate()) { |
| ret = true; |
| } |
| } |
| |
| log("isSampleRateSupported returns:" + ret); |
| _PACS*/ |
| return ret; |
| } |
| public List<BleBroadcastSourceChannel> selectBises(BluetoothDevice device, |
| BleBroadcastSourceInfo srcInfo, BaseData base) { |
| boolean noPref = SystemProperties.getBoolean("persist.vendor.service.bt.bass_no_pref", false); |
| if (noPref) { |
| log("No pref selected"); |
| return null; |
| } else { |
| /*_PACS |
| mPacsClientService = PacsClientService.getPacsClientService(); |
| List<BleBroadcastSourceChannel> bChannels = new ArrayList<BleBroadcastSourceChannel>(); |
| //if (mPacsClientService == null) { |
| log("selectBises: Pacs Service is null, pick BISes apropriately"); |
| //Pacs not available |
| if (base != null) { |
| bChannels = base.pickAllBroadcastChannels(); |
| } else { |
| bChannels = null; |
| } |
| return bChannels; |
| //} |
| if (mPacsClientService != null) { |
| int supportedLocations = 1/*mPacsClientService.getSinkLocations(device); |
| ArrayList<BaseData.BaseInformation> broadcastedCodecInfo = base.getBISIndexInfos(); |
| if (broadcastedCodecInfo != null) { |
| for (int i=0; i<broadcastedCodecInfo.size(); i++) { |
| HashMap<Integer, String> consolidatedUniqueCodecInfo = broadcastedCodecInfo.get(i).consolidatedUniqueCodecInfo; |
| byte index = broadcastedCodecInfo.get(i).index; |
| if (consolidatedUniqueCodecInfo != null) { |
| |
| |
| byte[] bisChannelLocation = consolidatedUniqueCodecInfo.get(LOCATION).getBytes(); |
| byte[] locationValue = new byte[4]; |
| System.arraycopy(bisChannelLocation, 2, locationValue, 0, 4); |
| log ("locationValue>>> "); |
| printByteArray(locationValue); |
| ByteBuffer wrapped = ByteBuffer.wrap(locationValue); |
| int bisLocation = wrapped.getInt(); |
| log("bisLocation: " + bisLocation); |
| int reversebisLoc = Integer.reverseBytes(bisLocation); |
| log("reversebisLoc: " + reversebisLoc); |
| |
| byte[] bisSampleRate = consolidatedUniqueCodecInfo.get(SAMPLE_RATE).getBytes(); |
| byte bisSR = bisSampleRate[2]; |
| |
| //using bitwise operand as Location can be bitmask |
| if (isSampleRateSupported(device, bisSR) && (reversebisLoc & supportedLocations) == supportedLocations) { |
| log("matching location: bisLocation " + reversebisLoc + ":: " + supportedLocations); |
| BleBroadcastSourceChannel bc = new BleBroadcastSourceChannel(index, String.valueOf(index), true); |
| bChannels.add(bc); |
| } |
| } |
| } |
| } |
| } |
| |
| if (bChannels != null && bChannels.size() == 0) { |
| log("selectBises: no channel are selected"); |
| bChannels = null; |
| |
| } |
| return bChannels; |
| _PACS*/ |
| } |
| return null; |
| } |
| |
| public void triggerAutoAssist (BleBroadcastSourceInfo srcInfo) { |
| //searchforLeAudioBroadcasters (srcInfo.getSourceDevice(), null, AUTO_ASSIST_SCAN_TIMEOUT); |
| BluetoothDevice dev = srcInfo.getSourceDevice(); |
| |
| Message msg = mAutoAssistScanHandler.obtainMessage(AA_START_SCAN); |
| msg.obj = srcInfo.getSourceDevice(); |
| mAutoAssistScanHandler.sendMessage(msg); |
| } |
| |
| static void log(String msg) { |
| if (BassClientStateMachine.BASS_DBG) { |
| Log.d(TAG, msg); |
| } |
| } |
| |
| static void printByteArray(byte[] array) { |
| log("Entire byte Array as string: " + Arrays.toString(array)); |
| log("printitng byte by bte"); |
| for (int i=0; i<array.length; i++) { |
| log( "array[" + i + "] :" + Byte.toUnsignedInt(array[i])); |
| } |
| } |
| } |