Skip to content

Commit

Permalink
feat(ios)!: Improve AudioContextIOS (#1591)
Browse files Browse the repository at this point in the history
# Description

- Add assertions for AudioContextIOS
- Convert AudioContextIOS.options from List to Set

Closes #1584 

## Breaking Change

### Migration instructions

Before:
```dart
const AudioContext();
const AudioContextIOS();
AudioContextIOS(options: [ /* ... */ ]);
```

After:
```dart
AudioContext();
AudioContextIOS();
AudioContextIOS(options: { /* ... */ });
```

## Related Issues

Closes #1584
  • Loading branch information
Gustl22 authored Aug 17, 2023
1 parent 2a79644 commit 25fbec0
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 29 deletions.
4 changes: 2 additions & 2 deletions packages/audioplayers/example/lib/tabs/audio_context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class AudioContextTabState extends State<AudioContextTab>
AudioContextConfig audioContextConfig = AudioContextConfig();

/// Set config for each platform individually
AudioContext audioContext = const AudioContext();
AudioContext audioContext = AudioContext();

@override
Widget build(BuildContext context) {
Expand Down Expand Up @@ -194,7 +194,7 @@ class AudioContextTabState extends State<AudioContextTab>
Widget _iosTab() {
final iosOptions = AVAudioSessionOptions.values.map(
(option) {
final options = audioContext.iOS.options.toList();
final options = audioContext.iOS.options;
return Cbx(
option.name,
value: options.contains(option),
Expand Down
8 changes: 4 additions & 4 deletions packages/audioplayers/test/global_audioplayers_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ void main() {
/// If using [AVAudioSessionCategory.playAndRecord] the audio will come from
/// the earpiece unless [AVAudioSessionOptions.defaultToSpeaker] is used.
test('set AudioContext', () async {
await globalScope.setAudioContext(const AudioContext());
await globalScope.setAudioContext(AudioContext());
final call = globalPlatform.popLastCall();
expect(call.method, 'setGlobalAudioContext');
expect(
call.value,
const AudioContext(
android: AudioContextAndroid(
AudioContext(
android: const AudioContextAndroid(
isSpeakerphoneOn: false,
audioMode: AndroidAudioMode.normal,
stayAwake: false,
Expand All @@ -49,7 +49,7 @@ void main() {
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: [],
options: const {},
),
),
);
Expand Down
166 changes: 156 additions & 10 deletions packages/audioplayers_platform_interface/lib/src/api/audio_context.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';

/// An Audio Context is a set of secondary, platform-specific aspects of audio
/// playback, typically related to how the act of playing audio interacts with
/// other features of the device. [AudioContext] is containing platform specific
/// configurations: [AudioContextAndroid] and [AudioContextIOS].
@immutable
class AudioContext {
final AudioContextAndroid android;
final AudioContextIOS iOS;
late final AudioContextIOS iOS;

const AudioContext({
this.android = const AudioContextAndroid(),
this.iOS = const AudioContextIOS(),
});
AudioContext({
AudioContextAndroid? android,
AudioContextIOS? iOS,
}) : android = android ?? const AudioContextAndroid() {
this.iOS = iOS ?? AudioContextIOS();
}

AudioContext copy({
AudioContextAndroid? android,
Expand All @@ -36,10 +40,34 @@ class AudioContext {
return <String, dynamic>{};
}
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContext &&
runtimeType == other.runtimeType &&
android == other.android &&
iOS == other.iOS;
}

@override
int get hashCode => Object.hash(
android,
iOS,
);

@override
String toString() {
return 'AudioContext('
'android: $android, '
'iOS: $iOS'
')';
}
}

/// A platform-specific class to encapsulate a collection of attributes about an
/// Android audio stream.
@immutable
class AudioContextAndroid {
/// Sets the speakerphone on or off, globally.
///
Expand Down Expand Up @@ -98,23 +126,118 @@ class AudioContextAndroid {
'audioFocus': audioFocus.value,
};
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContextAndroid &&
runtimeType == other.runtimeType &&
isSpeakerphoneOn == other.isSpeakerphoneOn &&
audioMode == other.audioMode &&
stayAwake == other.stayAwake &&
contentType == other.contentType &&
usageType == other.usageType &&
audioFocus == other.audioFocus;
}

@override
int get hashCode => Object.hash(
isSpeakerphoneOn,
audioMode,
stayAwake,
contentType,
usageType,
audioFocus,
);

@override
String toString() {
return 'AudioContextAndroid('
'isSpeakerphoneOn: $isSpeakerphoneOn, '
'audioMode: $audioMode, '
'stayAwake: $stayAwake, '
'contentType: $contentType, '
'usageType: $usageType, '
'audioFocus: $audioFocus'
')';
}
}

/// A platform-specific class to encapsulate a collection of attributes about an
/// iOS audio stream.
@immutable
class AudioContextIOS {
final AVAudioSessionCategory category;
final List<AVAudioSessionOptions> options;
final Set<AVAudioSessionOptions> options;

// Note when changing the defaults, it should also be changed in native code.
const AudioContextIOS({
AudioContextIOS({
this.category = AVAudioSessionCategory.playback,
this.options = const [],
});
this.options = const {},
}) : assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.mixWithOthers),
'You can set the option `mixWithOthers` explicitly only if the '
'audio session category is `playAndRecord`, `playback`, or '
'`multiRoute`.'),
assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.duckOthers),
'You can set the option `duckOthers` explicitly only if the audio '
'session category is `playAndRecord`, `playback`, or `multiRoute`.',
),
assert(
category == AVAudioSessionCategory.playback ||
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(
AVAudioSessionOptions.interruptSpokenAudioAndMixWithOthers,
),
'You can set the option `interruptSpokenAudioAndMixWithOthers` '
'explicitly only if the audio session category is `playAndRecord`, '
'`playback`, or `multiRoute`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
!options.contains(AVAudioSessionOptions.allowBluetooth),
'You can set the option `allowBluetooth` explicitly only if the '
'audio session category is `playAndRecord` or `record`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(AVAudioSessionOptions.allowBluetoothA2DP),
'You can set the option `allowBluetoothA2DP` explicitly only if '
'the audio session category is `playAndRecord`, `record`, or '
'`multiRoute`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
!options.contains(AVAudioSessionOptions.allowAirPlay),
'You can set the option `allowAirPlay` explicitly only if the '
'audio session category is `playAndRecord`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
!options.contains(AVAudioSessionOptions.defaultToSpeaker),
'You can set the option `defaultToSpeaker` explicitly only if the '
'audio session category is `playAndRecord`.'),
assert(
category == AVAudioSessionCategory.playAndRecord ||
category == AVAudioSessionCategory.record ||
category == AVAudioSessionCategory.multiRoute ||
!options.contains(
AVAudioSessionOptions.overrideMutedMicrophoneInterruption,
),
'You can set the option `overrideMutedMicrophoneInterruption` '
'explicitly only if the audio session category is `playAndRecord`, '
'`record`, or `multiRoute`.');

AudioContextIOS copy({
AVAudioSessionCategory? category,
List<AVAudioSessionOptions>? options,
Set<AVAudioSessionOptions>? options,
}) {
return AudioContextIOS(
category: category ?? this.category,
Expand All @@ -128,6 +251,29 @@ class AudioContextIOS {
'options': options.map((e) => e.name).toList(),
};
}

@override
bool operator ==(Object other) {
return identical(this, other) ||
other is AudioContextIOS &&
runtimeType == other.runtimeType &&
category == other.category &&
const SetEquality().equals(options, other.options);
}

@override
int get hashCode => Object.hash(
category,
options,
);

@override
String toString() {
return 'AudioContextIOS('
'category: $category, '
'options: $options'
')';
}
}

/// "what" you are playing. The content type expresses the general category of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,20 @@ class AudioContextConfig {
: (route == AudioContextConfigRoute.earpiece
? AVAudioSessionCategory.playAndRecord
: AVAudioSessionCategory.playback)),
options: (duckAudio
? [AVAudioSessionOptions.duckOthers]
: <AVAudioSessionOptions>[]) +
(route == AudioContextConfigRoute.speaker
? [AVAudioSessionOptions.defaultToSpeaker]
: []),
options: {
if (duckAudio) AVAudioSessionOptions.duckOthers,
if (route == AudioContextConfigRoute.speaker)
AVAudioSessionOptions.defaultToSpeaker,
},
);
}

void validateIOS() {
// Please create a custom [AudioContextIOS] if the generic flags cannot
// represent your needs.
if (respectSilence && route == AudioContextConfigRoute.speaker) {
throw 'On iOS it is impossible to set both respectSilence and '
'forceSpeaker';
throw 'On iOS it is impossible to set both `respectSilence` and route '
'`speaker`';
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/audioplayers_platform_interface/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ homepage: https://github.com/bluefireteam/audioplayers
repository: https://github.com/bluefireteam/audioplayers/tree/master/packages/audioplayers_platform_interface

dependencies:
collection: ^1.17.1
flutter:
sdk: flutter
meta: ^1.7.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//ignore_for_file: avoid_redundant_argument_values

import 'package:audioplayers_platform_interface/audioplayers_platform_interface.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

test('Create default AudioContext', () async {
final context = AudioContext();
expect(
context,
AudioContext(
android: const AudioContextAndroid(
isSpeakerphoneOn: false,
audioMode: AndroidAudioMode.normal,
stayAwake: false,
contentType: AndroidContentType.music,
usageType: AndroidUsageType.media,
audioFocus: AndroidAudioFocus.gain,
),
iOS: AudioContextIOS(
category: AVAudioSessionCategory.playback,
options: const {},
),
),
);
});

test('Create invalid AudioContextIOS', () async {
try {
// Throws AssertionError:
AudioContextIOS(
category: AVAudioSessionCategory.ambient,
options: const {AVAudioSessionOptions.mixWithOthers},
);
fail('AssertionError not thrown');
// ignore: avoid_catches_without_on_clauses
} catch (e) {
expect(e, isInstanceOf<AssertionError>());
expect(
(e as AssertionError).message,
'You can set the option `mixWithOthers` explicitly only if the audio '
'session category is `playAndRecord`, `playback`, or `multiRoute`.');
}
});

test('Equality of AudioContextIOS', () async {
final context1 = AudioContextIOS(
category: AVAudioSessionCategory.playAndRecord,
options: const {
AVAudioSessionOptions.mixWithOthers,
AVAudioSessionOptions.defaultToSpeaker,
},
);
final context2 = AudioContextIOS(
category: AVAudioSessionCategory.playAndRecord,
options: const {
AVAudioSessionOptions.defaultToSpeaker,
AVAudioSessionOptions.mixWithOthers,
},
);
expect(context1, context2);
});
}
Loading

0 comments on commit 25fbec0

Please sign in to comment.