1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.googlecode.android_scripting.facade.bluetooth;
18 
19 import android.app.Service;
20 import android.bluetooth.BluetoothA2dp;
21 import android.bluetooth.BluetoothAdapter;
22 import android.bluetooth.BluetoothCodecConfig;
23 import android.bluetooth.BluetoothCodecStatus;
24 import android.bluetooth.BluetoothDevice;
25 import android.bluetooth.BluetoothProfile;
26 import android.bluetooth.BluetoothUuid;
27 import android.content.BroadcastReceiver;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.IntentFilter;
31 import android.os.Bundle;
32 import android.os.ParcelUuid;
33 
34 import com.googlecode.android_scripting.Log;
35 import com.googlecode.android_scripting.facade.EventFacade;
36 import com.googlecode.android_scripting.facade.FacadeManager;
37 import com.googlecode.android_scripting.jsonrpc.RpcReceiver;
38 import com.googlecode.android_scripting.rpc.Rpc;
39 import com.googlecode.android_scripting.rpc.RpcParameter;
40 
41 import java.util.List;
42 
43 public class BluetoothA2dpFacade extends RpcReceiver {
44     static final ParcelUuid[] SINK_UUIDS = {
45         BluetoothUuid.A2DP_SINK, BluetoothUuid.ADV_AUDIO_DIST,
46     };
47     private BluetoothCodecConfig mBluetoothCodecConfig;
48 
49     private final Service mService;
50     private final EventFacade mEventFacade;
51     private final BroadcastReceiver mBluetoothA2dpReceiver;
52     private final BluetoothAdapter mBluetoothAdapter;
53 
54     private static volatile boolean sIsA2dpReady = false;
55     private static BluetoothA2dp sA2dpProfile = null;
56 
BluetoothA2dpFacade(FacadeManager manager)57     public BluetoothA2dpFacade(FacadeManager manager) {
58         super(manager);
59         mService = manager.getService();
60         mEventFacade = manager.getReceiver(EventFacade.class);
61 
62         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
63         mBluetoothA2dpReceiver = new BluetoothA2dpReceiver();
64         mBluetoothCodecConfig = new BluetoothCodecConfig(
65                 BluetoothCodecConfig.SOURCE_CODEC_TYPE_INVALID,
66                 BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
67                 BluetoothCodecConfig.SAMPLE_RATE_NONE,
68                 BluetoothCodecConfig.BITS_PER_SAMPLE_NONE,
69                 BluetoothCodecConfig.CHANNEL_MODE_NONE,
70                 0L, 0L, 0L, 0L);
71         mBluetoothAdapter.getProfileProxy(mService, new A2dpServiceListener(),
72                 BluetoothProfile.A2DP);
73 
74         mService.registerReceiver(mBluetoothA2dpReceiver,
75                           new IntentFilter(BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED));
76     }
77 
78     class A2dpServiceListener implements BluetoothProfile.ServiceListener {
79         @Override
onServiceConnected(int profile, BluetoothProfile proxy)80         public void onServiceConnected(int profile, BluetoothProfile proxy) {
81             sA2dpProfile = (BluetoothA2dp) proxy;
82             sIsA2dpReady = true;
83         }
84 
85         @Override
onServiceDisconnected(int profile)86         public void onServiceDisconnected(int profile) {
87             sIsA2dpReady = false;
88         }
89     }
90 
91     class BluetoothA2dpReceiver extends BroadcastReceiver {
92         @Override
onReceive(Context context, Intent intent)93         public void onReceive(Context context, Intent intent) {
94             String action = intent.getAction();
95 
96             if (BluetoothA2dp.ACTION_CODEC_CONFIG_CHANGED.equals(action)) {
97                 BluetoothCodecStatus codecStatus = intent.getParcelableExtra(
98                         BluetoothCodecStatus.EXTRA_CODEC_STATUS);
99                 if (codecStatus.getCodecConfig().equals(mBluetoothCodecConfig)) {
100                     mEventFacade.postEvent("BluetoothA2dpCodecConfigChanged", new Bundle());
101                 }
102             }
103         }
104     }
105 
106 
107 
108     /**
109      * Connect A2DP Profile to input BluetoothDevice
110      *
111      * @param device the BluetoothDevice object to connect to
112      * @return if the connection was successfull or not
113     */
a2dpConnect(BluetoothDevice device)114     public Boolean a2dpConnect(BluetoothDevice device) {
115         List<BluetoothDevice> sinks = sA2dpProfile.getConnectedDevices();
116         if (sinks != null) {
117             for (BluetoothDevice sink : sinks) {
118                 sA2dpProfile.disconnect(sink);
119             }
120         }
121         return sA2dpProfile.connect(device);
122     }
123 
124     /**
125      * Disconnect A2DP Profile from input BluetoothDevice
126      *
127      * @param device the BluetoothDevice object to disconnect from
128      * @return if the disconnection was successfull or not
129     */
a2dpDisconnect(BluetoothDevice device)130     public Boolean a2dpDisconnect(BluetoothDevice device) {
131         if (sA2dpProfile == null) return false;
132         if (sA2dpProfile.getPriority(device) > BluetoothProfile.PRIORITY_ON) {
133             sA2dpProfile.setPriority(device, BluetoothProfile.PRIORITY_ON);
134         }
135         return sA2dpProfile.disconnect(device);
136     }
137 
138     /**
139      * Checks to see if the A2DP profile is ready for use.
140      *
141      * @return Returns true if the A2DP Profile is ready.
142      */
143     @Rpc(description = "Is A2dp profile ready.")
bluetoothA2dpIsReady()144     public Boolean bluetoothA2dpIsReady() {
145         return sIsA2dpReady;
146     }
147 
148     /**
149      * Set Bluetooth A2DP connection priority
150      *
151      * @param deviceStr the Bluetooth device's mac address to set the connection priority of
152      * @param priority the integer priority to be set
153      */
154     @Rpc(description = "Set priority of the profile")
bluetoothA2dpSetPriority( @pcParametername = "device", description = "Mac address of a BT device.") String deviceStr, @RpcParameter(name = "priority", description = "Priority that needs to be set.") Integer priority)155     public void bluetoothA2dpSetPriority(
156             @RpcParameter(name = "device", description = "Mac address of a BT device.")
157             String deviceStr,
158             @RpcParameter(name = "priority", description = "Priority that needs to be set.")
159             Integer priority)
160             throws Exception {
161         if (sA2dpProfile == null) return;
162         BluetoothDevice device =
163                 BluetoothFacade.getDevice(mBluetoothAdapter.getBondedDevices(), deviceStr);
164         Log.d("Changing priority of device " + device.getAlias() + " p: " + priority);
165         sA2dpProfile.setPriority(device, priority);
166     }
167 
168 
169     /**
170      * Connect to remote device using the A2DP profile.
171      *
172      * @param deviceID the name or mac address of the remote Bluetooth device.
173      * @return True if connected successfully.
174      * @throws Exception
175      */
176     @Rpc(description = "Connect to an A2DP device.")
bluetoothA2dpConnect( @pcParametername = "deviceID", description = "Name or MAC address of a bluetooth device.") String deviceID)177     public Boolean bluetoothA2dpConnect(
178             @RpcParameter(name = "deviceID",
179                 description = "Name or MAC address of a bluetooth device.")
180             String deviceID)
181             throws Exception {
182         if (sA2dpProfile == null) {
183             return false;
184         }
185         BluetoothDevice mDevice =
186                 BluetoothFacade.getDevice(
187                         mBluetoothAdapter.getBondedDevices(), deviceID);
188         Log.d("Connecting to device " + mDevice.getAlias());
189         return a2dpConnect(mDevice);
190     }
191 
192     /**
193      * Disconnect a remote device using the A2DP profile.
194      *
195      * @param deviceID the name or mac address of the remote Bluetooth device.
196      * @return True if connected successfully.
197      * @throws Exception
198      */
199     @Rpc(description = "Disconnect an A2DP device.")
bluetoothA2dpDisconnect( @pcParametername = "deviceID", description = "Name or MAC address of a device.") String deviceID)200     public Boolean bluetoothA2dpDisconnect(
201             @RpcParameter(name = "deviceID", description = "Name or MAC address of a device.")
202             String deviceID)
203             throws Exception {
204         if (sA2dpProfile == null) {
205             return false;
206         }
207         List<BluetoothDevice> connectedA2dpDevices =
208                 sA2dpProfile.getConnectedDevices();
209         Log.d("Connected a2dp devices " + connectedA2dpDevices);
210         BluetoothDevice mDevice = BluetoothFacade.getDevice(
211                 connectedA2dpDevices, deviceID);
212         return a2dpDisconnect(mDevice);
213     }
214 
215     /**
216      * Get the list of devices connected through the A2DP profile.
217      *
218      * @return List of bluetooth devices that are in one of the following states:
219      *   connected, connecting, and disconnecting.
220      */
221     @Rpc(description = "Get all the devices connected through A2DP.")
bluetoothA2dpGetConnectedDevices()222     public List<BluetoothDevice> bluetoothA2dpGetConnectedDevices() {
223         while (!sIsA2dpReady) {
224             continue;
225         }
226         return sA2dpProfile.getDevicesMatchingConnectionStates(
227                 new int[] { BluetoothProfile.STATE_CONNECTED,
228                     BluetoothProfile.STATE_CONNECTING,
229                     BluetoothProfile.STATE_DISCONNECTING});
230     }
231 
isSelectableCodec(BluetoothCodecConfig target, BluetoothCodecConfig capability)232     private boolean isSelectableCodec(BluetoothCodecConfig target,
233             BluetoothCodecConfig capability) {
234         return target.getCodecType() == capability.getCodecType()
235                 && (target.getSampleRate() & capability.getSampleRate()) != 0
236                 && (target.getBitsPerSample() & capability.getBitsPerSample()) != 0
237                 && (target.getChannelMode() & capability.getChannelMode()) != 0;
238     }
239 
240     /**
241      * Set active devices with giving codec config
242      *
243      * @param codecType codec type want to set to, list in BluetoothCodecConfig.
244      * @param sampleRate sample rate want to set to, list in BluetoothCodecConfig.
245      * @param bitsPerSample bits per sample want to set to, list in BluetoothCodecConfig.
246      * @param channelMode channel mode want to set to, list in BluetoothCodecConfig.
247      * @return True if set codec config successfully.
248      */
249     @Rpc(description = "Set A2dp codec config.")
bluetoothA2dpSetCodecConfigPreference( @pcParametername = "codecType") Integer codecType, @RpcParameter(name = "sampleRate") Integer sampleRate, @RpcParameter(name = "bitsPerSample") Integer bitsPerSample, @RpcParameter(name = "channelMode") Integer channelMode, @RpcParameter(name = "codecSpecific1") Long codecSpecific1)250     public boolean bluetoothA2dpSetCodecConfigPreference(
251             @RpcParameter(name = "codecType") Integer codecType,
252             @RpcParameter(name = "sampleRate") Integer sampleRate,
253             @RpcParameter(name = "bitsPerSample") Integer bitsPerSample,
254             @RpcParameter(name = "channelMode") Integer channelMode,
255             @RpcParameter(name = "codecSpecific1") Long codecSpecific1)
256             throws Exception {
257         while (!sIsA2dpReady) {
258             continue;
259         }
260         BluetoothCodecConfig codecConfig = new BluetoothCodecConfig(
261                 codecType,
262                 BluetoothCodecConfig.CODEC_PRIORITY_HIGHEST,
263                 sampleRate,
264                 bitsPerSample,
265                 channelMode,
266                 codecSpecific1,
267                 0L, 0L, 0L);
268         BluetoothDevice activeDevice = sA2dpProfile.getActiveDevice();
269         if (activeDevice == null) {
270             Log.e("No active device");
271             throw new Exception("No active device");
272         }
273         BluetoothCodecStatus currentCodecStatus = sA2dpProfile.getCodecStatus(activeDevice);
274         BluetoothCodecConfig currentCodecConfig = currentCodecStatus.getCodecConfig();
275         if (isSelectableCodec(codecConfig, currentCodecConfig)
276                 && codecConfig.getCodecSpecific1() == currentCodecConfig.getCodecSpecific1()) {
277             Log.e("Same as current codec configuration " + currentCodecConfig);
278             return false;
279         }
280         for (BluetoothCodecConfig selectable :
281                 currentCodecStatus.getCodecsSelectableCapabilities()) {
282             if (isSelectableCodec(codecConfig, selectable)) {
283                 mBluetoothCodecConfig = codecConfig;
284                 sA2dpProfile.setCodecConfigPreference(activeDevice, mBluetoothCodecConfig);
285                 return true;
286             }
287         }
288         return false;
289     }
290 
291     /**
292      * Get current active device codec config
293      *
294      * @return Current active device codec config,
295      */
296     @Rpc(description = "Get current codec config.")
bluetoothA2dpGetCurrentCodecConfig()297     public BluetoothCodecConfig bluetoothA2dpGetCurrentCodecConfig() throws Exception {
298         while (!sIsA2dpReady) {
299             continue;
300         }
301         if (sA2dpProfile.getActiveDevice() == null) {
302             Log.e("No active device.");
303             throw new Exception("No active device");
304         }
305         return sA2dpProfile.getCodecStatus(sA2dpProfile.getActiveDevice()).getCodecConfig();
306     }
307 
308     @Override
shutdown()309     public void shutdown() {
310         mService.unregisterReceiver(mBluetoothA2dpReceiver);
311     }
312 }
313