diff --git a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt index 0ae16091f..eb96510ee 100644 --- a/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt +++ b/app/src/main/java/org/oxycblt/auxio/database/PlaybackStateDatabase.kt @@ -13,7 +13,6 @@ import org.oxycblt.auxio.logD * A SQLite database for managing the persistent playback state and queue. * Yes, I know androidx has Room which supposedly makes database creation easier, but it also * has a crippling bug where it will endlessly allocate rows even if you clear the entire db, so... - * TODO: Turn queue loading info flow * @author OxygenCobalt */ class PlaybackStateDatabase(context: Context) : diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 7d9c676b1..af5cd0c5a 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -19,6 +19,7 @@ import org.oxycblt.auxio.ui.setupAlbumActions /** * The [DetailFragment] for an artist. + * TODO: Show a list of songs? * @author OxygenCobalt */ class ArtistDetailFragment : DetailFragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 7ed402728..fc0e46f24 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -19,7 +19,6 @@ import org.oxycblt.auxio.ui.memberBinding /** * A Base [Fragment] implementing the base features shared across all detail fragments. * TODO: Add custom artist images - * TODO: Add playing item highlighting * @author OxygenCobalt */ abstract class DetailFragment : Fragment() { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt index ad47aa673..feabe7ed1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/AlbumDetailAdapter.kt @@ -13,8 +13,8 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.recycler.DiffCallback -import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder +import org.oxycblt.auxio.recycler.viewholders.Highlightable import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.setTextColorResource diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt index 941bdf597..6cb601374 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt @@ -13,8 +13,8 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.recycler.DiffCallback -import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder +import org.oxycblt.auxio.recycler.viewholders.Highlightable import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.setTextColorResource diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt index 3cd1196ee..af3868147 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt @@ -13,8 +13,8 @@ import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.recycler.DiffCallback -import org.oxycblt.auxio.recycler.Highlightable import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder +import org.oxycblt.auxio.recycler.viewholders.Highlightable import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.setTextColorResource diff --git a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt index ce9e63c4a..6f1a79a9a 100644 --- a/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/library/LibraryFragment.kt @@ -42,7 +42,7 @@ import org.oxycblt.auxio.ui.setupSongActions * A [Fragment] that shows a custom list of [Genre], [Artist], or [Album] data. Also allows for * search functionality. * FIXME: Heisenleak when navving from search - * FIXME: Heisen on older versions + * FIXME: Heisenleak on older versions */ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener { diff --git a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt index bb46bb3c0..fa2de7dbe 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/coil/CoilUtils.kt @@ -14,6 +14,8 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song +// TODO: Add option to ignore MediaStore + /** * Get a bitmap for a song. onDone will be called when the bitmap is loaded. * **Do not use this on the UI elements, instead use the Binding Adapters.** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt index 306e86a89..8bad99ccb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackService.kt @@ -1,5 +1,6 @@ package org.oxycblt.auxio.playback +import android.animation.ValueAnimator import android.app.NotificationManager import android.app.Service import android.bluetooth.BluetoothDevice @@ -15,7 +16,11 @@ import android.os.Parcelable import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaSessionCompat import android.view.KeyEvent +import androidx.core.animation.addListener import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.media.AudioFocusRequestCompat +import androidx.media.AudioManagerCompat import com.google.android.exoplayer2.C import com.google.android.exoplayer2.ExoPlaybackException import com.google.android.exoplayer2.MediaItem @@ -75,7 +80,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca private lateinit var notificationManager: NotificationManager private lateinit var notification: NotificationCompat.Builder - private var changeIsFromAudioFocus = true + private lateinit var audioFocusManager: AudioFocusManager private var isForeground = false private val serviceJob = Job() @@ -103,13 +108,15 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca // Set up AudioFocus/AudioAttributes player.setAudioAttributes( - audioAttributes, settingsManager.doAudioFocus + audioAttributes, false ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { player.experimentalSetOffloadSchedulingEnabled(true) } + audioFocusManager = AudioFocusManager() + // --- SYSTEM RECEIVER SETUP --- // Set up the media button callbacks @@ -173,9 +180,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca // Release everything that could cause a memory leak if left around player.release() mediaSession.release() + audioFocusManager.destroy() playbackManager.removeCallback(this) settingsManager.removeCallback(this) + // The service coroutines last job is to save the state to the DB, before terminating itself. serviceScope.launch { playbackManager.saveStateToDatabase(this@PlaybackService) serviceJob.cancel() @@ -187,8 +196,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca // --- PLAYER EVENT LISTENER OVERRIDES --- override fun onPlaybackStateChanged(state: Int) { - changeIsFromAudioFocus = false - if (state == Player.STATE_ENDED) { playbackManager.next() } else if (state == Player.STATE_READY) { @@ -196,16 +203,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } } - override fun onIsPlayingChanged(isPlaying: Boolean) { - // Only sync the playing status with PlaybackStateManager if the change occurred - // from an Audio Focus change. Nowhere else. - if (isPlaying != playbackManager.isPlaying && changeIsFromAudioFocus) { - playbackManager.setPlayingStatus(isPlaying) - } - - changeIsFromAudioFocus = true - } - override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { // If the song loops while in the LOOP_ONCE mode, then stop looping after that. if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT && @@ -260,11 +257,10 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } override fun onPlayingUpdate(isPlaying: Boolean) { - changeIsFromAudioFocus = false - if (isPlaying && !player.isPlaying) { player.play() notification.updatePlaying(this) + audioFocusManager.requestFocus() startForegroundOrNotify("Play") startPollingPosition() @@ -276,8 +272,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } override fun onLoopUpdate(mode: LoopMode) { - changeIsFromAudioFocus = false - when (mode) { LoopMode.NONE -> { player.repeatMode = Player.REPEAT_MODE_OFF @@ -300,8 +294,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } override fun onSeekConfirm(position: Long) { - changeIsFromAudioFocus = false - player.seekTo(position) } @@ -327,13 +319,6 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca startForegroundOrNotify("Notif action update") } - override fun onAudioFocusUpdate(doAudioFocus: Boolean) { - player.setAudioAttributes( - audioAttributes, - doAudioFocus - ) - } - // --- OTHER FUNCTIONS --- /** @@ -517,6 +502,76 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca return false } + /** + * Object that manages the AudioFocus state. + * Adapted from NewPipe (https://github.com/TeamNewPipe/NewPipe) + */ + inner class AudioFocusManager : AudioManager.OnAudioFocusChangeListener { + private val audioManager = ContextCompat.getSystemService( + this@PlaybackService, AudioManager::class.java + ) ?: error("Cannot obtain AudioManager.") + + private val request = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN) + .setWillPauseWhenDucked(true) + .setOnAudioFocusChangeListener(this) + .build() + + fun requestFocus() { + AudioManagerCompat.requestAudioFocus(audioManager, request) + } + + fun destroy() { + AudioManagerCompat.abandonAudioFocusRequest(audioManager, request) + } + + override fun onAudioFocusChange(focusChange: Int) { + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> onGain() + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> onDuck() + AudioManager.AUDIOFOCUS_LOSS, AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> onLoss() + } + } + + private fun onGain() { + if (settingsManager.doAudioFocus) { + if (player.volume == VOLUME_DUCK && playbackManager.isPlaying) { + player.volume = VOLUME_DUCK + animateVolume(VOLUME_DUCK, VOLUME_FULL) + } else { + playbackManager.setPlayingStatus(true) + } + } + } + + private fun onLoss() { + if (settingsManager.doAudioFocus) { + playbackManager.setPlayingStatus(false) + } + } + + private fun onDuck() { + if (settingsManager.doAudioFocus) { + player.volume = VOLUME_DUCK + } + } + + private fun animateVolume(from: Float, to: Float) { + ValueAnimator().apply { + setFloatValues(from, to) + duration = DUCK_DURATION + addListener( + onStart = { player.volume = from }, + onCancel = { player.volume = to }, + onEnd = { player.volume = to } + ) + addUpdateListener { + player.volume = it.animatedValue as Float + } + start() + } + } + } + /** * A [BroadcastReceiver] for receiving system events from the media notification or the headset. */ @@ -593,5 +648,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca companion object { private const val DISCONNECTED = 0 private const val CONNECTED = 1 + + private const val VOLUME_DUCK = 0.2f + private const val DUCK_DURATION = 1500L + private const val VOLUME_FULL = 1.0f } } diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/Highlightable.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/Highlightable.kt similarity index 76% rename from app/src/main/java/org/oxycblt/auxio/recycler/Highlightable.kt rename to app/src/main/java/org/oxycblt/auxio/recycler/viewholders/Highlightable.kt index a3e3bcbec..31556c46c 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/Highlightable.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/Highlightable.kt @@ -1,4 +1,4 @@ -package org.oxycblt.auxio.recycler +package org.oxycblt.auxio.recycler.viewholders /** * Interface that allows the highlighting of certain ViewHolders diff --git a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt index 2d8b81474..0c7134c54 100644 --- a/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt +++ b/app/src/main/java/org/oxycblt/auxio/settings/SettingsManager.kt @@ -10,7 +10,7 @@ import org.oxycblt.auxio.ui.ACCENTS /** * Wrapper around the [SharedPreferences] class that writes & reads values without a context. - * TODO: Add option to play song from genre, now that its possible + * TODO: Add option to hide covers * @author OxygenCobalt */ class SettingsManager private constructor(context: Context) : @@ -165,10 +165,6 @@ class SettingsManager private constructor(context: Context) : Keys.KEY_LIBRARY_DISPLAY_MODE -> callbacks.forEach { it.onLibDisplayModeUpdate(libraryDisplayMode) } - - Keys.KEY_AUDIO_FOCUS -> callbacks.forEach { - it.onAudioFocusUpdate(doAudioFocus) - } } } @@ -248,7 +244,7 @@ class SettingsManager private constructor(context: Context) : } /** - * An safe interface for receiving some preference updates. Use/Extend this instead of + * An interface for receiving some preference updates. Use/Extend this instead of * [SharedPreferences.OnSharedPreferenceChangeListener] if possible, as it doesn't require a * context. */ @@ -256,6 +252,5 @@ class SettingsManager private constructor(context: Context) : fun onColorizeNotifUpdate(doColorize: Boolean) {} fun onNotifActionUpdate(useAltAction: Boolean) {} fun onLibDisplayModeUpdate(displayMode: DisplayMode) {} - fun onAudioFocusUpdate(doAudioFocus: Boolean) {} } }