Skip to content

Commit ce282dd

Browse files
author
John Qualls
authored
AHOYAPPS-824: Add public audio focus change listener (#62)
* Create manual instrumentation package * Rename AudioSwitchTest.kt -> AudioSwitchIntegrationTest.kt * Add test cases for audio focus change listener * Add impl for audioFocusChangeListener * Update changelog * Add note about listener updates after activation has been called * Add another audio focus test case
1 parent ee6d1dc commit ce282dd

File tree

12 files changed

+229
-96
lines changed

12 files changed

+229
-96
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ val audioSwitch = AudioSwitch(context, loggingEnabled = true)
1212
audioSwitch.start { _, _ -> }
1313
```
1414

15+
- Added another constructor parameter that allows developers to subscribe to system audio focus changes while the library is activated.
16+
17+
```kotlin
18+
val audioSwitch = AudioSwitch(context, audioFocusChangeListener = OnAudioFocusChangeListener { focusChange ->
19+
// Do something with audio focus change
20+
))}
21+
22+
audioSwitch.start { _, _ -> }
23+
// Audio focus changes are received after activating
24+
audioSwitch.activate()
25+
```
26+
1527
### 0.3.0
1628

1729
Enhancements
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.twilio.audioswitch
2+
3+
import android.media.AudioAttributes
4+
import android.media.AudioFocusRequest
5+
import android.media.AudioManager
6+
import android.os.Build
7+
8+
class AudioFocusUtil(
9+
private val audioManager: AudioManager,
10+
private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener
11+
) {
12+
13+
private lateinit var request: AudioFocusRequest
14+
15+
fun requestFocus() {
16+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
17+
val playbackAttributes = AudioAttributes.Builder()
18+
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
19+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
20+
.build()
21+
request = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
22+
.setAudioAttributes(playbackAttributes)
23+
.setAcceptsDelayedFocusGain(true)
24+
.setOnAudioFocusChangeListener(audioFocusChangeListener)
25+
.build()
26+
audioManager.requestAudioFocus(request)
27+
} else {
28+
audioManager.requestAudioFocus(
29+
audioFocusChangeListener,
30+
AudioManager.STREAM_VOICE_CALL,
31+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
32+
}
33+
}
34+
35+
fun abandonFocus() {
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
37+
audioManager.abandonAudioFocusRequest(request)
38+
} else {
39+
audioManager.abandonAudioFocus(audioFocusChangeListener)
40+
}
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package com.twilio.audioswitch
2+
3+
import android.content.Context
4+
import android.media.AudioManager
5+
import androidx.test.annotation.UiThreadTest
6+
import androidx.test.ext.junit.runners.AndroidJUnit4
7+
import androidx.test.platform.app.InstrumentationRegistry
8+
import java.util.concurrent.CountDownLatch
9+
import java.util.concurrent.TimeUnit
10+
import junit.framework.TestCase.assertEquals
11+
import junit.framework.TestCase.assertFalse
12+
import junit.framework.TestCase.assertNotNull
13+
import junit.framework.TestCase.assertTrue
14+
import org.junit.Test
15+
import org.junit.runner.RunWith
16+
17+
@RunWith(AndroidJUnit4::class)
18+
class AudioSwitchIntegrationTest {
19+
20+
@Test
21+
@UiThreadTest
22+
fun it_should_disable_logging_by_default() {
23+
val audioSwitch = AudioSwitch(getInstrumentationContext())
24+
25+
assertFalse(audioSwitch.loggingEnabled)
26+
}
27+
28+
@Test
29+
@UiThreadTest
30+
fun it_should_allow_enabling_logging() {
31+
val audioSwitch = AudioSwitch(getInstrumentationContext())
32+
33+
audioSwitch.loggingEnabled = true
34+
35+
assertTrue(audioSwitch.loggingEnabled)
36+
}
37+
38+
@Test
39+
@UiThreadTest
40+
fun it_should_allow_enabling_logging_at_construction() {
41+
val audioSwitch = AudioSwitch(getInstrumentationContext(), loggingEnabled = true)
42+
43+
assertTrue(audioSwitch.loggingEnabled)
44+
}
45+
46+
@Test
47+
@UiThreadTest
48+
fun it_should_allow_toggling_logging_while_in_use() {
49+
val audioSwitch = AudioSwitch(getInstrumentationContext())
50+
audioSwitch.loggingEnabled = true
51+
assertTrue(audioSwitch.loggingEnabled)
52+
audioSwitch.start { _, _ -> }
53+
val earpiece = audioSwitch.availableAudioDevices
54+
.find { it is AudioDevice.Earpiece }
55+
assertNotNull(earpiece)
56+
audioSwitch.selectDevice(earpiece!!)
57+
assertEquals(earpiece, audioSwitch.selectedAudioDevice)
58+
audioSwitch.stop()
59+
60+
audioSwitch.loggingEnabled = false
61+
assertFalse(audioSwitch.loggingEnabled)
62+
63+
audioSwitch.start { _, _ -> }
64+
audioSwitch.stop()
65+
}
66+
67+
@Test
68+
@UiThreadTest
69+
fun `it_should_return_valid_semver_formatted_version`() {
70+
val semVerRegex = Regex("^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-" +
71+
"Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$")
72+
val version: String = AudioSwitch.VERSION
73+
assertNotNull(version)
74+
assertTrue(version.matches(semVerRegex))
75+
}
76+
77+
@Test
78+
fun it_should_receive_audio_focus_changes_if_configured() {
79+
val audioFocusLostLatch = CountDownLatch(1)
80+
val audioFocusGainedLatch = CountDownLatch(1)
81+
val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
82+
when (focusChange) {
83+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> audioFocusLostLatch.countDown()
84+
AudioManager.AUDIOFOCUS_GAIN -> audioFocusGainedLatch.countDown()
85+
}
86+
}
87+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
88+
val audioSwitch = AudioSwitch(getTargetContext(), true, audioFocusChangeListener)
89+
audioSwitch.start { _, _ -> }
90+
audioSwitch.activate()
91+
}
92+
93+
val audioManager = getInstrumentationContext()
94+
.getSystemService(Context.AUDIO_SERVICE) as AudioManager
95+
val audioFocusUtil = AudioFocusUtil(audioManager, audioFocusChangeListener)
96+
audioFocusUtil.requestFocus()
97+
98+
assertTrue(audioFocusLostLatch.await(5, TimeUnit.SECONDS))
99+
audioFocusUtil.abandonFocus()
100+
assertTrue(audioFocusGainedLatch.await(5, TimeUnit.SECONDS))
101+
}
102+
103+
@Test
104+
fun it_should_acquire_audio_focus_if_it_is_already_acquired_in_the_system() {
105+
val audioFocusLostLatch = CountDownLatch(1)
106+
val audioFocusGainedLatch = CountDownLatch(1)
107+
val audioFocusChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange ->
108+
when (focusChange) {
109+
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> audioFocusLostLatch.countDown()
110+
AudioManager.AUDIOFOCUS_GAIN -> audioFocusGainedLatch.countDown()
111+
}
112+
}
113+
val audioManager = getInstrumentationContext()
114+
.getSystemService(Context.AUDIO_SERVICE) as AudioManager
115+
val audioFocusUtil = AudioFocusUtil(audioManager, audioFocusChangeListener)
116+
audioFocusUtil.requestFocus()
117+
118+
val audioSwitch = AudioSwitch(getTargetContext(), true)
119+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
120+
audioSwitch.start { _, _ -> }
121+
audioSwitch.activate()
122+
}
123+
124+
assertTrue(audioFocusLostLatch.await(5, TimeUnit.SECONDS))
125+
InstrumentationRegistry.getInstrumentation().runOnMainSync {
126+
audioSwitch.stop()
127+
}
128+
assertTrue(audioFocusGainedLatch.await(5, TimeUnit.SECONDS))
129+
}
130+
}

audioswitch/src/androidTest/java/com.twilio.audioswitch/AudioSwitchTest.kt

-74
This file was deleted.

audioswitch/src/androidTest/java/com.twilio.audioswitch/TestUtil.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.bluetooth.BluetoothDevice
55
import android.content.Context
66
import android.content.Intent
77
import android.media.AudioManager
8+
import android.media.AudioManager.OnAudioFocusChangeListener
89
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
910
import com.twilio.audioswitch.android.BuildWrapper
1011
import com.twilio.audioswitch.android.DEVICE_NAME
@@ -25,7 +26,8 @@ internal fun setupFakeAudioSwitch(context: Context):
2526
logger,
2627
audioManager,
2728
BuildWrapper(),
28-
AudioFocusRequestWrapper())
29+
AudioFocusRequestWrapper(),
30+
OnAudioFocusChangeListener {})
2931
val wiredHeadsetReceiver = WiredHeadsetReceiver(context, logger)
3032
val headsetManager = BluetoothAdapter.getDefaultAdapter()?.let { bluetoothAdapter ->
3133
BluetoothHeadsetManager(context, logger, bluetoothAdapter, audioDeviceManager,
@@ -35,6 +37,7 @@ internal fun setupFakeAudioSwitch(context: Context):
3537
}
3638
return Pair(AudioSwitch(context,
3739
logger,
40+
OnAudioFocusChangeListener {},
3841
audioDeviceManager,
3942
wiredHeadsetReceiver,
4043
headsetManager),

audioswitch/src/androidTest/java/com.twilio.audioswitch/ConnectedBluetoothHeadsetTest.kt renamed to audioswitch/src/androidTest/java/com/twilio/audioswitch/manual/ConnectedBluetoothHeadsetTest.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.twilio.audioswitch
1+
package com.twilio.audioswitch.manual
22

33
import android.bluetooth.BluetoothAdapter
44
import android.bluetooth.BluetoothHeadset
@@ -10,6 +10,11 @@ import android.content.IntentFilter
1010
import android.media.AudioManager
1111
import androidx.test.ext.junit.runners.AndroidJUnit4
1212
import androidx.test.platform.app.InstrumentationRegistry
13+
import com.twilio.audioswitch.AudioDevice
14+
import com.twilio.audioswitch.AudioSwitch
15+
import com.twilio.audioswitch.getInstrumentationContext
16+
import com.twilio.audioswitch.isSpeakerPhoneOn
17+
import com.twilio.audioswitch.retryAssertion
1318
import java.util.concurrent.CountDownLatch
1419
import java.util.concurrent.TimeUnit
1520
import junit.framework.TestCase.assertEquals

audioswitch/src/main/java/com/twilio/audioswitch/AudioDeviceManager.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import android.content.pm.PackageManager
66
import android.media.AudioDeviceInfo
77
import android.media.AudioFocusRequest
88
import android.media.AudioManager
9+
import android.media.AudioManager.OnAudioFocusChangeListener
910
import android.os.Build
1011
import com.twilio.audioswitch.android.BuildWrapper
1112
import com.twilio.audioswitch.android.Logger
@@ -18,8 +19,7 @@ internal class AudioDeviceManager(
1819
private val audioManager: AudioManager,
1920
private val build: BuildWrapper = BuildWrapper(),
2021
private val audioFocusRequest: AudioFocusRequestWrapper = AudioFocusRequestWrapper(),
21-
private val audioFocusChangeListener: AudioManager.OnAudioFocusChangeListener =
22-
AudioManager.OnAudioFocusChangeListener { }
22+
private val audioFocusChangeListener: OnAudioFocusChangeListener
2323
) {
2424

2525
private var savedAudioMode = 0
@@ -58,7 +58,7 @@ internal class AudioDeviceManager(
5858
fun setAudioFocus() {
5959
// Request audio focus before making any device switch.
6060
if (build.getVersion() >= Build.VERSION_CODES.O) {
61-
audioRequest = audioFocusRequest.buildRequest()
61+
audioRequest = audioFocusRequest.buildRequest(audioFocusChangeListener)
6262
audioRequest?.let { audioManager.requestAudioFocus(it) }
6363
} else {
6464
audioManager.requestAudioFocus(

audioswitch/src/main/java/com/twilio/audioswitch/AudioFocusRequestWrapper.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ import android.annotation.SuppressLint
44
import android.media.AudioAttributes
55
import android.media.AudioFocusRequest
66
import android.media.AudioManager
7+
import android.media.AudioManager.OnAudioFocusChangeListener
78

89
internal class AudioFocusRequestWrapper {
910

1011
@SuppressLint("NewApi")
11-
fun buildRequest(): AudioFocusRequest {
12+
fun buildRequest(audioFocusChangeListener: OnAudioFocusChangeListener): AudioFocusRequest {
1213
val playbackAttributes = AudioAttributes.Builder()
1314
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
1415
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
1516
.build()
1617
return AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
1718
.setAudioAttributes(playbackAttributes)
1819
.setAcceptsDelayedFocusGain(true)
19-
.setOnAudioFocusChangeListener { i: Int -> }
20+
.setOnAudioFocusChangeListener(audioFocusChangeListener)
2021
.build()
2122
}
2223
}

audioswitch/src/main/java/com/twilio/audioswitch/AudioSwitch.kt

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.twilio.audioswitch
33
import android.bluetooth.BluetoothAdapter
44
import android.content.Context
55
import android.media.AudioManager
6+
import android.media.AudioManager.OnAudioFocusChangeListener
67
import androidx.annotation.VisibleForTesting
78
import com.twilio.audioswitch.AudioDevice.BluetoothHeadset
89
import com.twilio.audioswitch.AudioDevice.Earpiece
@@ -104,17 +105,25 @@ class AudioSwitch {
104105
*
105106
* @param context The application context.
106107
* @param loggingEnabled Toggle whether logging is enabled. This argument is false by default.
108+
* @param audioFocusChangeListener A listener that is invoked when the system audio focus is
109+
* updated. Note that updates are only sent to the listener after [activate] has been called.
107110
*/
108111
@JvmOverloads
109-
constructor(context: Context, loggingEnabled: Boolean = false) : this(context, Logger(loggingEnabled))
112+
constructor(
113+
context: Context,
114+
loggingEnabled: Boolean = false,
115+
audioFocusChangeListener: OnAudioFocusChangeListener = OnAudioFocusChangeListener {}
116+
) : this(context, Logger(loggingEnabled), audioFocusChangeListener)
110117

111118
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
112119
internal constructor(
113120
context: Context,
114121
logger: Logger,
122+
audioFocusChangeListener: OnAudioFocusChangeListener,
115123
audioDeviceManager: AudioDeviceManager = AudioDeviceManager(context,
116124
logger,
117-
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager),
125+
context.getSystemService(Context.AUDIO_SERVICE) as AudioManager,
126+
audioFocusChangeListener = audioFocusChangeListener),
118127
wiredHeadsetReceiver: WiredHeadsetReceiver = WiredHeadsetReceiver(context, logger),
119128
headsetManager: BluetoothHeadsetManager? = BluetoothHeadsetManager.newInstance(context,
120129
logger,

0 commit comments

Comments
 (0)