@@ -12,14 +12,18 @@ import android.media.AudioDeviceCallback
12
12
import android.media.AudioDeviceInfo
13
13
import android.media.AudioManager
14
14
import android.os.Build
15
+ import android.os.PowerManager
15
16
import android.webkit.JavascriptInterface
16
17
import android.webkit.WebView
18
+ import androidx.core.content.getSystemService
17
19
import kotlinx.coroutines.MainScope
18
20
import kotlinx.coroutines.launch
21
+ import kotlinx.serialization.Serializable
22
+ import kotlinx.serialization.Transient
23
+ import kotlinx.serialization.json.Json
19
24
import timber.log.Timber
20
25
import java.util.concurrent.Executors
21
26
import java.util.concurrent.atomic.AtomicBoolean
22
- import kotlin.math.exp
23
27
24
28
class WebViewAudioManager (
25
29
private val webView : WebView ,
@@ -43,6 +47,12 @@ class WebViewAudioManager(
43
47
44
48
private val audioManager = webView.context.getSystemService(Context .AUDIO_SERVICE ) as AudioManager
45
49
50
+ private val proximitySensorWakeLock by lazy {
51
+ webView.context.getSystemService<PowerManager >()
52
+ ?.takeIf { it.isWakeLockLevelSupported(PowerManager .PROXIMITY_SCREEN_OFF_WAKE_LOCK ) }
53
+ ?.newWakeLock(PowerManager .PROXIMITY_SCREEN_OFF_WAKE_LOCK , " ${webView.context.packageName} :ProximitySensorCallWakeLock" )
54
+ }
55
+
46
56
private val commsDeviceChangedListener = AudioManager .OnCommunicationDeviceChangedListener { device ->
47
57
if (device?.id == expectedNewCommunicationDeviceId) {
48
58
if (device != null ) {
@@ -65,19 +75,38 @@ class WebViewAudioManager(
65
75
66
76
private val audioDeviceCallback = object : AudioDeviceCallback () {
67
77
override fun onAudioDevicesAdded (addedDevices : Array <out AudioDeviceInfo >? ) {
68
- setAvailableAudioDevices()
69
- // TODO: maybe only change the selected device to a new external one
70
- selectDefaultAudioDevice()
78
+ val validNewDevices = addedDevices.orEmpty().filter { it.type in wantedDeviceTypes && it.isSink }
79
+ if (validNewDevices.isEmpty()) return
80
+
81
+ val audioDevices = (listAudioDevices() + validNewDevices).distinctBy { it.id }
82
+ setAvailableAudioDevices(audioDevices.map(SerializableAudioDevice ::fromAudioDeviceInfo))
83
+ // This should automatically switch to a new device if it has a higher priority than the current one
84
+ selectDefaultAudioDevice(audioDevices)
71
85
}
72
86
73
87
override fun onAudioDevicesRemoved (removedDevices : Array <out AudioDeviceInfo >? ) {
88
+ // Update the available devices
74
89
setAvailableAudioDevices()
75
- // TODO: maybe only change the selected device if it was one of the added devices
76
- selectDefaultAudioDevice()
90
+
91
+ // Unless the removed device is the current one, we don't need to do anything else
92
+ val removedCurrentDevice = removedDevices.orEmpty().any { it.id == currentDeviceId }
93
+ if (! removedCurrentDevice) return
94
+
95
+ val previousDevice = previousSelectedDevice
96
+ if (previousDevice != null ) {
97
+ previousSelectedDevice = null
98
+ // If we have a previous device, we should select it again
99
+ audioManager.selectAudioDevice(previousDevice.id.toString())
100
+ } else {
101
+ // If we don't have a previous device, we should select the default one
102
+ selectDefaultAudioDevice()
103
+ }
77
104
}
78
105
}
79
106
107
+ private var currentDeviceId: Int? = null
80
108
private var expectedNewCommunicationDeviceId: Int? = null
109
+ private var previousSelectedDevice: AudioDeviceInfo ? = null
81
110
82
111
val isInCallMode = AtomicBoolean (false )
83
112
@@ -123,6 +152,10 @@ class WebViewAudioManager(
123
152
audioManager.removeOnCommunicationDeviceChangedListener(commsDeviceChangedListener)
124
153
}
125
154
155
+ if (proximitySensorWakeLock?.isHeld == true ) {
156
+ proximitySensorWakeLock?.release()
157
+ }
158
+
126
159
audioManager.mode = AudioManager .MODE_NORMAL
127
160
}
128
161
@@ -131,49 +164,45 @@ class WebViewAudioManager(
131
164
webView.evaluateJavascript(" controls.onOutputDeviceSelect = (id) => { onAudioDeviceSelectedCallback.setOutputDevice(id); };" , null )
132
165
}
133
166
134
- private fun setAvailableAudioDevices () {
135
- val devices = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
136
- audioManager.availableCommunicationDevices.map( CompatAudioDevice ::fromAudioDeviceInfo)
167
+ private fun listAudioDevices (): List < AudioDeviceInfo > {
168
+ return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
169
+ audioManager.availableCommunicationDevices
137
170
} else {
138
171
val rawAudioDevices = audioManager.getDevices(AudioManager .GET_DEVICES_ALL )
139
- rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }.map { CompatAudioDevice .fromAudioDeviceInfo(it) }
172
+ rawAudioDevices.filter { it.type in wantedDeviceTypes && it.isSink }
140
173
}
174
+ }
175
+
176
+ private fun setAvailableAudioDevices (
177
+ devices : List <SerializableAudioDevice > = listAudioDevices().map(SerializableAudioDevice ::fromAudioDeviceInfo),
178
+ ) {
141
179
Timber .d(" Updating available audio devices" )
142
- val deviceList = devices.joinToString(" ," ) { " { 'id': '${it.id} ', 'name': '${deviceName(it.type, it.name)} ' }" }
143
- webView.evaluateJavascript(" controls.setAvailableOutputDevices([$deviceList ]);" , {
180
+ val jsonSerializer = Json {
181
+ encodeDefaults = true
182
+ explicitNulls = false
183
+ }
184
+ val deviceList = jsonSerializer.encodeToString(devices)
185
+ webView.evaluateJavascript(" controls.setAvailableOutputDevices($deviceList );" , {
144
186
Timber .d(" Audio: setAvailableOutputDevices result: $it " )
145
187
})
146
188
}
147
189
148
190
private fun registerWebViewDeviceSelectedCallback () {
149
- val webViewAudioDeviceSelectedCallback = WebViewAudioOutputCallback {
150
- Timber .d(" Audio device selected in webview, id: $it " )
151
- audioManager.selectAudioDevice(it)
191
+ val webViewAudioDeviceSelectedCallback = WebViewAudioOutputCallback { selectedDeviceId ->
192
+ Timber .d(" Audio device selected in webview, id: $selectedDeviceId " )
193
+ previousSelectedDevice = listAudioDevices().find { it.id.toString() == selectedDeviceId }
194
+ audioManager.selectAudioDevice(selectedDeviceId)
152
195
}
153
196
Timber .d(" Setting onAudioDeviceSelectedCallback javascript interface in webview" )
154
197
webView.addJavascriptInterface(webViewAudioDeviceSelectedCallback, " onAudioDeviceSelectedCallback" )
155
198
}
156
199
157
- @Suppress(" DEPRECATION" )
158
- private fun selectDefaultAudioDevice () {
159
- val selectedDevice = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
160
- val devices = audioManager.availableCommunicationDevices
161
- devices.minByOrNull {
162
- wantedDeviceTypes.indexOf(it.type).let { index ->
163
- // If the device type is not in the wantedDeviceTypes list, we give it a low priority
164
- if (index == - 1 ) Int .MAX_VALUE else index
165
- }
200
+ private fun selectDefaultAudioDevice (availableDevices : List <AudioDeviceInfo > = listAudioDevices()) {
201
+ val selectedDevice = availableDevices.minByOrNull {
202
+ wantedDeviceTypes.indexOf(it.type).let { index ->
203
+ // If the device type is not in the wantedDeviceTypes list, we give it a low priority
204
+ if (index == - 1 ) Int .MAX_VALUE else index
166
205
}
167
- } else {
168
- // If we don't have access to the new APIs, use the deprecated ones
169
- val devices = audioManager.getDevices(AudioManager .GET_DEVICES_ALL )
170
- devices.filter { it.isSink }
171
- .minByOrNull {
172
- wantedDeviceTypes.indexOf(it.type).let { index ->
173
- // If the device type is not in the wantedDeviceTypes list, we give it a low priority
174
- if (index == - 1 ) Int .MAX_VALUE else index
175
- }
176
- }
177
206
}
178
207
179
208
expectedNewCommunicationDeviceId = selectedDevice?.id
@@ -189,32 +218,45 @@ class WebViewAudioManager(
189
218
private fun selectAudioDeviceInWebView (deviceId : String ) {
190
219
MainScope ().launch { webView.evaluateJavascript(" controls.setOutputDevice('$deviceId ');" , null ) }
191
220
}
192
- }
193
221
194
- private fun AudioManager.selectAudioDevice (device : String ) {
195
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
196
- val audioDevice = availableCommunicationDevices.find { it.id.toString() == device }
197
- selectAudioDevice(audioDevice)
198
- } else {
199
- val rawAudioDevices = getDevices(AudioManager .GET_DEVICES_ALL )
200
- val audioDevice = rawAudioDevices.find { it.id.toString() == device }
201
- selectAudioDevice(audioDevice)
222
+ private fun AudioManager.selectAudioDevice (device : String ) {
223
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
224
+ val audioDevice = availableCommunicationDevices.find { it.id.toString() == device }
225
+ selectAudioDevice(audioDevice)
226
+ } else {
227
+ val rawAudioDevices = getDevices(AudioManager .GET_DEVICES_ALL )
228
+ val audioDevice = rawAudioDevices.find { it.id.toString() == device }
229
+ selectAudioDevice(audioDevice)
230
+ }
202
231
}
203
- }
204
232
205
- private fun AudioManager.selectAudioDevice (device : AudioDeviceInfo ? ) {
206
- if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
207
- if (device != null ) {
208
- setCommunicationDevice(device)
233
+ private fun AudioManager.selectAudioDevice (device : AudioDeviceInfo ? ) {
234
+ currentDeviceId = device?.id
235
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
236
+ if (device != null ) {
237
+ if (device != communicationDevice) {
238
+ setCommunicationDevice(device)
239
+ }
240
+ } else {
241
+ audioManager.clearCommunicationDevice()
242
+ }
209
243
} else {
210
- Timber .w(" Audio: unable to select audio device with id: ${device?.id} " )
244
+ if (device != null ) {
245
+ isSpeakerphoneOn = device.type == AudioDeviceInfo .TYPE_BUILTIN_SPEAKER
246
+ isBluetoothScoOn = device.type == AudioDeviceInfo .TYPE_BLUETOOTH_SCO
247
+ } else {
248
+ isSpeakerphoneOn = false
249
+ isBluetoothScoOn = false
250
+ }
211
251
}
212
- } else {
213
- if (device != null ) {
214
- isSpeakerphoneOn = device.type == AudioDeviceInfo .TYPE_BUILTIN_SPEAKER
215
- isBluetoothScoOn = device.type == AudioDeviceInfo .TYPE_BLUETOOTH_SCO
216
- } else {
217
- Timber .w(" Audio: unable to select audio device with id: ${device?.id} " )
252
+
253
+ @Suppress(" WakeLock" , " WakeLockTimeout" )
254
+ if (device?.type == AudioDeviceInfo .TYPE_BUILTIN_EARPIECE && proximitySensorWakeLock?.isHeld == false ) {
255
+ // If the device is the built-in earpiece, we need to acquire the proximity sensor wake lock
256
+ proximitySensorWakeLock?.acquire()
257
+ } else if (proximitySensorWakeLock?.isHeld == true ) {
258
+ // If the device is no longer the earpiece, we need to release the wake lock
259
+ proximitySensorWakeLock?.release()
218
260
}
219
261
}
220
262
}
@@ -245,12 +287,7 @@ private fun deviceName(type: Int, name: String): String {
245
287
return if (isBuiltIn(type)) {
246
288
typePart
247
289
} else {
248
- val namePart = if (name.length > 10 ) {
249
- name.substring(0 , 10 ) + " …"
250
- } else {
251
- name
252
- }
253
- " $namePart - $typePart "
290
+ " $typePart - $name "
254
291
}
255
292
}
256
293
@@ -262,15 +299,19 @@ private fun isBuiltIn(type: Int): Boolean = when (type) {
262
299
else -> false
263
300
}
264
301
265
-
266
- data class CompatAudioDevice (
302
+ @Suppress(" unused" )
303
+ @Serializable
304
+ class SerializableAudioDevice (
267
305
val id : String ,
268
306
val name : String ,
269
- val type : Int ,
307
+ @Transient val type : Int = 0 ,
308
+ val isEarpiece : Boolean = type == AudioDeviceInfo .TYPE_BUILTIN_EARPIECE ,
309
+ val isSpeaker : Boolean = type == AudioDeviceInfo .TYPE_BUILTIN_SPEAKER ,
310
+ val isExternalHeadset : Boolean = type == AudioDeviceInfo .TYPE_BLUETOOTH_SCO ,
270
311
) {
271
312
companion object {
272
- fun fromAudioDeviceInfo (audioDeviceInfo : AudioDeviceInfo ): CompatAudioDevice {
273
- return CompatAudioDevice (
313
+ fun fromAudioDeviceInfo (audioDeviceInfo : AudioDeviceInfo ): SerializableAudioDevice {
314
+ return SerializableAudioDevice (
274
315
id = audioDeviceInfo.id.toString(),
275
316
name = deviceName(type = audioDeviceInfo.type, name = audioDeviceInfo.productName.toString()),
276
317
type = audioDeviceInfo.type,
0 commit comments