-
-
Save LouisCAD/4ca4bd6cb85e3d8b2509492d67b282f7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.media.AudioAttributes | |
import android.os.Build.VERSION.SDK_INT | |
import androidx.annotation.RequiresApi | |
import com.beepiz.extensions.android.media.AudioAttrs.Usage.Voice | |
import splitties.bitflags.withFlag | |
/** | |
* @property allowedCapturePolicy is only supported starting with API 29 | |
* @property muteHapticChannels defaults to true (disabled), and is only supported starting with API 29 | |
*/ | |
data class AudioAttrs( | |
val usage: Usage, | |
val contentType: ContentType, | |
val allowedCapturePolicy: CapturePolicy = CapturePolicy.AllowCaptureByAll, | |
val muteHapticChannels: Boolean = true, | |
val flags: Int | |
) { | |
/** | |
* [legacyAudioTrackLowLatency] works starting from API 24, and is deprecated in API 26. | |
*/ | |
@Suppress("InlinedApi") | |
constructor( | |
usage: Usage, | |
contentType: ContentType, | |
allowedCapturePolicy: CapturePolicy = CapturePolicy.AllowCaptureByAll, | |
muteHapticChannels: Boolean = true, | |
enforceAudibility: Boolean = false, | |
requestHardwareAvSync: Boolean = false, | |
legacyAudioTrackLowLatency: Boolean = false | |
) : this( | |
usage = usage, | |
contentType = contentType, | |
allowedCapturePolicy = allowedCapturePolicy, | |
muteHapticChannels = muteHapticChannels, | |
flags = 0 | |
.withFlag(if (enforceAudibility) AudioAttributes.FLAG_AUDIBILITY_ENFORCED else 0) | |
.withFlag(if (requestHardwareAvSync) AudioAttributes.FLAG_HW_AV_SYNC else 0) | |
.withFlag( | |
if (legacyAudioTrackLowLatency) { | |
@Suppress("deprecation") AudioAttributes.FLAG_LOW_LATENCY | |
} else 0 | |
) | |
) | |
sealed interface Usage { | |
/** E.g.: wake-up alarm. */ | |
object Alarm : Usage | |
enum class Assistance : Usage { | |
/** Accessibility, such as with a screen reader. */ | |
Accessibility, | |
/** Driving or navigation directions. */ | |
NavigationGuidance, | |
/** Sonifications, such as with interface sounds. */ | |
Sonification | |
} | |
/** Audio responses to user queries, audio instructions or help utterances. */ | |
@RequiresApi(26) | |
object Assistant : Usage | |
/** Game audio. */ | |
object Game : Usage | |
/** Media, such as music, or movie soundtracks */ | |
object Media : Usage | |
sealed interface Notification : Usage { | |
/** | |
* Generic notification. See other notification usages for more specialized uses. | |
*/ | |
companion object : Notification | |
sealed interface Communication : Notification { | |
/** Non-immediate type of communication such as e-mail. */ | |
object Delayed : Communication | |
/** "Instant" communication such as chat, or SMS. */ | |
object Instant : Communication | |
/** | |
* Request to enter/end a communication, such as a VoIP communication or | |
* video-conference. | |
*/ | |
object Request : Communication | |
} | |
/** | |
* Designed to attract the user's attention, such as a reminder or low battery warning. | |
*/ | |
object Event : Notification | |
/** Telephony ringtone. */ | |
object Ringtone : Notification | |
} | |
class Other(val value: Int) : Usage | |
object Unknown : Usage | |
sealed interface Voice : Usage { | |
sealed interface Communication : Voice { | |
companion object : Communication | |
object Signalling : Communication | |
} | |
} | |
} | |
enum class ContentType { | |
Movie, Music, Sonification, Speech, Unknown | |
} | |
enum class CapturePolicy { | |
AllowCaptureByAll, | |
AllowCaptureBySystem, | |
AllowCaptureByNone | |
} | |
@RequiresApi(21) | |
fun toAudioAttributes(): AudioAttributes { | |
return AudioAttributes.Builder().also { | |
it.setUsage( | |
when (usage) { | |
Usage.Alarm -> AudioAttributes.USAGE_ALARM | |
is Assistance -> when (usage) { | |
Assistance.Accessibility -> AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY | |
Assistance.NavigationGuidance -> { | |
AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE | |
} | |
Assistance.Sonification -> AudioAttributes.USAGE_ASSISTANCE_SONIFICATION | |
} | |
Usage.Assistant -> @Suppress("InlinedApi") AudioAttributes.USAGE_ASSISTANT | |
Usage.Game -> AudioAttributes.USAGE_GAME | |
Usage.Media -> AudioAttributes.USAGE_MEDIA | |
is Usage.Notification -> when (usage) { | |
is Communication -> when (usage) { | |
Delayed -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED | |
Instant -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT | |
Request -> AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST | |
} | |
Usage.Notification -> AudioAttributes.USAGE_NOTIFICATION | |
Usage.Notification.Event -> AudioAttributes.USAGE_NOTIFICATION_EVENT | |
Usage.Notification.Ringtone -> AudioAttributes.USAGE_NOTIFICATION_RINGTONE | |
} | |
Usage.Unknown -> AudioAttributes.USAGE_UNKNOWN | |
is Usage.Other -> usage.value | |
is Voice.Communication -> when (usage) { | |
Voice.Communication -> AudioAttributes.USAGE_VOICE_COMMUNICATION | |
Voice.Communication.Signalling -> { | |
AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING | |
} | |
} | |
} | |
) | |
it.setContentType( | |
when (contentType) { | |
ContentType.Movie -> AudioAttributes.CONTENT_TYPE_MOVIE | |
ContentType.Music -> AudioAttributes.CONTENT_TYPE_MUSIC | |
ContentType.Sonification -> AudioAttributes.CONTENT_TYPE_SONIFICATION | |
ContentType.Speech -> AudioAttributes.CONTENT_TYPE_SPEECH | |
ContentType.Unknown -> AudioAttributes.CONTENT_TYPE_UNKNOWN | |
} | |
) | |
if (SDK_INT >= 29) { | |
it.setHapticChannelsMuted(muteHapticChannels) | |
it.setAllowedCapturePolicy( | |
when (allowedCapturePolicy) { | |
CapturePolicy.AllowCaptureByAll -> AudioAttributes.ALLOW_CAPTURE_BY_ALL | |
CapturePolicy.AllowCaptureBySystem -> AudioAttributes.ALLOW_CAPTURE_BY_SYSTEM | |
CapturePolicy.AllowCaptureByNone -> AudioAttributes.ALLOW_CAPTURE_BY_NONE | |
} | |
) | |
} | |
it.setFlags(flags) | |
}.build() | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.media.AudioAttributes | |
import android.media.AudioFocusRequest | |
import android.media.AudioManager | |
import android.os.Build.VERSION.SDK_INT | |
import androidx.annotation.RequiresApi | |
import kotlinx.coroutines.flow.* | |
import splitties.mainhandler.mainHandler | |
import splitties.systemservices.audioManager | |
sealed interface AudioFocus { | |
sealed interface Gain : AudioFocus { | |
companion object : Gain | |
sealed interface Transient : Gain { | |
companion object : Transient | |
@RequiresApi(19) | |
object Exclusive : Transient | |
object MayDuck : Transient | |
} | |
} | |
sealed interface Loss : AudioFocus { | |
companion object : Loss | |
sealed interface Transient : Loss { | |
companion object : Transient | |
object CanDuck : Transient | |
} | |
} | |
} | |
/** | |
* The [Flow] of [AudioFocus] passed to [block] can receive any [AudioFocus.Loss] subclass, | |
* and [AudioFocus.Gain] (but not its subclasses). | |
*/ | |
inline fun withAudioFocus( | |
gain: AudioFocus.Gain, | |
audioAttrs: AudioAttrs, | |
acceptDelayedFocusGain: Boolean, | |
willPauseWhenDucked: Boolean = false, | |
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit | |
) { | |
when { | |
SDK_INT >= 26 -> withAudioFocus( | |
gain = gain, | |
audioAttributes = audioAttrs.toAudioAttributes(), | |
acceptDelayedFocusGain = acceptDelayedFocusGain, | |
willPauseWhenDucked = willPauseWhenDucked, | |
block = block | |
) | |
else -> withAudioFocusLegacy( | |
gain = gain, | |
streamType = audioAttrs.legacyStreamType(), | |
block = block | |
) | |
} | |
} | |
@PublishedApi | |
internal inline fun withAudioFocusLegacy( | |
gain: AudioFocus.Gain, | |
streamType: Int, | |
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit | |
) { | |
val focusChangeFlow: MutableStateFlow<AudioFocus> = MutableStateFlow(AudioFocus.Loss) | |
val listener = AudioManager.OnAudioFocusChangeListener { focusChange -> | |
focusChangeFlow.value = audioFocus(focusChange) | |
} | |
@Suppress("deprecation") | |
try { | |
val result = audioManager.requestAudioFocus(listener, streamType, gain.value()) | |
focusChangeFlow.value = audioFocusRequestResult(result) | |
block(focusChangeFlow) | |
} finally { | |
audioManager.abandonAudioFocus(listener) | |
} | |
} | |
/** | |
* The [Flow] of [AudioFocus] passed to [block] can receive any [AudioFocus.Loss] subclass, | |
* and [AudioFocus.Gain] (but not its subclasses). | |
*/ | |
@PublishedApi | |
@RequiresApi(26) | |
internal inline fun withAudioFocus( | |
gain: AudioFocus.Gain, | |
audioAttributes: AudioAttributes, | |
acceptDelayedFocusGain: Boolean, | |
willPauseWhenDucked: Boolean, | |
block: (audioFocusFlow: Flow<AudioFocus>) -> Unit | |
) { | |
val focusChangeFlow: MutableStateFlow<AudioFocus> = MutableStateFlow(AudioFocus.Loss) | |
val audioFocusRequest = AudioFocusRequest.Builder(gain.value()).also { | |
it.setAudioAttributes(audioAttributes) | |
it.setWillPauseWhenDucked(willPauseWhenDucked) | |
it.setAcceptsDelayedFocusGain(acceptDelayedFocusGain) | |
it.setOnAudioFocusChangeListener({ focusChange -> | |
focusChangeFlow.value = audioFocus(focusChange) | |
}, mainHandler) | |
}.build() | |
try { | |
val result = audioManager.requestAudioFocus(audioFocusRequest) | |
focusChangeFlow.value = audioFocusRequestResult(result) | |
block(focusChangeFlow) | |
} finally { | |
audioManager.abandonAudioFocusRequest(audioFocusRequest) | |
} | |
} | |
@PublishedApi | |
internal fun audioFocusRequestResult(result: Int): AudioFocus = when (result) { | |
AudioManager.AUDIOFOCUS_REQUEST_FAILED -> AudioFocus.Loss | |
AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> AudioFocus.Loss.Transient | |
AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> AudioFocus.Gain | |
else -> error("Unexpected audio focus request result: $result") | |
} | |
@PublishedApi | |
internal fun audioFocus(androidValue: Int): AudioFocus = when (androidValue) { | |
AudioManager.AUDIOFOCUS_GAIN -> AudioFocus.Gain | |
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> AudioFocus.Gain.Transient | |
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE -> AudioFocus.Gain.Transient.Exclusive | |
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> AudioFocus.Gain.Transient.MayDuck | |
AudioManager.AUDIOFOCUS_LOSS -> AudioFocus.Loss | |
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> AudioFocus.Loss.Transient | |
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> AudioFocus.Loss.Transient.CanDuck | |
else -> throw IllegalArgumentException("Unexpected audio focus value: $androidValue") | |
} | |
@PublishedApi | |
internal fun AudioFocus.Gain.value(): Int { | |
return when (this) { | |
AudioFocus.Gain -> AudioManager.AUDIOFOCUS_GAIN | |
AudioFocus.Gain.Transient -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT | |
AudioFocus.Gain.Transient.Exclusive -> when { | |
SDK_INT >= 19 -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | |
else -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT | |
} | |
AudioFocus.Gain.Transient.MayDuck -> AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment