Skip to content

Commit

Permalink
Merge branch 'rwrz-AndroidBluetoothSCO'
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanheise committed Dec 8, 2022
2 parents 004c875 + 4feccf7 commit 103bd49
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
build/

pubspec.lock

.idea
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ private static class Singleton {
private List<AndroidAudioManager> instances = new ArrayList<>();
private AudioFocusRequestCompat audioFocusRequest;
private BroadcastReceiver noisyReceiver;
private BroadcastReceiver scoReceiver;
private Context applicationContext;
private AudioManager audioManager;
private Object audioDeviceCallback;
Expand Down Expand Up @@ -337,13 +338,15 @@ private boolean requestAudioFocus(List<?> args) {
boolean success = status == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
if (success) {
registerNoisyReceiver();
registerScoReceiver();
}
return success;
}

private boolean abandonAudioFocus() {
if (applicationContext == null) return false;
unregisterNoisyReceiver();
unregisterScoReceiver();
if (audioFocusRequest == null) {
return true;
} else {
Expand Down Expand Up @@ -607,6 +610,28 @@ private void unregisterNoisyReceiver() {
noisyReceiver = null;
}

private void registerScoReceiver() {
if (scoReceiver != null) return;
scoReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// emit [onScoAudioStateUpdated] with current state [EXTRA_SCO_AUDIO_STATE] and previous state [EXTRA_SCO_AUDIO_PREVIOUS_STATE]
invokeMethod(
"onScoAudioStateUpdated",
intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1),
intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1)
);
}
};
applicationContext.registerReceiver(scoReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED));
}

private void unregisterScoReceiver() {
if (scoReceiver == null || applicationContext == null) return;
applicationContext.unregisterReceiver(scoReceiver);
scoReceiver = null;
}

private AudioAttributesCompat decodeAudioAttributes(Map<?, ?> attributes) {
AudioAttributesCompat.Builder builder = new AudioAttributesCompat.Builder();
if (attributes.get("contentType") != null) {
Expand Down
4 changes: 2 additions & 2 deletions audio_session.iml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
<excludeFolder url="file://$MODULE_DIR$/.idea" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
</content>
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart Packages" level="project" />
<orderEntry type="library" name="Dart SDK" level="project" />
<orderEntry type="library" name="Flutter Plugins" level="project" />
</component>
Expand Down
92 changes: 85 additions & 7 deletions lib/src/android.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AndroidAudioManager {
const MethodChannel('com.ryanheise.android_audio_manager');
static AndroidAudioManager? _instance;

final _scoAudioUpdatedEventSubject = BehaviorSubject<AndroidScoAudioEvent>();
final _becomingNoisyEventSubject = PublishSubject<void>();
AndroidOnAudioFocusChanged? _onAudioFocusChanged;
AndroidOnAudioDevicesChanged? _onAudioDevicesAdded;
Expand Down Expand Up @@ -54,13 +55,22 @@ class AndroidAudioManager {
_onAudioDevicesRemoved!(_decodeAudioDevices(args[0]));
}
break;
case 'onScoAudioStateUpdated':
_scoAudioUpdatedEventSubject.add(_decodeScoAudioEvent(args));
break;
}
});
}

Stream<void> get becomingNoisyEventStream =>
_becomingNoisyEventSubject.stream;

Stream<AndroidScoAudioEvent> get scoAudioEventStream =>
_scoAudioUpdatedEventSubject.stream;

AndroidScoAudioState? get currentScoAudioState =>
_scoAudioUpdatedEventSubject.valueOrNull?.currentState;

Future<bool> requestAudioFocus(AndroidAudioFocusRequest focusRequest) async {
_onAudioFocusChanged = focusRequest.onAudioFocusChanged;
return (await _channel
Expand Down Expand Up @@ -147,7 +157,6 @@ class AndroidAudioManager {
await _channel.invokeMethod<int>('setRingerMode', [ringerMode.index]);
}

/// (UNTESTED)
Future<void> setStreamVolume(AndroidStreamType streamType, int index,
AndroidAudioVolumeFlags flags) async {
await _channel.invokeMethod(
Expand All @@ -166,17 +175,17 @@ class AndroidAudioManager {
_decodeAudioDevices((await _channel.invokeMethod<List<dynamic>>(
'getAvailableCommunicationDevices')));

/// (UNTESTED) Requires API level 31
/// Requires API level 31
Future<bool> setCommunicationDevice(AndroidAudioDeviceInfo device) async =>
(await _channel
.invokeMethod<bool>('setCommunicationDevice', [device.id]))!;

/// (UNTESTED) Requires API level 31
/// Requires API level 31
Future<AndroidAudioDeviceInfo> getCommunicationDevice() async =>
_decodeAudioDevice(
await _channel.invokeMethod<dynamic>('getCommunicationDevice'));

/// (UNTESTED) Requires API level 31
/// Requires API level 31
Future<void> clearCommunicationDevice() async =>
await _channel.invokeMethod('clearCommunicationDevice');

Expand Down Expand Up @@ -211,17 +220,14 @@ class AndroidAudioManager {
.invokeMethod<bool>('isBluetoothScoAvailableOffCall'))!;
}

/// (UNTESTED)
Future<void> startBluetoothSco() async {
await _channel.invokeMethod('startBluetoothSco');
}

/// (UNTESTED)
Future<void> stopBluetoothSco() async {
await _channel.invokeMethod('stopBluetoothSco');
}

/// (UNTESTED)
Future<void> setBluetoothScoOn(bool enabled) async {
await _channel.invokeMethod<bool>('setBluetoothScoOn', [enabled]);
}
Expand Down Expand Up @@ -395,6 +401,16 @@ class AndroidAudioManager {
return (rawList as List<dynamic>).map(_decodeAudioDevice).toList();
}

AndroidScoAudioEvent _decodeScoAudioEvent(List<dynamic> args) {
final AndroidScoAudioState current = decodeMapEnum(
AndroidScoAudioState.values, args[0],
defaultValue: AndroidScoAudioState.error);
final AndroidScoAudioState previous = decodeMapEnum(
AndroidScoAudioState.values, args[1],
defaultValue: AndroidScoAudioState.error);
return AndroidScoAudioEvent(current, previous);
}

AndroidAudioDeviceInfo _decodeAudioDevice(dynamic raw) {
return AndroidAudioDeviceInfo(
id: raw['id'],
Expand Down Expand Up @@ -932,3 +948,65 @@ class AndroidKeyEvent {
'eventTime': eventTime,
};
}

class AndroidScoAudioState {
/// [error] there was an error trying to obtain the state
///
/// Requires API level 8
static const error = AndroidScoAudioState._(-1);

/// [disconnected] indicating that the SCO audio channel is not established
///
/// Requires API level 8
static const disconnected = AndroidScoAudioState._(0);

/// [connecting] indicating that the SCO audio channel is established
///
/// Requires API level 8
static const connected = AndroidScoAudioState._(1);

/// [connected] indicating that the SCO audio channel is being established
///
/// Requires API level 14
static const connecting = AndroidScoAudioState._(2);

static const values = {
-1: error,
0: disconnected,
1: connected,
2: connecting
};

final int index;

const AndroidScoAudioState._(this.index);

@override
String toString() {
String strIndex = 'error';
switch (index) {
case 0:
strIndex = 'disconnected';
break;
case 1:
strIndex = 'connected';
break;
case 2:
strIndex = 'connecting';
break;
}
return 'AndroidScoAudioState{$strIndex}';
}
}

class AndroidScoAudioEvent {
AndroidScoAudioEvent(this.currentState, this.previousState);

final AndroidScoAudioState currentState;
final AndroidScoAudioState previousState;

@override
String toString() {
return 'AndroidScoAudioEvent{currentState: $currentState, previousState: $previousState}';
}
}

0 comments on commit 103bd49

Please sign in to comment.