diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1aab4d51e..c1f57fe6a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,11 +3,15 @@ package="org.oxycblt.auxio"> - + + = 3000) { + if (settingsManager.rewindWithPrev && mPosition >= REWIND_THRESHOLD) { rewind() } else { // Only decrement the index if there's a song to move back to AND if we are not exiting @@ -808,6 +808,8 @@ class PlaybackStateManager private constructor() { } companion object { + private const val REWIND_THRESHOLD = 3000L + @Volatile private var INSTANCE: PlaybackStateManager? = null diff --git a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt index d53af1ab1..8ac1eedba 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/system/PlaybackService.kt @@ -9,7 +9,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.pm.ServiceInfo import android.media.AudioManager -import android.media.audiofx.Visualizer +import android.media.MediaPlayer import android.os.Build import android.os.IBinder import android.os.PowerManager @@ -59,16 +59,18 @@ import org.oxycblt.auxio.ui.getSystemServiceSafe * @author OxygenCobalt */ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Callback, SettingsManager.Callback { - private val player: SimpleExoPlayer by lazy(::newPlayer) - private val playbackManager = PlaybackStateManager.getInstance() - private val settingsManager = SettingsManager.getInstance() - + private lateinit var player: SimpleExoPlayer private lateinit var mediaSession: MediaSessionCompat - private lateinit var notificationManager: NotificationManager + private lateinit var notification: PlaybackNotification + private lateinit var notificationManager: NotificationManager private lateinit var audioReactor: AudioReactor - private lateinit var systemReceiver: SystemEventReceiver + private lateinit var wakeLock: PowerManager.WakeLock + private val systemReceiver = SystemEventReceiver() + + private val playbackManager = PlaybackStateManager.getInstance() + private val settingsManager = SettingsManager.getInstance() private var isForeground = false @@ -92,6 +94,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca // --- PLAYER SETUP --- + player = newPlayer() player.addListener(this@PlaybackService) player.setAudioAttributes( AudioAttributes.Builder() @@ -102,10 +105,11 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca ) audioReactor = AudioReactor(this, player) + wakeLock = getSystemServiceSafe(PowerManager::class).newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, this::class.simpleName + ) - // --- SYSTEM RECEIVER SETUP --- - - systemReceiver = SystemEventReceiver() + // --- CALLBACKS --- // Set up the media button callbacks mediaSession = MediaSessionCompat(this, packageName).apply { @@ -119,6 +123,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca } } + // Then the notif/headset callbacks IntentFilter().apply { addAction(PlaybackNotification.ACTION_LOOP) addAction(PlaybackNotification.ACTION_SHUFFLE) @@ -165,6 +170,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca player.release() mediaSession.release() audioReactor.release() + wakeLock.release() playbackManager.removeCallback(this) settingsManager.removeCallback(this) @@ -185,8 +191,13 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca override fun onPlaybackStateChanged(state: Int) { when (state) { - Player.STATE_READY -> startPollingPosition() + Player.STATE_READY -> { + startPollingPosition() + releaseWakelock() + } + Player.STATE_ENDED -> playbackManager.next() + else -> {} } } @@ -196,6 +207,9 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT) { playbackManager.clearLoopMode() } + + // We use the wakelock to ensure that the CPU is active while music is being loaded + holdWakelock() } override fun onPlayerError(error: ExoPlaybackException) { @@ -376,7 +390,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca val pollFlow = flow { while (true) { emit(player.currentPosition) - delay(500) + delay(POS_POLL_INTERVAL) } }.conflate() @@ -423,6 +437,26 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca isForeground = false } + /** + * Hold the wakelock for the default amount of time [25 Seconds] + */ + private fun holdWakelock() { + logD("Holding wakelock.") + + wakeLock.acquire(WAKELOCK_TIME) + } + + /** + * Release the wakelock if its currently being held. + */ + private fun releaseWakelock() { + logD("Attempting to release the wakelock.") + + if (wakeLock.isHeld) { + wakeLock.release() + } + } + /** * Handle a media button intent. */ @@ -547,5 +581,7 @@ class PlaybackService : Service(), Player.EventListener, PlaybackStateManager.Ca companion object { private const val DISCONNECTED = 0 private const val CONNECTED = 1 + private const val WAKELOCK_TIME = 25000L + private const val POS_POLL_INTERVAL = 500L } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt index 94b7e4b1d..c5e16fa14 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/InterfaceUtils.kt @@ -88,7 +88,7 @@ fun Context.getPlural(@PluralsRes pluralsRes: Int, value: Int): String { * @throws IllegalStateException If the system service cannot be retrieved. */ fun Context.getSystemServiceSafe(serviceClass: KClass): T { - return checkNotNull(ContextCompat.getSystemService(this, serviceClass.java)) { + return requireNotNull(ContextCompat.getSystemService(this, serviceClass.java)) { "System service ${serviceClass.simpleName} could not be instantiated" } } diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index b174e8d00..ffd45738a 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -110,7 +110,7 @@ Auxio's playback system is somewhat unorthodox, as it avoids a lot of the built- PlaybackStateManager───────────────────┘ ``` -`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. This object should ***NEVER*** be used in a UI, as it does not sanitize input and can cause major problems if a Volatile UI interacts with it. It's callback system is also prone to memory leaks if not cleared when done. `PlaybackViewModel` should be used instead, as it exposes stable data and safe functions that UI's can use to interact with the playback state. +`PlaybackStateManager` is the shared object that contains the master copy of the playback state, doing all operations on it. This object should ***NEVER*** be used in a UI, as it does not sanitize input and can cause major problems if a Volatile UI interacts with it. It's callback system is also prone to memory leaks if not cleared when done. `PlaybackViewModel` should be used instead, as it exposes stable data and safe functions that UIs can use to interact with the playback state. `PlaybackService`'s job is to use the playback state to manage the ExoPlayer instance and notification and also modify the state depending on system events, such as when a button is pressed on a headset. It should **never** be bound to, mostly because there is no need given that `PlaybackViewModel` exposes the same data in a much safer fashion.