1 /*
<lambda>null2  * Copyright (C) 2024 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.android.pandora
18 
19 import android.bluetooth.BluetoothAdapter
20 import android.bluetooth.BluetoothDevice
21 import android.bluetooth.BluetoothDevice.TRANSPORT_LE
22 import android.bluetooth.BluetoothHapClient
23 import android.bluetooth.BluetoothHapClient.Callback
24 import android.bluetooth.BluetoothHapPresetInfo
25 import android.bluetooth.BluetoothLeAudio
26 import android.bluetooth.BluetoothManager
27 import android.bluetooth.BluetoothProfile
28 import android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED
29 import android.content.Context
30 import android.content.IntentFilter
31 import android.media.AudioManager
32 import android.media.AudioTrack
33 import android.util.Log
34 import com.google.protobuf.Empty
35 import io.grpc.Status
36 import io.grpc.stub.StreamObserver
37 import java.io.Closeable
38 import java.io.PrintWriter
39 import java.io.StringWriter
40 import java.util.concurrent.Executors
41 import kotlinx.coroutines.CoroutineScope
42 import kotlinx.coroutines.Dispatchers
43 import kotlinx.coroutines.cancel
44 import kotlinx.coroutines.channels.awaitClose
45 import kotlinx.coroutines.flow.SharingStarted
46 import kotlinx.coroutines.flow.callbackFlow
47 import kotlinx.coroutines.flow.filter
48 import kotlinx.coroutines.flow.first
49 import kotlinx.coroutines.flow.map
50 import kotlinx.coroutines.flow.shareIn
51 import pandora.HAPGrpc.HAPImplBase
52 import pandora.HapProto.*
53 import pandora.HostProto.Connection
54 
55 @kotlinx.coroutines.ExperimentalCoroutinesApi
56 class Hap(val context: Context) : HAPImplBase(), Closeable {
57     private val TAG = "PandoraHap"
58 
59     private val scope: CoroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
60 
61     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)!!
62     private val bluetoothAdapter = bluetoothManager.adapter
63     private val audioManager = context.getSystemService(AudioManager::class.java)!!
64 
65     private val bluetoothHapClient =
66         getProfileProxy<BluetoothHapClient>(context, BluetoothProfile.HAP_CLIENT)
67 
68     private val bluetoothLeAudio =
69         getProfileProxy<BluetoothLeAudio>(context, BluetoothProfile.LE_AUDIO)
70 
71     private val flow =
72         intentFlow(
73                 context,
74                 IntentFilter().apply {
75                     addAction(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED)
76                 },
77                 scope,
78             )
79             .shareIn(scope, SharingStarted.Eagerly)
80 
81     private var audioTrack: AudioTrack? = null
82 
83     private class PresetInfoChanged(
84         var connection: Connection,
85         var presetInfoList: List<BluetoothHapPresetInfo>,
86         var reason: Int,
87     ) {}
88 
89     private val mPresetChanged = callbackFlow {
90         val callback =
91             object : BluetoothHapClient.Callback {
92                 override fun onPresetSelected(
93                     device: BluetoothDevice,
94                     presetIndex: Int,
95                     reason: Int,
96                 ) {
97                     Log.i(TAG, "$device preset info changed")
98                 }
99 
100                 override fun onPresetSelectionFailed(device: BluetoothDevice, reason: Int) {
101                     trySend(null)
102                 }
103 
104                 override fun onPresetSelectionForGroupFailed(hapGroupId: Int, reason: Int) {
105                     trySend(null)
106                 }
107 
108                 override fun onPresetInfoChanged(
109                     device: BluetoothDevice,
110                     presetInfoList: List<BluetoothHapPresetInfo>,
111                     reason: Int,
112                 ) {
113                     Log.i(TAG, "$device preset info changed")
114 
115                     var infoChanged =
116                         PresetInfoChanged(device.toConnection(TRANSPORT_LE), presetInfoList, reason)
117 
118                     trySend(infoChanged)
119                 }
120 
121                 override fun onSetPresetNameFailed(device: BluetoothDevice, reason: Int) {
122                     trySend(null)
123                 }
124 
125                 override fun onSetPresetNameForGroupFailed(hapGroupId: Int, reason: Int) {
126                     trySend(null)
127                 }
128             }
129 
130         bluetoothHapClient.registerCallback(Executors.newSingleThreadExecutor(), callback)
131 
132         awaitClose { bluetoothHapClient.unregisterCallback(callback) }
133     }
134 
135     override fun close() {
136         // Deinit the CoroutineScope
137         scope.cancel()
138     }
139 
140     override fun getFeatures(
141         request: GetFeaturesRequest,
142         responseObserver: StreamObserver<GetFeaturesResponse>,
143     ) {
144         grpcUnary<GetFeaturesResponse>(scope, responseObserver) {
145             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
146             Log.i(TAG, "getFeatures(${device})")
147             GetFeaturesResponse.newBuilder()
148                 .setFeatures(bluetoothHapClient.getFeatures(device))
149                 .build()
150         }
151     }
152 
153     override fun getPresetRecord(
154         request: GetPresetRecordRequest,
155         responseObserver: StreamObserver<GetPresetRecordResponse>,
156     ) {
157         grpcUnary<GetPresetRecordResponse>(scope, responseObserver) {
158             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
159             Log.i(TAG, "getPresetRecord($device, ${request.index})")
160 
161             val presetInfo: BluetoothHapPresetInfo? =
162                 bluetoothHapClient.getPresetInfo(device, request.index)
163 
164             if (presetInfo != null) {
165                 GetPresetRecordResponse.newBuilder()
166                     .setPresetRecord(
167                         PresetRecord.newBuilder()
168                             .setIndex(presetInfo.getIndex())
169                             .setName(presetInfo.getName())
170                             .setIsWritable(presetInfo.isWritable())
171                             .setIsAvailable(presetInfo.isAvailable())
172                     )
173                     .build()
174             } else {
175                 GetPresetRecordResponse.getDefaultInstance()
176             }
177         }
178     }
179 
180     override fun getAllPresetRecords(
181         request: GetAllPresetRecordsRequest,
182         responseObserver: StreamObserver<GetAllPresetRecordsResponse>,
183     ) {
184         grpcUnary<GetAllPresetRecordsResponse>(scope, responseObserver) {
185             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
186             Log.i(TAG, "getAllPresetRecords(${device})")
187 
188             GetAllPresetRecordsResponse.newBuilder()
189                 .addAllPresetRecordList(
190                     bluetoothHapClient
191                         .getAllPresetInfo(device)
192                         .stream()
193                         .map { it: BluetoothHapPresetInfo ->
194                             PresetRecord.newBuilder()
195                                 .setIndex(it.getIndex())
196                                 .setName(it.getName())
197                                 .setIsWritable(it.isWritable())
198                                 .setIsAvailable(it.isAvailable())
199                                 .build()
200                         }
201                         .toList()
202                 )
203                 .build()
204         }
205     }
206 
207     override fun writePresetName(
208         request: WritePresetNameRequest,
209         responseObserver: StreamObserver<Empty>,
210     ) {
211         grpcUnary<Empty>(scope, responseObserver) {
212             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
213             Log.i(TAG, "writePresetName($device, ${request.index}, ${request.name})")
214 
215             bluetoothHapClient.setPresetName(device, request.index, request.name)
216 
217             Empty.getDefaultInstance()
218         }
219     }
220 
221     override fun setActivePreset(
222         request: SetActivePresetRequest,
223         responseObserver: StreamObserver<Empty>,
224     ) {
225         grpcUnary<Empty>(scope, responseObserver) {
226             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
227             Log.i(TAG, "SetActivePreset($device, ${request.index})")
228 
229             bluetoothHapClient.selectPreset(device, request.index)
230 
231             Empty.getDefaultInstance()
232         }
233     }
234 
235     override fun getActivePresetRecord(
236         request: GetActivePresetRecordRequest,
237         responseObserver: StreamObserver<GetActivePresetRecordResponse>,
238     ) {
239         grpcUnary<GetActivePresetRecordResponse>(scope, responseObserver) {
240             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
241             Log.i(TAG, "GetActivePresetRecord($device)")
242 
243             val presetInfo: BluetoothHapPresetInfo? = bluetoothHapClient.getActivePresetInfo(device)
244 
245             if (presetInfo != null) {
246                 GetActivePresetRecordResponse.newBuilder()
247                     .setPresetRecord(
248                         PresetRecord.newBuilder()
249                             .setIndex(presetInfo.getIndex())
250                             .setName(presetInfo.getName())
251                             .setIsWritable(presetInfo.isWritable())
252                             .setIsAvailable(presetInfo.isAvailable())
253                     )
254                     .build()
255             } else {
256                 GetActivePresetRecordResponse.getDefaultInstance()
257             }
258         }
259     }
260 
261     override fun setNextPreset(
262         request: SetNextPresetRequest,
263         responseObserver: StreamObserver<Empty>,
264     ) {
265         grpcUnary<Empty>(scope, responseObserver) {
266             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
267             Log.i(TAG, "setNextPreset($device)")
268 
269             bluetoothHapClient.switchToNextPreset(device)
270 
271             Empty.getDefaultInstance()
272         }
273     }
274 
275     override fun setPreviousPreset(
276         request: SetPreviousPresetRequest,
277         responseObserver: StreamObserver<Empty>,
278     ) {
279         grpcUnary<Empty>(scope, responseObserver) {
280             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
281             Log.i(TAG, "setPreviousPreset($device)")
282 
283             bluetoothHapClient.switchToPreviousPreset(device)
284 
285             Empty.getDefaultInstance()
286         }
287     }
288 
289     override fun haPlaybackAudio(
290         responseObserver: StreamObserver<Empty>
291     ): StreamObserver<HaPlaybackAudioRequest> {
292         Log.i(TAG, "haPlaybackAudio")
293 
294         if (audioTrack == null) {
295             audioTrack = buildAudioTrack()
296         }
297 
298         // Play an audio track.
299         audioTrack!!.play()
300 
301         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
302             responseObserver.onError(
303                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
304             )
305         }
306 
307         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
308         // enabling the test runner to do comparisons between input and output audio signal.
309         // Any volume modification should be done before providing the audio data.
310         if (audioManager.isVolumeFixed) {
311             Log.w(TAG, "Volume is fixed, cannot max out the volume")
312         } else {
313             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
314             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
315                 audioManager.setStreamVolume(
316                     AudioManager.STREAM_MUSIC,
317                     maxVolume,
318                     AudioManager.FLAG_SHOW_UI,
319                 )
320             }
321         }
322 
323         return object : StreamObserver<HaPlaybackAudioRequest> {
324             override fun onNext(request: HaPlaybackAudioRequest) {
325                 val data = request.data.toByteArray()
326                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
327                 if (written != data.size) {
328                     responseObserver.onError(
329                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
330                     )
331                 }
332             }
333 
334             override fun onError(t: Throwable) {
335                 t.printStackTrace()
336                 val sw = StringWriter()
337                 t.printStackTrace(PrintWriter(sw))
338                 responseObserver.onError(
339                     Status.UNKNOWN.withCause(t).withDescription(sw.toString()).asException()
340                 )
341             }
342 
343             override fun onCompleted() {
344                 responseObserver.onNext(Empty.getDefaultInstance())
345                 responseObserver.onCompleted()
346             }
347         }
348     }
349 
350     override fun waitPresetChanged(
351         request: Empty,
352         responseObserver: StreamObserver<WaitPresetChangedResponse>,
353     ) {
354         grpcUnary<WaitPresetChangedResponse>(scope, responseObserver) {
355             val presetChangedReceived = mPresetChanged.first()!!
356             val presetRecordList = arrayListOf<PresetRecord>()
357 
358             for (presetRecord in presetChangedReceived.presetInfoList) {
359                 presetRecordList.add(
360                     PresetRecord.newBuilder()
361                         .setIndex(presetRecord.getIndex())
362                         .setName(presetRecord.getName())
363                         .setIsWritable(presetRecord.isWritable())
364                         .setIsAvailable(presetRecord.isAvailable())
365                         .build()
366                 )
367             }
368 
369             WaitPresetChangedResponse.newBuilder()
370                 .setConnection(presetChangedReceived.connection)
371                 .addAllPresetRecordList(presetRecordList)
372                 .setReason(presetChangedReceived.reason)
373                 .build()
374         }
375     }
376 
377     override fun waitPeripheral(
378         request: WaitPeripheralRequest,
379         responseObserver: StreamObserver<Empty>,
380     ) {
381         grpcUnary<Empty>(scope, responseObserver) {
382             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
383             Log.i(TAG, "waitPeripheral(${device}")
384             if (bluetoothHapClient.getConnectionState(device) != BluetoothProfile.STATE_CONNECTED) {
385                 Log.d(TAG, "Manual call to setConnectionPolicy")
386                 bluetoothHapClient.setConnectionPolicy(device, CONNECTION_POLICY_ALLOWED)
387                 Log.d(TAG, "now waiting for bluetoothHapClient profile connection")
388                 flow
389                     .filter { it.getBluetoothDeviceExtra() == device }
390                     .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
391                     .filter { it == BluetoothProfile.STATE_CONNECTED }
392                     .first()
393             }
394 
395             Empty.getDefaultInstance()
396         }
397     }
398 }
399