6.4.0 commit
This commit is contained in:
parent
770c50efd1
commit
ceabe0ece3
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020231
|
versionCode 3020232
|
||||||
versionName "6.3.7"
|
versionName "6.4.0"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
@ -125,6 +125,15 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
splits {
|
||||||
|
abi {
|
||||||
|
enable true
|
||||||
|
reset()
|
||||||
|
include "arm64-v8a" // Specify the ABI you want to split.
|
||||||
|
universalApk true // This will generate a universal APK that includes all ABIs.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
|
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
|
||||||
|
@ -156,7 +165,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def composeBom = platform('androidx.compose:compose-bom:2024.06.00')
|
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
|
||||||
implementation composeBom
|
implementation composeBom
|
||||||
androidTestImplementation composeBom
|
androidTestImplementation composeBom
|
||||||
|
|
||||||
|
@ -215,6 +224,7 @@ dependencies {
|
||||||
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
// implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||||
|
|
||||||
|
// 5.5.0-b01 is newer than 5.5.0-compose01
|
||||||
implementation 'com.mikepenz:iconics-core:5.5.0-b01'
|
implementation 'com.mikepenz:iconics-core:5.5.0-b01'
|
||||||
implementation 'com.mikepenz:iconics-views:5.5.0-b01'
|
implementation 'com.mikepenz:iconics-views:5.5.0-b01'
|
||||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.3-kotlin@aar'
|
implementation 'com.mikepenz:google-material-typeface:4.0.0.3-kotlin@aar'
|
||||||
|
@ -243,7 +253,7 @@ dependencies {
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
|
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
|
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
|
||||||
androidTestImplementation "androidx.test:runner:1.6.1"
|
androidTestImplementation "androidx.test:runner:1.6.2"
|
||||||
androidTestImplementation "androidx.test:rules:1.6.1"
|
androidTestImplementation "androidx.test:rules:1.6.1"
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||||
androidTestImplementation 'org.awaitility:awaitility:4.2.1'
|
androidTestImplementation 'org.awaitility:awaitility:4.2.1'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package de.test.podcini.playback
|
package de.test.podcini.playback
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
|
@ -46,7 +46,7 @@ class PlaybackTest {
|
||||||
|
|
||||||
private var uiTestUtils: UITestUtils? = null
|
private var uiTestUtils: UITestUtils? = null
|
||||||
protected lateinit var context: Context
|
protected lateinit var context: Context
|
||||||
private var controller: PlaybackController? = null
|
private var controller: ServiceStatusHandler? = null
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -71,7 +71,7 @@ class PlaybackTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupPlaybackController() {
|
private fun setupPlaybackController() {
|
||||||
controller = object : PlaybackController(activityTestRule.activity) {
|
controller = object : ServiceStatusHandler(activityTestRule.activity) {
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@
|
||||||
tools:ignore="ExportedService">
|
tools:ignore="ExportedService">
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
<action android:name="androidx.media3.session.MediaLibraryService"/>
|
||||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||||
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
|
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -1,394 +0,0 @@
|
||||||
package ac.mdiq.podcini.playback
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
|
||||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
|
||||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
|
||||||
import android.content.*
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Communicates with the playback service. GUI classes should use this class to
|
|
||||||
* control playback instead of communicating with the PlaybackService directly.
|
|
||||||
*/
|
|
||||||
@UnstableApi
|
|
||||||
abstract class PlaybackController(private val activity: FragmentActivity) {
|
|
||||||
|
|
||||||
private var mediaInfoLoaded = false
|
|
||||||
private var loadedFeedMediaId: Long = -1
|
|
||||||
private var released = false
|
|
||||||
private var initialized = false
|
|
||||||
private var eventsRegistered = false
|
|
||||||
|
|
||||||
private val mConnection: ServiceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
||||||
if (service is LocalBinder) {
|
|
||||||
playbackService = service.service
|
|
||||||
onPlaybackServiceConnected()
|
|
||||||
if (!released) {
|
|
||||||
queryService()
|
|
||||||
Logd(TAG, "Connection to Service established")
|
|
||||||
} else Logd(TAG, "Connection to playback service has been established, but controller has already been released")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
|
||||||
playbackService = null
|
|
||||||
initialized = false
|
|
||||||
Logd(TAG, "Disconnected from Service")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var prevStatus = PlayerStatus.STOPPED
|
|
||||||
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
Log.d(TAG, "statusUpdate onReceive called with action: ${intent.action}")
|
|
||||||
if (playbackService != null && mPlayerInfo != null) {
|
|
||||||
val info = mPlayerInfo!!
|
|
||||||
Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
|
|
||||||
if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
|
|
||||||
MediaPlayerBase.status = info.playerStatus
|
|
||||||
prevStatus = MediaPlayerBase.status
|
|
||||||
curMedia = info.playable
|
|
||||||
handleStatus()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Logd(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
|
|
||||||
if (isRunning) bindToService()
|
|
||||||
else {
|
|
||||||
MediaPlayerBase.status = PlayerStatus.STOPPED
|
|
||||||
handleStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
Log.d(TAG, "notificationReceiver onReceive called with action: ${intent.action}")
|
|
||||||
val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
|
|
||||||
val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
|
|
||||||
if (code == -1 || type == -1) {
|
|
||||||
Logd(TAG, "Bad arguments. Won't handle intent")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (type) {
|
|
||||||
PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
|
|
||||||
if (playbackService == null && isRunning) {
|
|
||||||
bindToService()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mediaInfoLoaded = false
|
|
||||||
queryService()
|
|
||||||
}
|
|
||||||
PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
fun init() {
|
|
||||||
Logd(TAG, "controller init")
|
|
||||||
if (!eventsRegistered) {
|
|
||||||
procFlowEvents()
|
|
||||||
eventsRegistered = true
|
|
||||||
}
|
|
||||||
if (isRunning) initServiceRunning()
|
|
||||||
else updatePlayButtonShowsPlay(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var eventSink: Job? = null
|
|
||||||
private fun cancelFlowEvents() {
|
|
||||||
eventSink?.cancel()
|
|
||||||
eventSink = null
|
|
||||||
}
|
|
||||||
private fun procFlowEvents() {
|
|
||||||
if (eventSink != null) return
|
|
||||||
eventSink = activity.lifecycleScope.launch {
|
|
||||||
EventFlow.events.collectLatest { event ->
|
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
|
||||||
when (event) {
|
|
||||||
is FlowEvent.PlaybackServiceEvent -> if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED) init()
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun initServiceRunning() {
|
|
||||||
if (initialized) return
|
|
||||||
initialized = true
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
||||||
activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED)
|
|
||||||
activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED)
|
|
||||||
} else {
|
|
||||||
activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED))
|
|
||||||
activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: java.lang.IllegalStateException: Can't call init() after release() has been called
|
|
||||||
// at ac.mdiq.podcini.playback.PlaybackController.initServiceRunning(SourceFile:104)
|
|
||||||
if (!released) {
|
|
||||||
bindToService()
|
|
||||||
} else {
|
|
||||||
released = false
|
|
||||||
bindToService()
|
|
||||||
Logd(TAG, "Testing bindToService if released")
|
|
||||||
// throw IllegalStateException("Can't call init() after release() has been called")
|
|
||||||
}
|
|
||||||
checkMediaInfoLoaded()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called if the PlaybackController is no longer needed, for
|
|
||||||
* example in the activity's onStop() method.
|
|
||||||
*/
|
|
||||||
fun release() {
|
|
||||||
Logd(TAG, "Releasing PlaybackController")
|
|
||||||
try {
|
|
||||||
activity.unregisterReceiver(statusUpdate)
|
|
||||||
activity.unregisterReceiver(notificationReceiver)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
unbind()
|
|
||||||
cancelFlowEvents()
|
|
||||||
released = true
|
|
||||||
eventsRegistered = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun unbind() {
|
|
||||||
try { activity.unbindService(mConnection) } catch (e: IllegalArgumentException) { }
|
|
||||||
initialized = false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should be called in the activity's onPause() method.
|
|
||||||
*/
|
|
||||||
fun pause() {
|
|
||||||
// TODO: why set it to false
|
|
||||||
// mediaInfoLoaded = false
|
|
||||||
Logd(TAG, "pause() does nothing")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to establish a connection to the PlaybackService. If it isn't
|
|
||||||
* running, the PlaybackService will be started with the last played media
|
|
||||||
* as the arguments of the launch intent.
|
|
||||||
*/
|
|
||||||
private fun bindToService() {
|
|
||||||
Logd(TAG, "Trying to connect to service")
|
|
||||||
check(isRunning) { "Trying to bind but service is not running" }
|
|
||||||
val bound = activity.bindService(Intent(activity, PlaybackService::class.java), mConnection, 0)
|
|
||||||
Logd(TAG, "Result for service binding: $bound")
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onPlaybackEnd() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is called whenever the PlaybackService changes its status. This method
|
|
||||||
* should be used to update the GUI or start/cancel background threads.
|
|
||||||
*/
|
|
||||||
private fun handleStatus() {
|
|
||||||
Log.d(TAG, "handleStatus() called status: ${MediaPlayerBase.status}")
|
|
||||||
checkMediaInfoLoaded()
|
|
||||||
|
|
||||||
when (MediaPlayerBase.status) {
|
|
||||||
PlayerStatus.PLAYING -> updatePlayButtonShowsPlay(false)
|
|
||||||
PlayerStatus.PREPARING -> updatePlayButtonShowsPlay(!isStartWhenPrepared)
|
|
||||||
PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
|
|
||||||
updatePlayButtonShowsPlay(true)
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkMediaInfoLoaded() {
|
|
||||||
if (!mediaInfoLoaded || loadedFeedMediaId != curState.curMediaId) {
|
|
||||||
loadedFeedMediaId = curState.curMediaId
|
|
||||||
Logd(TAG, "checkMediaInfoLoaded: $loadedFeedMediaId")
|
|
||||||
loadMediaInfo()
|
|
||||||
}
|
|
||||||
mediaInfoLoaded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun updatePlayButtonShowsPlay(showPlay: Boolean) {}
|
|
||||||
|
|
||||||
abstract fun loadMediaInfo()
|
|
||||||
|
|
||||||
open fun onPlaybackServiceConnected() { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when connection to playback service has been established or
|
|
||||||
* information has to be refreshed
|
|
||||||
*/
|
|
||||||
private fun queryService() {
|
|
||||||
Logd(TAG, "Querying service info")
|
|
||||||
if (playbackService != null && mPlayerInfo != null) {
|
|
||||||
MediaPlayerBase.status = mPlayerInfo!!.playerStatus
|
|
||||||
curMedia = mPlayerInfo!!.playable
|
|
||||||
// make sure that new media is loaded if it's available
|
|
||||||
mediaInfoLoaded = false
|
|
||||||
handleStatus()
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "queryService() was called without an existing connection to playbackservice")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ensureService() {
|
|
||||||
if (curMedia == null) return
|
|
||||||
if (playbackService == null) {
|
|
||||||
PlaybackServiceStarter(activity, curMedia!!).start()
|
|
||||||
// Log.w(TAG, "playbackservice was null, restarted!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun playPause() {
|
|
||||||
if (curMedia == null) return
|
|
||||||
if (playbackService == null) {
|
|
||||||
PlaybackServiceStarter(activity, curMedia!!).start()
|
|
||||||
Logd(TAG, "playbackservice was null, restarted!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (MediaPlayerBase.status) {
|
|
||||||
PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
|
|
||||||
PlayerStatus.PLAYING -> {
|
|
||||||
playbackService?.mPlayer?.pause(true, reinit = false)
|
|
||||||
playbackService?.isSpeedForward = false
|
|
||||||
playbackService?.isFallbackSpeed = false
|
|
||||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
|
||||||
}
|
|
||||||
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
|
||||||
playbackService?.mPlayer?.resume()
|
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
|
||||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!))
|
|
||||||
}
|
|
||||||
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
|
|
||||||
PlayerStatus.INITIALIZED -> {
|
|
||||||
if (playbackService != null) isStartWhenPrepared = true
|
|
||||||
playbackService?.mPlayer?.prepare()
|
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
PlaybackServiceStarter(activity, curMedia!!).callEvenIfRunning(true).start()
|
|
||||||
Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = PlaybackController::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
var playbackService: PlaybackService? = null
|
|
||||||
|
|
||||||
val curPosition: Int
|
|
||||||
get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
|
||||||
|
|
||||||
val duration: Int
|
|
||||||
get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
|
||||||
|
|
||||||
val curSpeedMultiplier: Float
|
|
||||||
get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
|
||||||
|
|
||||||
val isPlayingVideoLocally: Boolean
|
|
||||||
get() = when {
|
|
||||||
isCasting -> false
|
|
||||||
playbackService != null -> currentMediaType == MediaType.VIDEO
|
|
||||||
else -> curMedia?.getMediaType() == MediaType.VIDEO
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isStartWhenPrepared: Boolean
|
|
||||||
get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
|
|
||||||
set(s) {
|
|
||||||
playbackService?.mPlayer?.startWhenPrepared?.set(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mPlayerInfo: MediaPlayerInfo?
|
|
||||||
get() = playbackService?.mPlayer?.playerInfo
|
|
||||||
|
|
||||||
fun seekTo(time: Int) {
|
|
||||||
if (playbackService != null) {
|
|
||||||
playbackService!!.mPlayer?.seekTo(time)
|
|
||||||
// if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, time, duration))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fallbackSpeed(speed: Float) {
|
|
||||||
if (playbackService != null) {
|
|
||||||
when (MediaPlayerBase.status) {
|
|
||||||
PlayerStatus.PLAYING -> {
|
|
||||||
MediaPlayerBase.status = PlayerStatus.FALLBACK
|
|
||||||
setToFallback(speed)
|
|
||||||
}
|
|
||||||
PlayerStatus.FALLBACK -> {
|
|
||||||
MediaPlayerBase.status = PlayerStatus.PLAYING
|
|
||||||
setToFallback(speed)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setToFallback(speed: Float) {
|
|
||||||
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
|
|
||||||
|
|
||||||
if (!playbackService!!.isFallbackSpeed) {
|
|
||||||
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
|
|
||||||
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
|
||||||
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
|
||||||
|
|
||||||
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sleepTimerActive(): Boolean {
|
|
||||||
return playbackService?.taskManager?.isSleepTimerActive ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an intent which starts an audio- or videoplayer, depending on the
|
|
||||||
* type of media that is being played. If the playbackservice is not
|
|
||||||
* running, the type of the last played media will be looked up.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getPlayerActivityIntent(context: Context): Intent {
|
|
||||||
val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
|
|
||||||
else curState.curIsVideo
|
|
||||||
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
|
|
||||||
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Same as [.getPlayerActivityIntent], but here the type of activity
|
|
||||||
* depends on the medaitype that is provided as an argument.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
|
|
||||||
return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
|
|
||||||
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,220 @@
|
||||||
|
package ac.mdiq.podcini.playback
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
|
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||||
|
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Communicates with the playback service. GUI classes should use this class to
|
||||||
|
* control playback instead of communicating with the PlaybackService directly.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
|
private var mediaInfoLoaded = false
|
||||||
|
private var loadedFeedMediaId: Long = -1
|
||||||
|
private var initialized = false
|
||||||
|
|
||||||
|
private var prevStatus = PlayerStatus.STOPPED
|
||||||
|
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Log.d(TAG, "statusUpdate onReceive called with action: ${intent.action}")
|
||||||
|
if (playbackService != null && PlaybackService.mPlayerInfo != null) {
|
||||||
|
val info = PlaybackService.mPlayerInfo!!
|
||||||
|
// Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
|
||||||
|
if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
|
||||||
|
Logd(TAG, "statusUpdate onReceive doing updates")
|
||||||
|
MediaPlayerBase.status = info.playerStatus
|
||||||
|
prevStatus = MediaPlayerBase.status
|
||||||
|
curMedia = info.playable
|
||||||
|
handleStatus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logd(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
|
||||||
|
if (!isRunning) {
|
||||||
|
MediaPlayerBase.status = PlayerStatus.STOPPED
|
||||||
|
handleStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Log.d(TAG, "notificationReceiver onReceive called with action: ${intent.action}")
|
||||||
|
val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
|
||||||
|
val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
|
||||||
|
if (code == -1 || type == -1) {
|
||||||
|
Logd(TAG, "Bad arguments. Won't handle intent")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
|
||||||
|
if (playbackService == null && isRunning) return
|
||||||
|
mediaInfoLoaded = false
|
||||||
|
updateStatus()
|
||||||
|
}
|
||||||
|
PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun init() {
|
||||||
|
Logd(TAG, "controller init")
|
||||||
|
procFlowEvents()
|
||||||
|
if (isRunning) initServiceRunning()
|
||||||
|
else updatePlayButton(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var eventSink: Job? = null
|
||||||
|
private fun cancelFlowEvents() {
|
||||||
|
eventSink?.cancel()
|
||||||
|
eventSink = null
|
||||||
|
}
|
||||||
|
private fun procFlowEvents() {
|
||||||
|
if (eventSink != null) return
|
||||||
|
eventSink = activity.lifecycleScope.launch {
|
||||||
|
EventFlow.events.collectLatest { event ->
|
||||||
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
|
when (event) {
|
||||||
|
is FlowEvent.PlaybackServiceEvent -> {
|
||||||
|
if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_STARTED) {
|
||||||
|
init()
|
||||||
|
updateStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun initServiceRunning() {
|
||||||
|
if (initialized) return
|
||||||
|
initialized = true
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED)
|
||||||
|
} else {
|
||||||
|
activity.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED))
|
||||||
|
activity.registerReceiver(notificationReceiver, IntentFilter(PlaybackService.ACTION_PLAYER_NOTIFICATION))
|
||||||
|
}
|
||||||
|
checkMediaInfoLoaded()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called if the PlaybackController is no longer needed, for
|
||||||
|
* example in the activity's onStop() method.
|
||||||
|
*/
|
||||||
|
fun release() {
|
||||||
|
Logd(TAG, "Releasing PlaybackController")
|
||||||
|
try {
|
||||||
|
activity.unregisterReceiver(statusUpdate)
|
||||||
|
activity.unregisterReceiver(notificationReceiver)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
initialized = false
|
||||||
|
cancelFlowEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onPlaybackEnd() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is called whenever the PlaybackService changes its status. This method
|
||||||
|
* should be used to update the GUI or start/cancel background threads.
|
||||||
|
*/
|
||||||
|
private fun handleStatus() {
|
||||||
|
Log.d(TAG, "handleStatus() called status: ${MediaPlayerBase.status}")
|
||||||
|
checkMediaInfoLoaded()
|
||||||
|
when (MediaPlayerBase.status) {
|
||||||
|
PlayerStatus.PLAYING -> updatePlayButton(false)
|
||||||
|
PlayerStatus.PREPARING -> updatePlayButton(!PlaybackService.isStartWhenPrepared)
|
||||||
|
PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
|
||||||
|
updatePlayButton(true)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkMediaInfoLoaded() {
|
||||||
|
if (!mediaInfoLoaded || loadedFeedMediaId != curState.curMediaId) {
|
||||||
|
loadedFeedMediaId = curState.curMediaId
|
||||||
|
Logd(TAG, "checkMediaInfoLoaded: $loadedFeedMediaId")
|
||||||
|
loadMediaInfo()
|
||||||
|
}
|
||||||
|
mediaInfoLoaded = true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun updatePlayButton(showPlay: Boolean) {}
|
||||||
|
|
||||||
|
abstract fun loadMediaInfo()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when connection to playback service has been established or
|
||||||
|
* information has to be refreshed
|
||||||
|
*/
|
||||||
|
private fun updateStatus() {
|
||||||
|
Logd(TAG, "Querying service info")
|
||||||
|
if (playbackService != null && PlaybackService.mPlayerInfo != null) {
|
||||||
|
MediaPlayerBase.status = PlaybackService.mPlayerInfo!!.playerStatus
|
||||||
|
curMedia = PlaybackService.mPlayerInfo!!.playable
|
||||||
|
// make sure that new media is loaded if it's available
|
||||||
|
mediaInfoLoaded = false
|
||||||
|
handleStatus()
|
||||||
|
} else Log.e(TAG, "queryService() was called without an existing connection to playbackservice")
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = ServiceStatusHandler::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an intent which starts an audio- or videoplayer, depending on the
|
||||||
|
* type of media that is being played. If the playbackservice is not
|
||||||
|
* running, the type of the last played media will be looked up.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPlayerActivityIntent(context: Context): Intent {
|
||||||
|
val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
|
||||||
|
else curState.curIsVideo
|
||||||
|
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
|
||||||
|
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [.getPlayerActivityIntent], but here the type of activity
|
||||||
|
* depends on the medaitype that is provided as an argument.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
|
||||||
|
return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
|
||||||
|
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,22 +3,23 @@ package ac.mdiq.podcini.playback.base
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.Prefs.prefPlaybackSpeed
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
|
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences
|
import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
import android.net.Uri
|
||||||
import android.net.wifi.WifiManager
|
import android.net.wifi.WifiManager
|
||||||
import android.net.wifi.WifiManager.WifiLock
|
import android.net.wifi.WifiManager.WifiLock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Pair
|
import android.util.Pair
|
||||||
import android.view.SurfaceHolder
|
import android.view.SurfaceHolder
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
@ -316,6 +317,28 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun buildMetadata(p: Playable): MediaMetadata {
|
||||||
|
val builder = MediaMetadata.Builder()
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(true)
|
||||||
|
.setArtist(p.getFeedTitle())
|
||||||
|
.setTitle(p.getEpisodeTitle())
|
||||||
|
.setAlbumArtist(p.getFeedTitle())
|
||||||
|
.setDisplayTitle(p.getEpisodeTitle())
|
||||||
|
.setSubtitle(p.getFeedTitle())
|
||||||
|
.setArtworkUri(null)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun buildMediaItem(p: Playable): MediaItem? {
|
||||||
|
val url = p.getStreamUrl() ?: return null
|
||||||
|
val metadata = buildMetadata(p)
|
||||||
|
return MediaItem.Builder()
|
||||||
|
.setMediaId(url)
|
||||||
|
.setUri(Uri.parse(url))
|
||||||
|
.setMediaMetadata(metadata).build()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param currentPosition current position in a media file in ms
|
* @param currentPosition current position in a media file in ms
|
||||||
* @param lastPlayedTime timestamp when was media paused
|
* @param lastPlayedTime timestamp when was media paused
|
||||||
|
|
|
@ -166,17 +166,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
exoPlayer?.setAudioAttributes(b.build(), true)
|
exoPlayer?.setAudioAttributes(b.build(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMetadata(p: Playable): MediaMetadata {
|
|
||||||
val builder = MediaMetadata.Builder()
|
|
||||||
.setArtist(p.getFeedTitle())
|
|
||||||
.setTitle(p.getEpisodeTitle())
|
|
||||||
.setAlbumArtist(p.getFeedTitle())
|
|
||||||
.setDisplayTitle(p.getEpisodeTitle())
|
|
||||||
.setSubtitle(p.getFeedTitle())
|
|
||||||
.setArtworkUri(null)
|
|
||||||
return builder.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||||
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
|
private fun setDataSource(metadata: MediaMetadata, mediaUrl: String, user: String?, password: String?) {
|
||||||
Logd(TAG, "setDataSource: $mediaUrl")
|
Logd(TAG, "setDataSource: $mediaUrl")
|
||||||
|
@ -681,6 +670,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
||||||
|
|
||||||
private var trackSelector: DefaultTrackSelector? = null
|
private var trackSelector: DefaultTrackSelector? = null
|
||||||
|
|
||||||
var exoPlayer: ExoPlayer? = null
|
var exoPlayer: ExoPlayer? = null
|
||||||
|
|
||||||
private var exoplayerListener: Listener? = null
|
private var exoplayerListener: Listener? = null
|
||||||
|
|
|
@ -14,6 +14,8 @@ import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
|
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
|
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.buildMediaItem
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
|
@ -28,6 +30,7 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
|
import ac.mdiq.podcini.storage.database.Episodes.addToHistory
|
||||||
|
@ -74,6 +77,7 @@ import android.webkit.URLUtil
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.media3.common.MediaItem
|
||||||
import androidx.media3.common.MediaMetadata
|
import androidx.media3.common.MediaMetadata
|
||||||
import androidx.media3.common.Player.STATE_ENDED
|
import androidx.media3.common.Player.STATE_ENDED
|
||||||
import androidx.media3.common.Player.STATE_IDLE
|
import androidx.media3.common.Player.STATE_IDLE
|
||||||
|
@ -93,7 +97,9 @@ import kotlin.math.max
|
||||||
* Controls the MediaPlayer that plays a EpisodeMedia-file
|
* Controls the MediaPlayer that plays a EpisodeMedia-file
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class PlaybackService : MediaSessionService() {
|
class PlaybackService : MediaLibraryService() {
|
||||||
|
|
||||||
|
private var mediaSession: MediaLibrarySession? = null
|
||||||
|
|
||||||
internal var mPlayer: MediaPlayerBase? = null
|
internal var mPlayer: MediaPlayerBase? = null
|
||||||
internal lateinit var taskManager: TaskManager
|
internal lateinit var taskManager: TaskManager
|
||||||
|
@ -110,7 +116,6 @@ class PlaybackService : MediaSessionService() {
|
||||||
|
|
||||||
private var autoSkippedFeedMediaId: String? = null
|
private var autoSkippedFeedMediaId: String? = null
|
||||||
internal var normalSpeed = 1.0f
|
internal var normalSpeed = 1.0f
|
||||||
private var mediaSession: MediaSession? = null
|
|
||||||
|
|
||||||
private val mBinder: IBinder = LocalBinder()
|
private val mBinder: IBinder = LocalBinder()
|
||||||
private var clickCount = 0
|
private var clickCount = 0
|
||||||
|
@ -475,20 +480,162 @@ class PlaybackService : MediaSessionService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class LocalBinder : Binder() {
|
val rootItem = MediaItem.Builder()
|
||||||
val service: PlaybackService
|
.setMediaId("CurQueue")
|
||||||
get() = this@PlaybackService
|
.setMediaMetadata(
|
||||||
|
MediaMetadata.Builder()
|
||||||
|
.setIsBrowsable(true)
|
||||||
|
.setIsPlayable(false)
|
||||||
|
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)
|
||||||
|
.setTitle(curQueue.name)
|
||||||
|
.build())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val mediaItemsInQueue: MutableList<MediaItem> by lazy {
|
||||||
|
val list = mutableListOf<MediaItem>()
|
||||||
|
curQueue.episodes.forEach {
|
||||||
|
if (it.media != null) {
|
||||||
|
val item = buildMediaItem(it.media!!)
|
||||||
|
if (item != null) list += item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logd(TAG, "mediaItemsInQueue: ${list.size}")
|
||||||
|
list
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUnbind(intent: Intent): Boolean {
|
val mediaLibrarySessionCK = object: MediaLibrarySession.Callback {
|
||||||
Logd(TAG, "Received onUnbind event")
|
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||||
return super.onUnbind(intent)
|
Logd(TAG, "in MyMediaSessionCallback onConnect")
|
||||||
|
when {
|
||||||
|
session.isMediaNotificationController(controller) -> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
|
||||||
|
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
||||||
|
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
|
||||||
|
notificationCustomButtons.forEach { commandButton ->
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
|
||||||
|
commandButton.sessionCommand?.let(sessionCommands::add)
|
||||||
|
}
|
||||||
|
return MediaSession.ConnectionResult.accept(sessionCommands.build(), playerCommands.build())
|
||||||
|
}
|
||||||
|
session.isAutoCompanionController(controller) -> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
|
||||||
|
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
||||||
|
notificationCustomButtons.forEach { commandButton ->
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
|
||||||
|
commandButton.sessionCommand?.let(sessionCommands::add)
|
||||||
|
}
|
||||||
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||||
|
.setAvailableSessionCommands(sessionCommands.build())
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onConnect other controller: ${controller.toString()}")
|
||||||
|
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||||
|
super.onPostConnect(session, controller)
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onPostConnect")
|
||||||
|
if (notificationCustomButtons.isNotEmpty()) {
|
||||||
|
mediaSession?.setCustomLayout(notificationCustomButtons)
|
||||||
|
// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<SessionResult> {
|
||||||
|
Log.d(TAG, "MyMediaSessionCallback onCustomCommand ${customCommand.customAction}")
|
||||||
|
/* Handling custom command buttons from player notification. */
|
||||||
|
when (customCommand.customAction) {
|
||||||
|
NotificationCustomButton.REWIND.customAction -> mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||||
|
NotificationCustomButton.FORWARD.customAction -> mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||||
|
NotificationCustomButton.SKIP.customAction -> mPlayer?.skip()
|
||||||
|
}
|
||||||
|
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||||
|
}
|
||||||
|
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
|
||||||
|
val settable = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
|
||||||
|
// scope.launch {
|
||||||
|
// // Your app is responsible for storing the playlist and the start position
|
||||||
|
// // to use here
|
||||||
|
// val resumptionPlaylist = restorePlaylist()
|
||||||
|
// settable.set(resumptionPlaylist)
|
||||||
|
// }
|
||||||
|
return settable
|
||||||
|
}
|
||||||
|
override fun onDisconnected(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
||||||
|
Logd(TAG, "in MyMediaSessionCallback onDisconnected")
|
||||||
|
when {
|
||||||
|
session.isMediaNotificationController(controller) -> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onDisconnected isMediaNotificationController")
|
||||||
|
}
|
||||||
|
session.isAutoCompanionController(controller) -> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onDisconnected isAutoCompanionController")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onMediaButtonEvent(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, intent: Intent): Boolean {
|
||||||
|
val keyEvent = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent.extras!!.getParcelable(EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||||
|
else intent.extras!!.getParcelable(EXTRA_KEY_EVENT) as? KeyEvent
|
||||||
|
Log.d(TAG, "onMediaButtonEvent ${keyEvent?.keyCode}")
|
||||||
|
|
||||||
|
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
|
||||||
|
val keyCode = keyEvent.keyCode
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
||||||
|
clickCount++
|
||||||
|
clickHandler.removeCallbacksAndMessages(null)
|
||||||
|
clickHandler.postDelayed({
|
||||||
|
when (clickCount) {
|
||||||
|
1 -> handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
|
||||||
|
2 -> mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||||
|
3 -> mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||||
|
}
|
||||||
|
clickCount = 0
|
||||||
|
}, ViewConfiguration.getDoubleTapTimeout().toLong())
|
||||||
|
return true
|
||||||
|
} else return handleKeycode(keyCode, false)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
override fun onGetItem(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, mediaId: String): ListenableFuture<LibraryResult<MediaItem>> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onGetItem called mediaId:$mediaId")
|
||||||
|
return super.onGetItem(session, browser, mediaId)
|
||||||
|
}
|
||||||
|
override fun onGetLibraryRoot(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, params: LibraryParams?): ListenableFuture<LibraryResult<MediaItem>> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onGetLibraryRoot called")
|
||||||
|
return Futures.immediateFuture(LibraryResult.ofItem(rootItem, params))
|
||||||
|
}
|
||||||
|
override fun onGetChildren(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String, page: Int, pageSize: Int,
|
||||||
|
params: LibraryParams?): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onGetChildren called parentId:$parentId page:$page pageSize:$pageSize")
|
||||||
|
// return super.onGetChildren(session, browser, parentId, page, pageSize, params)
|
||||||
|
return Futures.immediateFuture(LibraryResult.ofItemList(mediaItemsInQueue, params))
|
||||||
|
}
|
||||||
|
override fun onSubscribe(session: MediaLibrarySession, browser: MediaSession.ControllerInfo, parentId: String,
|
||||||
|
params: LibraryParams?): ListenableFuture<LibraryResult<Void>> {
|
||||||
|
return Futures.immediateFuture(LibraryResult.ofVoid())
|
||||||
|
}
|
||||||
|
override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>): ListenableFuture<MutableList<MediaItem>> {
|
||||||
|
Logd(TAG, "MyMediaSessionCallback onAddMediaItems called ${mediaItems.size} ${mediaItems[0]}")
|
||||||
|
/* This is the trickiest part, if you don't do this here, nothing will play */
|
||||||
|
val episode = getEpisodeByGuidOrUrl(null, mediaItems.first().mediaId) ?: return Futures.immediateFuture(mutableListOf())
|
||||||
|
val media = episode.media ?: return Futures.immediateFuture(mutableListOf())
|
||||||
|
if (!InTheatre.isCurMedia(media)) {
|
||||||
|
PlaybackServiceStarter(applicationContext, media).callEvenIfRunning(true).start()
|
||||||
|
EventFlow.postEvent(FlowEvent.PlayEvent(episode))
|
||||||
|
}
|
||||||
|
val updatedMediaItems = mediaItems.map { it.buildUpon().setUri(it.mediaId).build() }.toMutableList()
|
||||||
|
// updatedMediaItems += mediaItemsInQueue
|
||||||
|
// Logd(TAG, "MyMediaSessionCallback onAddMediaItems updatedMediaItems: ${updatedMediaItems.size} ")
|
||||||
|
return Futures.immediateFuture(updatedMediaItems)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Logd(TAG, "Service created.")
|
Logd(TAG, "Service created.")
|
||||||
isRunning = true
|
isRunning = true
|
||||||
|
playbackService = this
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||||
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
|
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
|
||||||
|
@ -523,10 +670,10 @@ class PlaybackService : MediaSessionService() {
|
||||||
recreateMediaPlayer()
|
recreateMediaPlayer()
|
||||||
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
|
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
|
||||||
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
val intent = packageManager.getLaunchIntentForPackage(packageName)
|
||||||
val pendingIntent = PendingIntent.getActivity(this, 0, intent, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
val pendingIntent = PendingIntent.getActivity(this, 0, intent, if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else FLAG_UPDATE_CURRENT)
|
||||||
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
|
mediaSession = MediaLibrarySession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!, mediaLibrarySessionCK)
|
||||||
|
.setId(packageName)
|
||||||
.setSessionActivity(pendingIntent)
|
.setSessionActivity(pendingIntent)
|
||||||
.setCallback(MyMediaSessionCallback())
|
|
||||||
.setCustomLayout(notificationCustomButtons)
|
.setCustomLayout(notificationCustomButtons)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
@ -561,6 +708,7 @@ class PlaybackService : MediaSessionService() {
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
Logd(TAG, "Service is about to be destroyed")
|
Logd(TAG, "Service is about to be destroyed")
|
||||||
|
|
||||||
|
playbackService = null
|
||||||
isRunning = false
|
isRunning = false
|
||||||
currentMediaType = MediaType.UNKNOWN
|
currentMediaType = MediaType.UNKNOWN
|
||||||
castStateListener.destroy()
|
castStateListener.destroy()
|
||||||
|
@ -591,99 +739,19 @@ class PlaybackService : MediaSessionService() {
|
||||||
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
|
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
|
||||||
}
|
}
|
||||||
|
|
||||||
private inner class MyMediaSessionCallback : MediaSession.Callback {
|
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
|
||||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
|
||||||
Logd(TAG, "in MyMediaSessionCallback onConnect")
|
|
||||||
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
|
|
||||||
// .add(NotificationCustomButton.REWIND)
|
|
||||||
// .add(NotificationCustomButton.FORWARD)
|
|
||||||
when {
|
|
||||||
session.isMediaNotificationController(controller) -> {
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onConnect isMediaNotificationController")
|
|
||||||
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
|
|
||||||
notificationCustomButtons.forEach { commandButton ->
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onConnect commandButton ${commandButton.displayName}")
|
|
||||||
commandButton.sessionCommand?.let(sessionCommands::add)
|
|
||||||
}
|
|
||||||
return MediaSession.ConnectionResult.accept(
|
|
||||||
sessionCommands.build(),
|
|
||||||
playerCommands.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
session.isAutoCompanionController(controller) -> {
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onConnect isAutoCompanionController")
|
|
||||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
|
||||||
.setAvailableSessionCommands(sessionCommands.build())
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onConnect other controller")
|
|
||||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
|
|
||||||
super.onPostConnect(session, controller)
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onPostConnect")
|
|
||||||
if (notificationCustomButtons.isNotEmpty()) {
|
|
||||||
mediaSession?.setCustomLayout(notificationCustomButtons)
|
|
||||||
// mediaSession?.setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<SessionResult> {
|
|
||||||
Log.d(TAG, "onCustomCommand ${customCommand.customAction}")
|
|
||||||
/* Handling custom command buttons from player notification. */
|
|
||||||
when (customCommand.customAction) {
|
|
||||||
NotificationCustomButton.REWIND.customAction -> mPlayer?.seekDelta(-rewindSecs * 1000)
|
|
||||||
NotificationCustomButton.FORWARD.customAction -> mPlayer?.seekDelta(fastForwardSecs * 1000)
|
|
||||||
NotificationCustomButton.SKIP.customAction -> mPlayer?.skip()
|
|
||||||
}
|
|
||||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
|
||||||
}
|
|
||||||
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
|
|
||||||
Logd(TAG, "MyMediaSessionCallback onPlaybackResumption ")
|
|
||||||
val settable = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
|
|
||||||
// scope.launch {
|
|
||||||
// // Your app is responsible for storing the playlist and the start position
|
|
||||||
// // to use here
|
|
||||||
// val resumptionPlaylist = restorePlaylist()
|
|
||||||
// settable.set(resumptionPlaylist)
|
|
||||||
// }
|
|
||||||
return settable
|
|
||||||
}
|
|
||||||
override fun onMediaButtonEvent(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, intent: Intent): Boolean {
|
|
||||||
val keyEvent = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) intent.extras!!.getParcelable(EXTRA_KEY_EVENT, KeyEvent::class.java)
|
|
||||||
else intent.extras!!.getParcelable(EXTRA_KEY_EVENT) as? KeyEvent
|
|
||||||
Log.d(TAG, "onMediaButtonEvent ${keyEvent?.keyCode}")
|
|
||||||
|
|
||||||
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
|
|
||||||
val keyCode = keyEvent.keyCode
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
|
||||||
clickCount++
|
|
||||||
clickHandler.removeCallbacksAndMessages(null)
|
|
||||||
clickHandler.postDelayed({
|
|
||||||
when (clickCount) {
|
|
||||||
1 -> handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
|
|
||||||
2 -> mPlayer?.seekDelta(fastForwardSecs * 1000)
|
|
||||||
3 -> mPlayer?.seekDelta(-rewindSecs * 1000)
|
|
||||||
}
|
|
||||||
clickCount = 0
|
|
||||||
}, ViewConfiguration.getDoubleTapTimeout().toLong())
|
|
||||||
return true
|
|
||||||
} else return handleKeycode(keyCode, false)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
|
||||||
return mediaSession
|
return mediaSession
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
// override fun onBind(intent: Intent?): IBinder? {
|
||||||
Logd(TAG, "Received onBind event")
|
// Logd(TAG, "Received onBind event")
|
||||||
return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
|
// return if (intent?.action != null && intent.action == SERVICE_INTERFACE) super.onBind(intent) else mBinder
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// override fun onUnbind(intent: Intent): Boolean {
|
||||||
|
// Logd(TAG, "Received onUnbind event")
|
||||||
|
// return super.onUnbind(intent)
|
||||||
|
// }
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
||||||
|
@ -718,6 +786,11 @@ class PlaybackService : MediaSessionService() {
|
||||||
val handled = handleKeycode(keycode, !hardwareButton)
|
val handled = handleKeycode(keycode, !hardwareButton)
|
||||||
return super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
}
|
}
|
||||||
|
keyEvent != null && keyEvent.keyCode != -1 -> {
|
||||||
|
Logd(TAG, "onStartCommand Received button event: ${keyEvent.keyCode}")
|
||||||
|
val handled = handleKeycode(keyEvent.keyCode, !hardwareButton)
|
||||||
|
return super.onStartCommand(intent, flags, startId)
|
||||||
|
}
|
||||||
playable != null -> {
|
playable != null -> {
|
||||||
Logd(TAG, "onStartCommand status: $status")
|
Logd(TAG, "onStartCommand status: $status")
|
||||||
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
|
val allowStreamThisTime = intent?.getBooleanExtra(EXTRA_ALLOW_STREAM_THIS_TIME, false) ?: false
|
||||||
|
@ -964,6 +1037,7 @@ class PlaybackService : MediaSessionService() {
|
||||||
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
|
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
|
||||||
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
|
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
|
||||||
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
|
Logd(TAG, "onQueueEvent: ending playback curEpisode ${curEpisode?.title}")
|
||||||
|
notifyCurQueueItemsChanged()
|
||||||
for (e in event.episodes) {
|
for (e in event.episodes) {
|
||||||
Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
|
Logd(TAG, "onQueueEvent: ending playback event ${e.title}")
|
||||||
if (e.id == curEpisode?.id) {
|
if (e.id == curEpisode?.id) {
|
||||||
|
@ -974,6 +1048,12 @@ class PlaybackService : MediaSessionService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun notifyCurQueueItemsChanged(range_: Int = -1) {
|
||||||
|
val range = if (range_ > 0) range_ else curQueue.size()
|
||||||
|
Logd(TAG, "notifyCurQueueItemsChanged curQueue: ${curQueue.id}")
|
||||||
|
mediaSession?.notifyChildrenChanged("CurQueue", range, null)
|
||||||
|
}
|
||||||
|
|
||||||
// private fun onVolumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) {
|
// private fun onVolumeAdaptionChanged(event: FlowEvent.VolumeAdaptionChangedEvent) {
|
||||||
// if (mPlayer != null) updateVolumeIfNecessary(mPlayer!!, event.feedId, event.volumeAdaptionSetting)
|
// if (mPlayer != null) updateVolumeIfNecessary(mPlayer!!, event.feedId, event.volumeAdaptionSetting)
|
||||||
// }
|
// }
|
||||||
|
@ -1130,6 +1210,11 @@ class PlaybackService : MediaSessionService() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
val service: PlaybackService
|
||||||
|
get() = this@PlaybackService
|
||||||
|
}
|
||||||
|
|
||||||
enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
|
enum class NotificationCustomButton(val customAction: String, val commandButton: CommandButton) {
|
||||||
SKIP(
|
SKIP(
|
||||||
customAction = CUSTOM_COMMAND_SKIP_ACTION_ID,
|
customAction = CUSTOM_COMMAND_SKIP_ACTION_ID,
|
||||||
|
@ -1211,6 +1296,9 @@ class PlaybackService : MediaSessionService() {
|
||||||
private const val EXTRA_CODE_VIDEO: Int = 2
|
private const val EXTRA_CODE_VIDEO: Int = 2
|
||||||
private const val EXTRA_CODE_CAST: Int = 3
|
private const val EXTRA_CODE_CAST: Int = 3
|
||||||
|
|
||||||
|
var playbackService: PlaybackService? = null
|
||||||
|
var mediaBrowser: MediaBrowser? = null
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
var isRunning: Boolean = false
|
var isRunning: Boolean = false
|
||||||
|
|
||||||
|
@ -1259,6 +1347,88 @@ class PlaybackService : MediaSessionService() {
|
||||||
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply()
|
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefFollowQueue.name, value).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val curPositionFB: Int
|
||||||
|
get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
||||||
|
|
||||||
|
val curDurationFB: Int
|
||||||
|
get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||||
|
|
||||||
|
val curSpeedFB: Float
|
||||||
|
get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
||||||
|
|
||||||
|
val isPlayingVideoLocally: Boolean
|
||||||
|
get() = when {
|
||||||
|
isCasting -> false
|
||||||
|
playbackService != null -> currentMediaType == MediaType.VIDEO
|
||||||
|
else -> curMedia?.getMediaType() == MediaType.VIDEO
|
||||||
|
}
|
||||||
|
|
||||||
|
var isStartWhenPrepared: Boolean
|
||||||
|
get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
|
||||||
|
set(s) {
|
||||||
|
playbackService?.mPlayer?.startWhenPrepared?.set(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mPlayerInfo: MediaPlayerInfo?
|
||||||
|
get() = playbackService?.mPlayer?.playerInfo
|
||||||
|
|
||||||
|
fun seekTo(time: Int) {
|
||||||
|
playbackService?.mPlayer?.seekTo(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleFallbackSpeed(speed: Float) {
|
||||||
|
if (playbackService != null) {
|
||||||
|
when (MediaPlayerBase.status) {
|
||||||
|
PlayerStatus.PLAYING -> {
|
||||||
|
MediaPlayerBase.status = PlayerStatus.FALLBACK
|
||||||
|
setToFallbackSpeed(speed)
|
||||||
|
}
|
||||||
|
PlayerStatus.FALLBACK -> {
|
||||||
|
MediaPlayerBase.status = PlayerStatus.PLAYING
|
||||||
|
setToFallbackSpeed(speed)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setToFallbackSpeed(speed: Float) {
|
||||||
|
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
|
||||||
|
|
||||||
|
if (!playbackService!!.isFallbackSpeed) {
|
||||||
|
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
|
||||||
|
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||||
|
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||||
|
|
||||||
|
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isSleepTimerActive(): Boolean {
|
||||||
|
return playbackService?.taskManager?.isSleepTimerActive ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun playPause() {
|
||||||
|
when (MediaPlayerBase.status) {
|
||||||
|
PlayerStatus.FALLBACK -> toggleFallbackSpeed(1.0f)
|
||||||
|
PlayerStatus.PLAYING -> {
|
||||||
|
playbackService?.mPlayer?.pause(true, reinit = false)
|
||||||
|
playbackService?.isSpeedForward = false
|
||||||
|
playbackService?.isFallbackSpeed = false
|
||||||
|
}
|
||||||
|
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||||
|
playbackService?.mPlayer?.resume()
|
||||||
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
|
}
|
||||||
|
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
|
||||||
|
PlayerStatus.INITIALIZED -> {
|
||||||
|
if (playbackService != null) isStartWhenPrepared = true
|
||||||
|
playbackService?.mPlayer?.prepare()
|
||||||
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
|
}
|
||||||
|
else -> Log.w(TAG, "Play/Pause button was pressed and PlaybackService state was unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
|
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
|
||||||
val playable = curMedia
|
val playable = curMedia
|
||||||
if (playable is EpisodeMedia) {
|
if (playable is EpisodeMedia) {
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.ui.actions.actionbutton.StreamActionButton.StreamingConfirmationDialog
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
|
|
@ -10,7 +10,7 @@ import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.model.RemoteMedia
|
import ac.mdiq.podcini.storage.model.RemoteMedia
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -11,7 +11,6 @@ import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
||||||
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
import ac.mdiq.podcini.net.feed.discovery.ItunesTopListLoader
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
|
||||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
|
import ac.mdiq.podcini.preferences.ThemeSwitcher.getNoTitleTheme
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
|
import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
|
||||||
|
@ -36,7 +35,6 @@ import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -65,8 +63,6 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentContainerView
|
import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.session.MediaController
|
|
||||||
import androidx.media3.session.SessionToken
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.work.WorkInfo
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
|
@ -75,8 +71,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.common.util.concurrent.ListenableFuture
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
|
@ -96,7 +90,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
private lateinit var navDrawerFragment: NavDrawerFragment
|
private lateinit var navDrawerFragment: NavDrawerFragment
|
||||||
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
||||||
private lateinit var audioPlayerView: View
|
private lateinit var audioPlayerView: View
|
||||||
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
// private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||||
private lateinit var navDrawer: View
|
private lateinit var navDrawer: View
|
||||||
private lateinit var dummyView : View
|
private lateinit var dummyView : View
|
||||||
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
||||||
|
@ -529,19 +523,20 @@ class MainActivity : CastEnabledActivity() {
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
RatingDialog.init(this)
|
RatingDialog.init(this)
|
||||||
|
|
||||||
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
// val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||||
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
// controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||||
controllerFuture.addListener({
|
// controllerFuture.addListener({
|
||||||
// Call controllerFuture.get() to retrieve the MediaController.
|
// // Call controllerFuture.get() to retrieve the MediaController.
|
||||||
// MediaController implements the Player interface, so it can be
|
// // MediaController implements the Player interface, so it can be
|
||||||
// attached to the PlayerView UI component.
|
// // attached to the PlayerView UI component.
|
||||||
// playerView.setPlayer(controllerFuture.get())
|
//// playerView.setPlayer(controllerFuture.get())
|
||||||
}, MoreExecutors.directExecutor())
|
// val player = controllerFuture.get()
|
||||||
|
// }, MoreExecutors.directExecutor())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
MediaController.releaseFuture(controllerFuture)
|
// MediaController.releaseFuture(controllerFuture)
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,15 +3,15 @@ package ac.mdiq.podcini.ui.activity
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.AudioControlsBinding
|
import ac.mdiq.podcini.databinding.AudioControlsBinding
|
||||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
@ -21,8 +21,6 @@ import ac.mdiq.podcini.ui.dialog.ShareDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||||
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment
|
|
||||||
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion
|
|
||||||
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
|
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
|
||||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||||
|
@ -41,7 +39,6 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
@ -243,8 +240,8 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
|
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
|
menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
|
||||||
menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
|
menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
|
||||||
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
|
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
|
||||||
|
|
||||||
menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2)
|
menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2)
|
||||||
|
@ -382,7 +379,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
//Go to x% of video:
|
//Go to x% of video:
|
||||||
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
|
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
|
||||||
seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * duration).toInt())
|
seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * curDurationFB).toInt())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
|
@ -393,11 +390,11 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
private var _binding: AudioControlsBinding? = null
|
private var _binding: AudioControlsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var controller: PlaybackController? = null
|
private var controller: ServiceStatusHandler? = null
|
||||||
|
|
||||||
@UnstableApi override fun onStart() {
|
@UnstableApi override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
controller = object : PlaybackController(requireActivity()) {
|
controller = object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
setupAudioTracks()
|
setupAudioTracks()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,10 +2,10 @@ package ac.mdiq.podcini.ui.dialog
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.TimeDialogBinding
|
import ac.mdiq.podcini.databinding.TimeDialogBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
|
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
|
||||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
|
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
|
||||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
|
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
|
||||||
|
@ -51,7 +51,6 @@ import kotlinx.coroutines.launch
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
|
|
||||||
class SleepTimerDialog : DialogFragment() {
|
class SleepTimerDialog : DialogFragment() {
|
||||||
private var _binding: TimeDialogBinding? = null
|
private var _binding: TimeDialogBinding? = null
|
||||||
|
@ -146,7 +145,7 @@ class SleepTimerDialog : DialogFragment() {
|
||||||
val time = if (binding.endEpisode.isChecked) {
|
val time = if (binding.endEpisode.isChecked) {
|
||||||
val curPosition = curMedia?.getPosition() ?: 0
|
val curPosition = curMedia?.getPosition() ?: 0
|
||||||
val duration = curMedia?.getDuration() ?: 0
|
val duration = curMedia?.getDuration() ?: 0
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes
|
TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes
|
||||||
} else etxtTime.getText().toString().toLong()
|
} else etxtTime.getText().toString().toLong()
|
||||||
Logd(TAG, "Sleep timer set: $time")
|
Logd(TAG, "Sleep timer set: $time")
|
||||||
|
|
|
@ -2,12 +2,12 @@ package ac.mdiq.podcini.ui.dialog
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
|
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
|
@ -76,7 +76,7 @@ import java.util.*
|
||||||
if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
|
if (!settingCode[2]) binding.global.visibility = View.INVISIBLE
|
||||||
|
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedMultiplier))
|
updateSpeed(FlowEvent.SpeedChangedEvent(curSpeedFB))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onStop() {
|
@UnstableApi override fun onStop() {
|
||||||
|
|
|
@ -3,27 +3,29 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
|
||||||
import ac.mdiq.podcini.storage.model.Chapter
|
import ac.mdiq.podcini.storage.model.Chapter
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
|
@ -47,6 +49,7 @@ import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -63,12 +66,16 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.session.MediaController
|
||||||
|
import androidx.media3.session.SessionToken
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ErrorResult
|
import coil.request.ErrorResult
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import com.google.android.material.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
@ -100,7 +107,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
private lateinit var cardViewSeek: CardView
|
private lateinit var cardViewSeek: CardView
|
||||||
|
|
||||||
private var controller: PlaybackController? = null
|
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||||
|
private var controller: ServiceStatusHandler? = null
|
||||||
private var seekedToChapterStart = false
|
private var seekedToChapterStart = false
|
||||||
// private var currentChapterIndex = -1
|
// private var currentChapterIndex = -1
|
||||||
|
|
||||||
|
@ -128,7 +136,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
controller = createController()
|
controller = createHandler()
|
||||||
controller!!.init()
|
controller!!.init()
|
||||||
|
|
||||||
playerUI1 = PlayerUIFragment.newInstance(controller!!)
|
playerUI1 = PlayerUIFragment.newInstance(controller!!)
|
||||||
|
@ -152,7 +160,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
fun initDetailedView() {
|
private fun initDetailedView() {
|
||||||
if (playerDetailsFragment == null) {
|
if (playerDetailsFragment == null) {
|
||||||
val fm = requireActivity().supportFragmentManager
|
val fm = requireActivity().supportFragmentManager
|
||||||
val transaction = fm.beginTransaction()
|
val transaction = fm.beginTransaction()
|
||||||
|
@ -198,7 +206,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
val dividerPos = FloatArray(chapters.size)
|
val dividerPos = FloatArray(chapters.size)
|
||||||
|
|
||||||
for (i in chapters.indices) {
|
for (i in chapters.indices) {
|
||||||
dividerPos[i] = chapters[i].start / duration.toFloat()
|
dividerPos[i] = chapters[i].start / curDurationFB.toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -246,9 +254,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createController(): PlaybackController {
|
private fun createHandler(): ServiceStatusHandler {
|
||||||
return object : PlaybackController(requireActivity()) {
|
return object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
override fun updatePlayButton(showPlay: Boolean) {
|
||||||
isShowPlay = showPlay
|
isShowPlay = showPlay
|
||||||
playerUI?.butPlay?.setIsShowPlay(showPlay)
|
playerUI?.butPlay?.setIsShowPlay(showPlay)
|
||||||
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
|
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
|
||||||
|
@ -287,12 +295,21 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
Logd(TAG, "onStart() isCollapsed: $isCollapsed")
|
Logd(TAG, "onStart() isCollapsed: $isCollapsed")
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
|
|
||||||
|
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
|
||||||
|
controllerFuture = MediaController.Builder(requireContext(), sessionToken).buildAsync()
|
||||||
|
controllerFuture.addListener({
|
||||||
|
// mediaController = controllerFuture.get()
|
||||||
|
// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
||||||
|
}, MoreExecutors.directExecutor())
|
||||||
|
|
||||||
loadMediaInfo(false)
|
loadMediaInfo(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
Logd(TAG, "onStop()")
|
Logd(TAG, "onStop()")
|
||||||
super.onStop()
|
super.onStop()
|
||||||
|
MediaController.releaseFuture(controllerFuture)
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
||||||
}
|
}
|
||||||
|
@ -377,8 +394,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
when {
|
when {
|
||||||
fromUser -> {
|
fromUser -> {
|
||||||
val prog: Float = progress / (seekBar.max.toFloat())
|
val prog: Float = progress / (seekBar.max.toFloat())
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val position: Int = converter.convert((prog * duration).toInt())
|
val position: Int = converter.convert((prog * curDurationFB).toInt())
|
||||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position)
|
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position)
|
||||||
if (newChapterIndex > -1) {
|
if (newChapterIndex > -1) {
|
||||||
// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
|
// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
|
||||||
|
@ -393,7 +410,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}")
|
binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}")
|
||||||
} else binding.txtvSeek.text = DurationConverter.getDurationStringLong(position)
|
} else binding.txtvSeek.text = DurationConverter.getDurationStringLong(position)
|
||||||
}
|
}
|
||||||
duration != playbackService?.curDuration -> updateUi()
|
curDurationFB != playbackService?.curDuration -> updateUi()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,7 +431,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
seekedToChapterStart = false
|
seekedToChapterStart = false
|
||||||
} else {
|
} else {
|
||||||
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
||||||
seekTo((prog * duration).toInt())
|
seekTo((prog * curDurationFB).toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cardViewSeek.scaleX = 1f
|
cardViewSeek.scaleX = 1f
|
||||||
|
@ -438,8 +455,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
|
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
|
||||||
|
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
|
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!isSleepTimerActive())
|
||||||
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
|
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(isSleepTimerActive())
|
||||||
}
|
}
|
||||||
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
|
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
|
||||||
}
|
}
|
||||||
|
@ -457,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
playerDetailsFragment?.buildHomeReaderText()
|
playerDetailsFragment?.buildHomeReaderText()
|
||||||
}
|
}
|
||||||
R.id.show_video -> {
|
R.id.show_video -> {
|
||||||
controller!!.playPause()
|
playPause()
|
||||||
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
||||||
}
|
}
|
||||||
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
|
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
|
||||||
|
@ -535,10 +552,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
val mediaType = media.getMediaType()
|
val mediaType = media.getMediaType()
|
||||||
if (mediaType == MediaType.AUDIO ||
|
if (mediaType == MediaType.AUDIO ||
|
||||||
(mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) {
|
(mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) {
|
||||||
controller!!.ensureService()
|
ensureService()
|
||||||
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
|
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
|
||||||
} else {
|
} else {
|
||||||
controller?.playPause()
|
playPause()
|
||||||
// controller!!.ensureService()
|
// controller!!.ensureService()
|
||||||
val intent = getPlayerActivityIntent(requireContext(), mediaType)
|
val intent = getPlayerActivityIntent(requireContext(), mediaType)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
|
@ -557,12 +574,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
butPlay?.setOnClickListener {
|
butPlay?.setOnClickListener {
|
||||||
if (controller == null) return@setOnClickListener
|
if (controller == null) return@setOnClickListener
|
||||||
// val media = curMedia
|
|
||||||
if (curMedia != null) {
|
if (curMedia != null) {
|
||||||
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
||||||
controller!!.playPause()
|
playPause()
|
||||||
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
|
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
|
||||||
} else controller!!.playPause()
|
} else playPause()
|
||||||
if (!isControlButtonsSet) {
|
if (!isControlButtonsSet) {
|
||||||
sbPosition.visibility = View.VISIBLE
|
sbPosition.visibility = View.VISIBLE
|
||||||
isControlButtonsSet = true
|
isControlButtonsSet = true
|
||||||
|
@ -584,7 +600,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
butPlay?.setOnLongClickListener {
|
butPlay?.setOnLongClickListener {
|
||||||
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
|
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
|
||||||
val fallbackSpeed = UserPreferences.fallbackSpeed
|
val fallbackSpeed = UserPreferences.fallbackSpeed
|
||||||
if (fallbackSpeed > 0.1f) fallbackSpeed(fallbackSpeed)
|
if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -609,6 +625,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private fun ensureService() {
|
||||||
|
if (curMedia == null) return
|
||||||
|
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
|
||||||
|
}
|
||||||
private fun speedForward(speed: Float) {
|
private fun speedForward(speed: Float) {
|
||||||
// playbackService?.speedForward(speed)
|
// playbackService?.speedForward(speed)
|
||||||
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
|
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
|
||||||
|
@ -624,7 +644,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
if (controller == null) return@OnClickListener
|
if (controller == null) return@OnClickListener
|
||||||
showTimeLeft = !showTimeLeft
|
showTimeLeft = !showTimeLeft
|
||||||
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
||||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, duration))
|
onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPositionFB, curDurationFB))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
|
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
|
||||||
|
@ -634,8 +654,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) return
|
if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPositionFB == Playable.INVALID_TIME || curDurationFB == Playable.INVALID_TIME) return
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val currentPosition: Int = converter.convert(event.position)
|
val currentPosition: Int = converter.convert(event.position)
|
||||||
val duration: Int = converter.convert(event.duration)
|
val duration: Int = converter.convert(event.duration)
|
||||||
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
|
val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt())
|
||||||
|
@ -685,14 +705,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
Logd(TAG, "onPause() called")
|
Logd(TAG, "onPause() called")
|
||||||
super.onPause()
|
super.onPause()
|
||||||
controller?.pause()
|
// controller?.pause()
|
||||||
}
|
}
|
||||||
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||||
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
|
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
if (playbackService?.isServiceReady() == true) {
|
if (playbackService?.isServiceReady() == true) {
|
||||||
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
||||||
seekTo((prog * duration).toInt())
|
seekTo((prog * curDurationFB).toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
|
@ -736,8 +756,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var controller: PlaybackController? = null
|
var controller: ServiceStatusHandler? = null
|
||||||
fun newInstance(controller_: PlaybackController) : PlayerUIFragment {
|
fun newInstance(controller_: ServiceStatusHandler) : PlayerUIFragment {
|
||||||
controller = controller_
|
controller = controller_
|
||||||
return PlayerUIFragment()
|
return PlayerUIFragment()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.SimplechapterItemBinding
|
import ac.mdiq.podcini.databinding.SimplechapterItemBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
@ -11,8 +11,9 @@ import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.getCurrentChapterIndex
|
import ac.mdiq.podcini.storage.utils.ChapterUtils.getCurrentChapterIndex
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
|
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
import ac.mdiq.podcini.storage.model.Chapter
|
import ac.mdiq.podcini.storage.model.Chapter
|
||||||
import ac.mdiq.podcini.storage.model.EmbeddedChapterImage
|
import ac.mdiq.podcini.storage.model.EmbeddedChapterImage
|
||||||
import ac.mdiq.podcini.ui.view.CircularProgressBar
|
import ac.mdiq.podcini.ui.view.CircularProgressBar
|
||||||
|
@ -63,7 +64,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
private lateinit var progressBar: ProgressBar
|
private lateinit var progressBar: ProgressBar
|
||||||
private lateinit var adapter: ChaptersListAdapter
|
private lateinit var adapter: ChaptersListAdapter
|
||||||
|
|
||||||
private var controller: PlaybackController? = null
|
private var controller: ServiceStatusHandler? = null
|
||||||
private var focusedChapter = -1
|
private var focusedChapter = -1
|
||||||
private var media: Playable? = null
|
private var media: Playable? = null
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
|
|
||||||
adapter = ChaptersListAdapter(requireContext(), object : ChaptersListAdapter.Callback {
|
adapter = ChaptersListAdapter(requireContext(), object : ChaptersListAdapter.Callback {
|
||||||
override fun onPlayChapterButtonClicked(pos: Int) {
|
override fun onPlayChapterButtonClicked(pos: Int) {
|
||||||
if (MediaPlayerBase.status != PlayerStatus.PLAYING) controller!!.playPause()
|
if (MediaPlayerBase.status != PlayerStatus.PLAYING) playPause()
|
||||||
|
|
||||||
val chapter = adapter.getItem(pos)
|
val chapter = adapter.getItem(pos)
|
||||||
if (chapter != null) seekTo(chapter.start.toInt())
|
if (chapter != null) seekTo(chapter.start.toInt())
|
||||||
|
@ -111,7 +112,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
val wrapHeight = CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT)
|
val wrapHeight = CoordinatorLayout.LayoutParams(CoordinatorLayout.LayoutParams.MATCH_PARENT, CoordinatorLayout.LayoutParams.WRAP_CONTENT)
|
||||||
recyclerView.layoutParams = wrapHeight
|
recyclerView.layoutParams = wrapHeight
|
||||||
|
|
||||||
controller = object : PlaybackController(requireActivity()) {
|
controller = object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
this@ChaptersFragment.loadMediaInfo(false)
|
this@ChaptersFragment.loadMediaInfo(false)
|
||||||
}
|
}
|
||||||
|
@ -167,7 +168,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
private fun getCurrentChapter(media: Playable?): Int {
|
private fun getCurrentChapter(media: Playable?): Int {
|
||||||
if (controller == null) return -1
|
if (controller == null) return -1
|
||||||
|
|
||||||
return getCurrentChapterIndex(media, curPosition)
|
return getCurrentChapterIndex(media, curPositionFB)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMediaInfo(forceRefresh: Boolean) {
|
private fun loadMediaInfo(forceRefresh: Boolean) {
|
||||||
|
|
|
@ -7,8 +7,8 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||||
|
|
|
@ -3,10 +3,10 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.*
|
import ac.mdiq.podcini.storage.model.*
|
||||||
|
@ -93,7 +93,7 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
|
|
||||||
Logd(TAG, "fragment onCreateView")
|
Logd(TAG, "fragment onCreateView")
|
||||||
shownoteView = binding.webview
|
shownoteView = binding.webview
|
||||||
shownoteView.setTimecodeSelectedListener { time: Int? -> seekTo(time!!) }
|
shownoteView.setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
||||||
shownoteView.setPageFinishedListener {
|
shownoteView.setPageFinishedListener {
|
||||||
// Restoring the scroll position might not always work
|
// Restoring the scroll position might not always work
|
||||||
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
||||||
|
@ -347,7 +347,7 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
|
|
||||||
when {
|
when {
|
||||||
displayedChapterIndex < 1 -> seekTo(0)
|
displayedChapterIndex < 1 -> seekTo(0)
|
||||||
(curPosition - 10000 * curSpeedMultiplier) < curr.start -> {
|
(curPositionFB - 10000 * curSpeedFB) < curr.start -> {
|
||||||
refreshChapterData(displayedChapterIndex - 1)
|
refreshChapterData(displayedChapterIndex - 1)
|
||||||
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||||
import ac.mdiq.podcini.databinding.QueueFragmentBinding
|
import ac.mdiq.podcini.databinding.QueueFragmentBinding
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.mediaBrowser
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.Queues.clearQueue
|
import ac.mdiq.podcini.storage.database.Queues.clearQueue
|
||||||
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
|
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
|
||||||
|
@ -38,6 +41,8 @@ import ac.mdiq.podcini.ui.view.EpisodesRecyclerView
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
@ -64,12 +69,16 @@ import androidx.compose.ui.window.Dialog
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.media3.session.MediaBrowser
|
||||||
|
import androidx.media3.session.SessionToken
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors
|
||||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||||
import com.leinardi.android.speeddial.SpeedDialView
|
import com.leinardi.android.speeddial.SpeedDialView
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -104,6 +113,8 @@ import java.util.*
|
||||||
private var showBin: Boolean = false
|
private var showBin: Boolean = false
|
||||||
private var addToQueueActionItem: SpeedDialActionItem? = null
|
private var addToQueueActionItem: SpeedDialActionItem? = null
|
||||||
|
|
||||||
|
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
retainInstance = true
|
retainInstance = true
|
||||||
|
@ -138,9 +149,11 @@ import java.util.*
|
||||||
queueSpinner.setSelection(queueNames.indexOf(curQueue.name))
|
queueSpinner.setSelection(queueNames.indexOf(curQueue.name))
|
||||||
queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||||
|
val prevQueueSize = curQueue.size()
|
||||||
curQueue = upsertBlk(queues[position]) { it.update() }
|
curQueue = upsertBlk(queues[position]) { it.update() }
|
||||||
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
|
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
|
||||||
loadCurQueue(true)
|
loadCurQueue(true)
|
||||||
|
playbackService?.notifyCurQueueItemsChanged(Math.max(prevQueueSize, curQueue.size()))
|
||||||
}
|
}
|
||||||
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
override fun onNothingSelected(parent: AdapterView<*>?) {}
|
||||||
}
|
}
|
||||||
|
@ -216,6 +229,16 @@ import java.util.*
|
||||||
adapter?.refreshFragPosCallback = ::refreshPosCallback
|
adapter?.refreshFragPosCallback = ::refreshPosCallback
|
||||||
loadCurQueue(true)
|
loadCurQueue(true)
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
|
val sessionToken = SessionToken(requireContext(), ComponentName(requireContext(), PlaybackService::class.java))
|
||||||
|
browserFuture = MediaBrowser.Builder(requireContext(), sessionToken).buildAsync()
|
||||||
|
browserFuture.addListener(
|
||||||
|
{
|
||||||
|
// here we can get the root of media items tree or we can get also the children if it is an album for example.
|
||||||
|
mediaBrowser = browserFuture.get()
|
||||||
|
mediaBrowser?.subscribe("CurQueue", null)
|
||||||
|
},
|
||||||
|
MoreExecutors.directExecutor()
|
||||||
|
)
|
||||||
// if (queueItems.isNotEmpty()) recyclerView.restoreScrollPosition(TAG)
|
// if (queueItems.isNotEmpty()) recyclerView.restoreScrollPosition(TAG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,6 +247,9 @@ import java.util.*
|
||||||
super.onStop()
|
super.onStop()
|
||||||
adapter?.refreshFragPosCallback = null
|
adapter?.refreshFragPosCallback = null
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
|
mediaBrowser?.unsubscribe("CurQueue")
|
||||||
|
mediaBrowser = null
|
||||||
|
MediaBrowser.releaseFuture(browserFuture)
|
||||||
val childCount = recyclerView.childCount
|
val childCount = recyclerView.childCount
|
||||||
for (i in 0 until childCount) {
|
for (i in 0 until childCount) {
|
||||||
val child = recyclerView.getChildAt(i)
|
val child = recyclerView.getChildAt(i)
|
||||||
|
@ -327,7 +353,10 @@ import java.util.*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FlowEvent.QueueEvent.Action.SWITCH_QUEUE -> loadCurQueue(false)
|
FlowEvent.QueueEvent.Action.SWITCH_QUEUE -> {
|
||||||
|
loadCurQueue(false)
|
||||||
|
playbackService?.notifyCurQueueItemsChanged(event.episodes.size)
|
||||||
|
}
|
||||||
FlowEvent.QueueEvent.Action.CLEARED -> {
|
FlowEvent.QueueEvent.Action.CLEARED -> {
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
adapter?.updateItems(queueItems)
|
adapter?.updateItems(queueItems)
|
||||||
|
@ -385,6 +414,7 @@ import java.util.*
|
||||||
if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
|
if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("RestrictedApi")
|
||||||
private fun onKeyUp(event: KeyEvent) {
|
private fun onKeyUp(event: KeyEvent) {
|
||||||
if (!isAdded || !isVisible || !isMenuVisible) return
|
if (!isAdded || !isVisible || !isMenuVisible) return
|
||||||
|
|
||||||
|
@ -650,10 +680,9 @@ import java.util.*
|
||||||
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
|
||||||
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
|
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
if (showBin) {
|
if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
|
||||||
queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
|
|
||||||
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
|
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) })
|
||||||
} else {
|
else {
|
||||||
curQueue.episodes.clear()
|
curQueue.episodes.clear()
|
||||||
queueItems.addAll(curQueue.episodes)
|
queueItems.addAll(curQueue.episodes)
|
||||||
}
|
}
|
||||||
|
@ -664,6 +693,7 @@ import java.util.*
|
||||||
adapter?.updateItems(queueItems)
|
adapter?.updateItems(queueItems)
|
||||||
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||||
refreshInfoBar()
|
refreshInfoBar()
|
||||||
|
// playbackService?.notifyCurQueueItemsChanged()
|
||||||
loadItemsRunning = false
|
loadItemsRunning = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,17 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||||
|
@ -76,7 +77,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
private lateinit var webvDescription: ShownotesWebView
|
private lateinit var webvDescription: ShownotesWebView
|
||||||
|
|
||||||
var destroyingDueToReload = false
|
var destroyingDueToReload = false
|
||||||
var controller: PlaybackController? = null
|
var controller: ServiceStatusHandler? = null
|
||||||
var isFavorite = false
|
var isFavorite = false
|
||||||
|
|
||||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||||
|
@ -116,9 +117,9 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun newPlaybackController(): PlaybackController {
|
@OptIn(UnstableApi::class) private fun newPlaybackController(): ServiceStatusHandler {
|
||||||
return object : PlaybackController(requireActivity()) {
|
return object : ServiceStatusHandler(requireActivity()) {
|
||||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
override fun updatePlayButton(showPlay: Boolean) {
|
||||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||||
binding.playButton.setIsShowPlay(showPlay)
|
binding.playButton.setIsShowPlay(showPlay)
|
||||||
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
@ -159,9 +160,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
|
// this does nothing
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
|
// if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
|
||||||
}
|
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
|
||||||
|
// }
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +288,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
binding.durationLabel.setOnClickListener {
|
binding.durationLabel.setOnClickListener {
|
||||||
showTimeLeft = !showTimeLeft
|
showTimeLeft = !showTimeLeft
|
||||||
val media = curMedia ?: return@setOnClickListener
|
val media = curMedia ?: return@setOnClickListener
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val length: String
|
val length: String
|
||||||
if (showTimeLeft) {
|
if (showTimeLeft) {
|
||||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||||
|
@ -423,8 +425,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onPlayPause() {
|
fun onPlayPause() {
|
||||||
if (controller == null) return
|
playPause()
|
||||||
controller!!.playPause()
|
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -482,10 +483,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
private fun onPositionObserverUpdate() {
|
private fun onPositionObserverUpdate() {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val currentPosition = converter.convert(curPosition)
|
val currentPosition = converter.convert(curPositionFB)
|
||||||
val duration_ = converter.convert(duration)
|
val duration_ = converter.convert(curDurationFB)
|
||||||
val remainingTime = converter.convert(duration - curPosition)
|
val remainingTime = converter.convert(curDurationFB - curPositionFB)
|
||||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
||||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||||
|
@ -508,8 +509,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
prog = progress / (seekBar.max.toFloat())
|
prog = progress / (seekBar.max.toFloat())
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
val position = converter.convert((prog * duration).toInt())
|
val position = converter.convert((prog * curDurationFB).toInt())
|
||||||
binding.seekPositionLabel.text = getDurationStringLong(position)
|
binding.seekPositionLabel.text = getDurationStringLong(position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -526,7 +527,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
seekTo((prog * duration).toInt())
|
seekTo((prog * curDurationFB).toInt())
|
||||||
binding.seekCardView.scaleX = 1f
|
binding.seekCardView.scaleX = 1f
|
||||||
binding.seekCardView.scaleY = 1f
|
binding.seekCardView.scaleY = 1f
|
||||||
binding.seekCardView.animate()
|
binding.seekCardView.animate()
|
||||||
|
|
|
@ -4,7 +4,7 @@ buildscript {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
ext.kotlin_version = '2.0.10'
|
ext.kotlin_version = "$libs.versions.kotlin"
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
classpath 'com.android.tools.build:gradle:8.5.2'
|
classpath 'com.android.tools.build:gradle:8.5.2'
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
# 6.4.0
|
||||||
|
|
||||||
|
* PlaybackService is now a MediaLibraryService and also takes over many functionalities from PlaybackController
|
||||||
|
* PlaybackController is renamed to ServiceStatusHandler and is stripped to bare bones
|
||||||
|
* enabled Android Auto UI, currently playing episode and episodes in the active queue are shown in the Auto UI
|
||||||
|
* added code to handle case where keycode=-1 and keyEvent!=null, attempting to resolve the occassional issue of random start playing
|
||||||
|
|
||||||
# 6.3.7
|
# 6.3.7
|
||||||
|
|
||||||
* inlined some DB writes of Episodes in some routines
|
* inlined some DB writes of Episodes in some routines
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Version 6.3.7 brings several changes:
|
Version 6.4.0 brings several changes:
|
||||||
|
|
||||||
* inlined some DB writes of Episodes in some routines
|
* PlaybackService is now a MediaLibraryService and also takes over many functionalities from PlaybackController
|
||||||
* enhanced DB writes in download routine, fixed a write error
|
* PlaybackController is renamed to ServiceStatusHandler and is stripped to bare bones
|
||||||
* added a couple more Log.d statements in hope for tracking down the mysterious random playing
|
* enabled Android Auto UI, currently playing episode and episodes in the active queue are shown in the Auto UI
|
||||||
* Kotlin upped to 2.0.10
|
* added code to handle case where keycode=-1 and keyEvent!=null, attempting to resolve the occassional issue of random start playing
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Version 6.3.7 brings several changes:
|
||||||
|
|
||||||
|
* inlined some DB writes of Episodes in some routines
|
||||||
|
* enhanced DB writes in download routine, fixed a write error
|
||||||
|
* added a couple more Log.d statements in hope for tracking down the mysterious random playing
|
||||||
|
* Kotlin upped to 2.0.10
|
|
@ -1,6 +1,6 @@
|
||||||
|
|
||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.10"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
|
|
Loading…
Reference in New Issue