• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.BluetoothHearingAid
21 import android.bluetooth.BluetoothManager
22 import android.bluetooth.BluetoothProfile
23 import android.bluetooth.BluetoothProfile.STATE_CONNECTED
24 import android.content.Context
25 import android.content.Intent
26 import android.content.IntentFilter
27 import android.media.AudioDeviceCallback
28 import android.media.AudioDeviceInfo
29 import android.media.AudioManager
30 import android.media.AudioRouting
31 import android.media.AudioTrack
32 import android.os.Handler
33 import android.os.Looper
34 import android.util.Log
35 import io.grpc.Status
36 import io.grpc.stub.StreamObserver
37 import java.io.Closeable
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.Dispatchers
40 import kotlinx.coroutines.cancel
41 import kotlinx.coroutines.channels.awaitClose
42 import kotlinx.coroutines.channels.trySendBlocking
43 import kotlinx.coroutines.flow.Flow
44 import kotlinx.coroutines.flow.SharingStarted
45 import kotlinx.coroutines.flow.callbackFlow
46 import kotlinx.coroutines.flow.filter
47 import kotlinx.coroutines.flow.first
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.shareIn
50 import pandora.asha.AshaGrpc.AshaImplBase
51 import pandora.asha.AshaProto.*
52 
53 @kotlinx.coroutines.ExperimentalCoroutinesApi
54 class Asha(val context: Context) : AshaImplBase(), Closeable {
55     private val TAG = "PandoraAsha"
56     private val scope: CoroutineScope
57     private val flow: Flow<Intent>
58 
59     private val bluetoothManager =
60         context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
61     private val bluetoothHearingAid =
62         getProfileProxy<BluetoothHearingAid>(context, BluetoothProfile.HEARING_AID)
63     private val bluetoothAdapter = bluetoothManager.adapter
64     private val audioManager = context.getSystemService(AudioManager::class.java)!!
65 
66     private var audioTrack: AudioTrack? = null
67 
68     init {
69         // Init the CoroutineScope
70         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
71         val intentFilter = IntentFilter()
72         intentFilter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED)
73         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
74     }
75 
closenull76     override fun close() {
77         // Deinit the CoroutineScope
78         scope.cancel()
79     }
80 
waitPeripheralnull81     override fun waitPeripheral(
82         request: WaitPeripheralRequest,
83         responseObserver: StreamObserver<WaitPeripheralResponse>,
84     ) {
85         grpcUnary<WaitPeripheralResponse>(scope, responseObserver) {
86             Log.i(TAG, "waitPeripheral")
87 
88             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
89             Log.d(TAG, "connection address ${device.getAddress()}")
90 
91             if (bluetoothHearingAid.getConnectionState(device) != STATE_CONNECTED) {
92                 Log.d(TAG, "wait for bluetoothHearingAid profile connection")
93                 flow
94                     .filter {
95                         it.getAction() == BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED
96                     }
97                     .filter { it.getBluetoothDeviceExtra() == device }
98                     .map { it.getIntExtra(BluetoothProfile.EXTRA_STATE, BluetoothAdapter.ERROR) }
99                     .filter { it == STATE_CONNECTED }
100                     .first()
101             }
102 
103             WaitPeripheralResponse.getDefaultInstance()
104         }
105     }
106 
startnull107     override fun start(request: StartRequest, responseObserver: StreamObserver<StartResponse>) {
108         grpcUnary<StartResponse>(scope, responseObserver) {
109             Log.i(TAG, "play")
110 
111             // wait until BluetoothHearingAid profile is connected
112             val device = request.connection.toBluetoothDevice(bluetoothAdapter)
113             Log.d(TAG, "connection address ${device.getAddress()}")
114 
115             if (bluetoothHearingAid.getConnectionState(device) != STATE_CONNECTED) {
116                 throw RuntimeException("Hearing aid device is not connected, cannot start")
117             }
118 
119             // wait for hearing aid is added as an audio device
120             val audioDeviceAddedFlow = callbackFlow {
121                 val outputDevices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
122                 for (outputDevice in outputDevices) {
123                     if (
124                         outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
125                             outputDevice.address.equals(device.getAddress())
126                     ) {
127                         trySendBlocking(null)
128                     }
129                 }
130 
131                 val audioDeviceCallback =
132                     object : AudioDeviceCallback() {
133                         override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>) {
134                             for (addedDevice in addedDevices) {
135                                 if (
136                                     addedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID &&
137                                         addedDevice.address.equals(device.getAddress())
138                                 ) {
139                                     Log.d(
140                                         TAG,
141                                         "TYPE_HEARING_AID added with address: ${addedDevice.address}",
142                                     )
143                                     trySendBlocking(null)
144                                 }
145                             }
146                         }
147                     }
148 
149                 audioManager.registerAudioDeviceCallback(
150                     audioDeviceCallback,
151                     Handler(Looper.getMainLooper()),
152                 )
153                 awaitClose { audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) }
154             }
155             audioDeviceAddedFlow.first()
156 
157             if (audioTrack == null) {
158                 audioTrack = buildAudioTrack()
159                 Log.i(TAG, "buildAudioTrack")
160             }
161             audioTrack!!.play()
162 
163             // wait for hearing aid is selected as routed device
164             val audioRoutingFlow = callbackFlow {
165                 if (audioTrack!!.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
166                     Log.d(TAG, "already route to TYPE_HEARING_AID")
167                     trySendBlocking(null)
168                 }
169 
170                 val audioRoutingListener =
171                     object : AudioRouting.OnRoutingChangedListener {
172                         override fun onRoutingChanged(router: AudioRouting) {
173                             if (router.routedDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
174                                 Log.d(TAG, "Route to TYPE_HEARING_AID")
175                                 trySendBlocking(null)
176                             } else {
177                                 val outputDevices =
178                                     audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
179                                 for (outputDevice in outputDevices) {
180                                     Log.d(
181                                         TAG,
182                                         "available output device in listener:${outputDevice.type}",
183                                     )
184                                     if (outputDevice.type == AudioDeviceInfo.TYPE_HEARING_AID) {
185                                         val result = router.setPreferredDevice(outputDevice)
186                                         Log.d(TAG, "setPreferredDevice result:$result")
187                                         trySendBlocking(null)
188                                     }
189                                 }
190                             }
191                         }
192                     }
193 
194                 audioTrack!!.addOnRoutingChangedListener(
195                     audioRoutingListener,
196                     Handler(Looper.getMainLooper()),
197                 )
198                 awaitClose { audioTrack!!.removeOnRoutingChangedListener(audioRoutingListener) }
199             }
200             audioRoutingFlow.first()
201 
202             val minVolume = audioManager.getStreamMinVolume(AudioManager.STREAM_MUSIC)
203             audioManager.setStreamVolume(
204                 AudioManager.STREAM_MUSIC,
205                 minVolume,
206                 AudioManager.FLAG_SHOW_UI,
207             )
208 
209             StartResponse.getDefaultInstance()
210         }
211     }
212 
stopnull213     override fun stop(request: StopRequest, responseObserver: StreamObserver<StopResponse>) {
214         grpcUnary<StopResponse>(scope, responseObserver) {
215             Log.i(TAG, "stop")
216             audioTrack!!.pause()
217             audioTrack!!.flush()
218 
219             StopResponse.getDefaultInstance()
220         }
221     }
222 
playbackAudionull223     override fun playbackAudio(
224         responseObserver: StreamObserver<PlaybackAudioResponse>
225     ): StreamObserver<PlaybackAudioRequest> {
226         Log.i(TAG, "playbackAudio")
227         if (audioTrack!!.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
228             responseObserver.onError(
229                 Status.UNKNOWN.withDescription("AudioTrack is not started").asException()
230             )
231         }
232 
233         // Volume is maxed out to avoid any amplitude modification of the provided audio data,
234         // enabling the test runner to do comparisons between input and output audio signal.
235         // Any volume modification should be done before providing the audio data.
236         if (audioManager.isVolumeFixed) {
237             Log.w(TAG, "Volume is fixed, cannot max out the volume")
238         } else {
239             val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
240             if (audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) < maxVolume) {
241                 audioManager.setStreamVolume(
242                     AudioManager.STREAM_MUSIC,
243                     maxVolume,
244                     AudioManager.FLAG_SHOW_UI,
245                 )
246             }
247         }
248 
249         return object : StreamObserver<PlaybackAudioRequest> {
250             override fun onNext(request: PlaybackAudioRequest) {
251                 val data = request.data.toByteArray()
252                 Log.d(TAG, "audio track writes data=$data")
253                 val written = synchronized(audioTrack!!) { audioTrack!!.write(data, 0, data.size) }
254                 if (written != data.size) {
255                     Log.e(TAG, "AudioTrack write failed")
256                     responseObserver.onError(
257                         Status.UNKNOWN.withDescription("AudioTrack write failed").asException()
258                     )
259                 }
260             }
261 
262             override fun onError(t: Throwable?) {
263                 Log.e(TAG, t.toString())
264                 responseObserver.onError(t)
265             }
266 
267             override fun onCompleted() {
268                 Log.i(TAG, "onCompleted")
269                 responseObserver.onNext(PlaybackAudioResponse.getDefaultInstance())
270                 responseObserver.onCompleted()
271             }
272         }
273     }
274 }
275