/* * MediaPlayerLifecycleSupport.kt * Copyright (C) 2009-2021 Ultrasonic developers * * Distributed under terms of the GNU GPLv3 license. */ package org.moire.ultrasonic.service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.media.AudioManager import android.view.KeyEvent import io.reactivex.rxjava3.disposables.Disposable import org.koin.core.component.KoinComponent import org.koin.core.component.inject import org.moire.ultrasonic.R import org.moire.ultrasonic.app.UApp.Companion.applicationContext import org.moire.ultrasonic.domain.PlayerState import org.moire.ultrasonic.util.CacheCleaner import org.moire.ultrasonic.util.Constants import org.moire.ultrasonic.util.Settings import org.moire.ultrasonic.util.Util.ifNotNull import timber.log.Timber /** * This class is responsible for handling received events for the Media Player implementation * * @author Sindre Mehus */ class MediaPlayerLifecycleSupport : KoinComponent { private val playbackStateSerializer by inject() private val mediaPlayerController by inject() private val downloader by inject() private var created = false private var headsetEventReceiver: BroadcastReceiver? = null private var mediaButtonEventSubscription: Disposable? = null fun onCreate() { onCreate(false, null) } private fun onCreate(autoPlay: Boolean, afterCreated: Runnable?) { if (created) { afterCreated?.run() return } mediaButtonEventSubscription = RxBus.mediaButtonEventObservable.subscribe { handleKeyEvent(it) } registerHeadsetReceiver() mediaPlayerController.onCreate() if (autoPlay) mediaPlayerController.preload() playbackStateSerializer.deserialize { mediaPlayerController.restore( it!!.songs, it.currentPlayingIndex, it.currentPlayingPosition, autoPlay, false ) // Work-around: Serialize again, as the restore() method creates a // serialization without current playing info. playbackStateSerializer.serialize( downloader.getPlaylist(), downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) afterCreated?.run() } CacheCleaner().clean() created = true Timber.i("LifecycleSupport created") } fun onDestroy() { if (!created) return playbackStateSerializer.serializeNow( downloader.getPlaylist(), downloader.currentPlayingIndex, mediaPlayerController.playerPosition ) mediaPlayerController.clear(false) mediaButtonEventSubscription?.dispose() applicationContext().unregisterReceiver(headsetEventReceiver) mediaPlayerController.onDestroy() created = false Timber.i("LifecycleSupport destroyed") } fun receiveIntent(intent: Intent?) { if (intent == null) return val intentAction = intent.action if (intentAction == null || intentAction.isEmpty()) return Timber.i("Received intent: %s", intentAction) if (intentAction == Constants.CMD_PROCESS_KEYCODE) { if (intent.extras != null) { val event = intent.extras!![Intent.EXTRA_KEY_EVENT] as KeyEvent? event.ifNotNull { handleKeyEvent(it) } } } else { handleUltrasonicIntent(intentAction) } } /** * The Headset Intent Receiver is responsible for resuming playback when a headset is inserted * and pausing it when it is removed. * Unfortunately this Intent can't be registered in the AndroidManifest, so it works only * while Ultrasonic is running. */ private fun registerHeadsetReceiver() { val sp = Settings.preferences val context = applicationContext() val spKey = context .getString(R.string.settings_playback_resume_play_on_headphones_plug) headsetEventReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val extras = intent.extras ?: return Timber.i("Headset event for: %s", extras["name"]) val state = extras.getInt("state") if (state == 0) { if (!mediaPlayerController.isJukeboxEnabled) { mediaPlayerController.pause() } } else if (state == 1) { if (!mediaPlayerController.isJukeboxEnabled && sp.getBoolean( spKey, false ) && mediaPlayerController.playerState === PlayerState.PAUSED ) { mediaPlayerController.start() } } } } val headsetIntentFilter = IntentFilter(AudioManager.ACTION_HEADSET_PLUG) applicationContext().registerReceiver(headsetEventReceiver, headsetIntentFilter) } @Suppress("MagicNumber", "ComplexMethod") private fun handleKeyEvent(event: KeyEvent) { if (event.action != KeyEvent.ACTION_DOWN || event.repeatCount > 0) return val keyCode: Int val receivedKeyCode = event.keyCode // Translate PLAY and PAUSE codes to PLAY_PAUSE to improve compatibility with old Bluetooth devices keyCode = if (Settings.singleButtonPlayPause && ( receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PLAY || receivedKeyCode == KeyEvent.KEYCODE_MEDIA_PAUSE ) ) { Timber.i("Single button Play/Pause is set, rewriting keyCode to PLAY_PAUSE") KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE } else receivedKeyCode val autoStart = keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || keyCode == KeyEvent.KEYCODE_MEDIA_NEXT // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start onCreate(autoStart) { when (keyCode) { KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, KeyEvent.KEYCODE_HEADSETHOOK -> mediaPlayerController.togglePlayPause() KeyEvent.KEYCODE_MEDIA_PREVIOUS -> mediaPlayerController.previous() KeyEvent.KEYCODE_MEDIA_NEXT -> mediaPlayerController.next() KeyEvent.KEYCODE_MEDIA_STOP -> mediaPlayerController.stop() KeyEvent.KEYCODE_MEDIA_PLAY -> if (mediaPlayerController.playerState === PlayerState.IDLE) { mediaPlayerController.play() } else if (mediaPlayerController.playerState !== PlayerState.STARTED) { mediaPlayerController.start() } KeyEvent.KEYCODE_MEDIA_PAUSE -> mediaPlayerController.pause() KeyEvent.KEYCODE_1 -> mediaPlayerController.setSongRating(1) KeyEvent.KEYCODE_2 -> mediaPlayerController.setSongRating(2) KeyEvent.KEYCODE_3 -> mediaPlayerController.setSongRating(3) KeyEvent.KEYCODE_4 -> mediaPlayerController.setSongRating(4) KeyEvent.KEYCODE_5 -> mediaPlayerController.setSongRating(5) KeyEvent.KEYCODE_STAR -> mediaPlayerController.toggleSongStarred() else -> { } } } } /** * This function processes the intent that could come from other applications. */ @Suppress("ComplexMethod") private fun handleUltrasonicIntent(intentAction: String) { val isRunning = created // If Ultrasonic is not running, do nothing to stop or pause if ( !isRunning && ( intentAction == Constants.CMD_PAUSE || intentAction == Constants.CMD_STOP ) ) return val autoStart = intentAction == Constants.CMD_PLAY || intentAction == Constants.CMD_RESUME_OR_PLAY || intentAction == Constants.CMD_TOGGLEPAUSE || intentAction == Constants.CMD_PREVIOUS || intentAction == Constants.CMD_NEXT // We can receive intents when everything is stopped, so we need to start onCreate(autoStart) { when (intentAction) { Constants.CMD_PLAY -> mediaPlayerController.play() Constants.CMD_RESUME_OR_PLAY -> // If Ultrasonic wasn't running, the autoStart is enough to resume, // no need to call anything if (isRunning) mediaPlayerController.resumeOrPlay() Constants.CMD_NEXT -> mediaPlayerController.next() Constants.CMD_PREVIOUS -> mediaPlayerController.previous() Constants.CMD_TOGGLEPAUSE -> mediaPlayerController.togglePlayPause() Constants.CMD_STOP -> { // TODO: There is a stop() function, shouldn't we use that? mediaPlayerController.pause() mediaPlayerController.seekTo(0) } Constants.CMD_PAUSE -> mediaPlayerController.pause() } } } }