6.2.2 commit

This commit is contained in:
Xilin Jia 2024-07-29 23:11:20 +01:00
parent 13870cedd2
commit 5a40c6ac23
49 changed files with 321 additions and 211 deletions

View File

@ -34,7 +34,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
## Notable new features & enhancements
### Player
### Player and Queues
* More convenient player control displayed on all pages
* Revamped and more efficient expanded player view showing episode description on the front
@ -59,6 +59,14 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* easy switches on video player to other video mode or audio only
* default video player mode setting in preferences
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue
* any episodes can be easily added/moved to the active or any designated queues
* any queue can be associated with any feed for customized playing experience
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue
* Episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
### Podcast/Episode list
@ -82,11 +90,6 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* on action bar of FeedEpisodes view there is a direct access to Queue
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
* History view shows time of last play, and allows filters and sorts
* Multiple queues can be used: 5 queues are provided by default, user can add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue
### Podcast/Episode
* New share notes menu option on various episode views
@ -121,11 +124,12 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Each feed also has its own download policy (only new episodes, newest episodes, and oldest episodes. "newest episodes" meaning most recent episodes, new or old)
* Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app.
* Auto downloads run feeds or feed refreshes, scheduled or manual
* auto download always includes any undownloaded episodes (regardless of feeds) added in the current queue
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
* After auto download run, episodes with New status is changed to Unplayed.
* auto download feed setting dialog is also changed:
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently
* on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played"
* Sleep timer has a new option of "To the end of episode"
### Security and reliability

View File

@ -25,17 +25,14 @@ android {
kotlinOptions {
jvmTarget = '17'
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020222
versionName "6.2.1"
versionCode 3020223
versionName "6.2.2"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -44,7 +44,7 @@ object DownloadRequestCreator {
Logd(TAG, "Requesting download media from url " + media.downloadUrl)
val feed = media.episode?.feed
val feed = media.episodeOrFetch()?.feed
val username = feed?.preferences?.username
val password = feed?.preferences?.password

View File

@ -75,7 +75,8 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
@OptIn(UnstableApi::class) override fun cancel(context: Context, media: EpisodeMedia) {
Logd(TAG, "starting cancel")
// This needs to be done here, not in the worker. Reason: The worker might or might not be running.
if (media.episode != null) Episodes.deleteMediaOfEpisode(context, media.episode!!) // Remove partially downloaded file
val item_ = media.episodeOrFetch()
if (item_ != null) Episodes.deleteMediaOfEpisode(context, item_) // Remove partially downloaded file
val tag = WORK_TAG_EPISODE_URL + media.downloadUrl
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
@ -83,8 +84,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
try {
val workInfoList = future.get() // Wait for the completion of the future operation and retrieve the result
workInfoList.forEach { workInfo ->
// TODO: why cancel so many times??
if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) {
if (media.episode != null) Queues.removeFromQueue(media.episode!!)
val item_ = media.episodeOrFetch()
if (item_ != null) Queues.removeFromQueue(item_)
}
}
WorkManager.getInstance(context).cancelAllWorkByTag(tag)
@ -202,6 +205,10 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
@OptIn(UnstableApi::class)
private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result {
Logd(TAG, "starting performDownload")
if (request.destination == null) {
Log.e(TAG, "performDownload request.destination is null")
return Result.failure()
}
val dest = File(request.destination)
if (!dest.exists()) {
try {
@ -338,17 +345,19 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
return
}
// media.setDownloaded modifies played state
val broadcastUnreadStateUpdate = media.episode != null && media.episode!!.isNew
var item_ = media.episodeOrFetch()
val broadcastUnreadStateUpdate = item_?.isNew == true
// media.downloaded = true
media.setIsDownloaded()
Logd(TAG, "media.episode.isNew: ${media.episode?.isNew} ${media.episode?.playState}")
item_ = media.episodeOrFetch()
Logd(TAG, "media.episode.isNew: ${item_?.isNew} ${item_?.playState}")
media.setfileUrlOrNull(request.destination)
if (request.destination != null) media.size = File(request.destination).length()
media.checkEmbeddedPicture() // enforce check
// check if file has chapters
if (media.episode != null && media.episode!!.chapters.isEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (media.episode?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(media.episode!!.podcastIndexChapterUrl!!, false)
if (item_?.chapters.isNullOrEmpty()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
if (item_?.podcastIndexChapterUrl != null)
ChapterUtils.loadChaptersFromUrl(item_.podcastIndexChapterUrl!!, false)
// Get duration
var durationStr: String? = null
try {
@ -364,7 +373,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
Log.e(TAG, "Get duration failed", e)
media.setDuration(30000)
}
val item = media.episode
val item = media.episodeOrFetch()
item?.media = media
try {
// we've received the media, we don't want to autodownload it again

View File

@ -62,9 +62,10 @@ object SynchronizationQueueSink {
fun enqueueEpisodePlayedIfSyncActive(context: Context, media: EpisodeMedia, completed: Boolean) {
if (!isProviderConnected) return
if (media.episode?.feed == null || media.episode!!.feed!!.isLocalFeed) return
val item_ = media.episodeOrFetch()
if (item_?.feed?.isLocalFeed == true) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
val action = EpisodeAction.Builder(media.episode!!, EpisodeAction.PLAY)
val action = EpisodeAction.Builder(item_!!, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.startPosition / 1000)
.position((if (completed) media.getDuration() else media.getPosition()) / 1000)

View File

@ -48,7 +48,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
if (media is EpisodeMedia) {
curMedia = media
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
curEpisode = media.episode
curEpisode = media.episodeOrFetch()
// curMedia = curEpisode?.media
} else curMedia = media

View File

@ -19,20 +19,23 @@ import kotlinx.coroutines.*
object InTheatre {
val TAG: String = InTheatre::class.simpleName ?: "Anonymous"
var curIndexInQueue = -1
var curQueue: PlayQueue // unmanaged
var curEpisode: Episode? = null // unmanged
set(value) {
field = value
if (curMedia != field?.media) curMedia = field?.media
field = if (value != null) unmanaged(value) else null
if (field?.media != null && curMedia?.getIdentifier() != field?.media?.getIdentifier()) curMedia = unmanaged(field!!.media!!)
}
var curMedia: Playable? = null // unmanged if EpisodeMedia
set(value) {
field = if (value != null && value is EpisodeMedia) unmanaged(value) else value
if (field is EpisodeMedia) {
val media = (field as EpisodeMedia)
if (curEpisode != media.episode) curEpisode = media.episode
if (value is EpisodeMedia) {
field = unmanaged(value)
if (value.episode != null && curEpisode?.id != value.episode?.id) curEpisode = unmanaged(value.episode!!)
} else {
field = value
}
}
@ -115,7 +118,7 @@ object InTheatre {
val mediaId = curState.curMediaId
if (mediaId != 0L) {
curMedia = getEpisodeMedia(mediaId)
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episode
if (curEpisode != null) curEpisode = (curMedia as EpisodeMedia).episodeOrFetch()
}
} else Log.e(TAG, "Could not restore Playable object from preferences")
}

View File

@ -346,7 +346,8 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
if (media != null) {
playbackSpeed = curState.curTempSpeed
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
val prefs_ = media.episodeOrFetch()?.feed?.preferences
if (prefs_ != null) playbackSpeed = prefs_.playSpeed
}
}
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = getPlaybackSpeed(mediaType)

View File

@ -6,7 +6,9 @@ import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
import ac.mdiq.podcini.net.utils.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
@ -15,11 +17,13 @@ import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.config.ClientConfig
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import ac.mdiq.podcini.util.event.FlowEvent.PlayEvent.Action
import ac.mdiq.podcini.util.showStackTrace
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
@ -225,11 +229,18 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
*/
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
Logd(TAG, "playMediaObject status=$status stream=$stream startWhenPrepared=$startWhenPrepared prepareImmediately=$prepareImmediately forceReset=$forceReset ${playable.getEpisodeTitle()} ")
// showStackTrace()
if (curMedia != null) {
Logd(TAG, "playMediaObject: curMedia exist status=$status")
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
return
}
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
curIndexInQueue = EpisodeUtil.indexOfItemWithId(curQueue.episodes, media_.id)
} else curIndexInQueue = -1
Logd(TAG, "playMediaObject starts new media playable:${playable.getIdentifier()} curMedia:${curMedia!!.getIdentifier()} prevMedia:${prevMedia?.getIdentifier()}")
// set temporarily to pause in order to update list with current position
if (status == PlayerStatus.PLAYING) {
@ -241,12 +252,12 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARED) exoPlayer?.stop()
// if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier())
// callback.onPostPlayback(prevMedia, ended = false, skipped = false, true)
prevMedia = curMedia
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
}
Logd(TAG, "playMediaObject preparing for playable:${playable.getIdentifier()} ${playable.getEpisodeTitle()}")
curMedia = playable
prevMedia = curMedia
this.isStreaming = stream
mediaType = curMedia!!.getMediaType()
videoSize = null
@ -264,7 +275,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (streamurl != null) {
val media = curMedia
if (media is EpisodeMedia) {
val preferences = media.episode?.feed?.preferences
val preferences = media.episodeOrFetch()?.feed?.preferences
setDataSource(metadata, streamurl, preferences?.username, preferences?.password)
} else setDataSource(metadata, streamurl, null, null)
}
@ -289,6 +300,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
e.printStackTrace()
setPlayerStatus(PlayerStatus.ERROR, null)
EventFlow.postStickyEvent(FlowEvent.PlayerErrorEvent(e.localizedMessage ?: ""))
} finally {
}
}
@ -431,8 +443,9 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
var volumeLeft = volumeLeft
var volumeRight = volumeRight
val playable = curMedia
if (playable is EpisodeMedia && playable.episode?.feed?.preferences != null) {
val preferences = playable.episode!!.feed!!.preferences!!
if (playable is EpisodeMedia) {
val preferences = playable.episodeOrFetch()?.feed?.preferences
if (preferences != null) {
val volumeAdaptionSetting = preferences.volumeAdaptionSetting
if (volumeAdaptionSetting != null) {
val adaptionFactor = volumeAdaptionSetting.adaptionFactor
@ -440,6 +453,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
volumeRight *= adaptionFactor
}
}
}
if (volumeLeft > 1) {
exoPlayer?.volume = 1f
loudnessEnhancer?.setEnabled(true)
@ -532,6 +546,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) {
releaseWifiLockIfNecessary()
if (curMedia == null) return
val isPlaying = status == PlayerStatus.PLAYING
// we're relying on the position stored in the Playable object for post-playback processing
@ -566,7 +581,8 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
else Logd(TAG, "Ignored call to stop: Current player state is: $status")
}
val hasNext = nextMedia != null
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)
if (currentMedia != null) callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)
// curMedia = nextMedia
}
isPlaying -> callback.onPlaybackPause(currentMedia, currentMedia!!.getPosition())
}

View File

@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
import ac.mdiq.podcini.playback.base.InTheatre.curIndexInQueue
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.curState
@ -36,7 +37,6 @@ import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
import ac.mdiq.podcini.storage.database.Queues.addToQueue
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
@ -47,6 +47,7 @@ import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_OTHER
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PAUSED
import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYING
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
@ -283,7 +284,7 @@ class PlaybackService : MediaSessionService() {
// TODO: test
// return
}
var item = (playable as? EpisodeMedia)?.episode ?: currentitem
var item = (playable as? EpisodeMedia)?.episodeOrFetch() ?: currentitem
val smartMarkAsPlayed = hasAlmostEnded(playable)
if (!ended && smartMarkAsPlayed) Logd(TAG, "smart mark as played")
@ -350,12 +351,17 @@ class PlaybackService : MediaSessionService() {
override fun getNextInQueue(currentMedia: Playable?): Playable? {
Logd(TAG, "call getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}")
if (curIndexInQueue < 0) {
Logd(TAG, "getNextInQueue(), curMedia is not in curQueue")
writeNoMediaPlaying()
return null
}
if (currentMedia !is EpisodeMedia) {
Logd(TAG, "getNextInQueue(), but playable not an instance of EpisodeMedia, so not proceeding")
writeNoMediaPlaying()
return null
}
val item = currentMedia.episode
val item = currentMedia.episodeOrFetch()
if (item == null) {
Logd(TAG, "getNextInQueue() with EpisodeMedia object whose FeedItem is null")
writeNoMediaPlaying()
@ -367,13 +373,20 @@ class PlaybackService : MediaSessionService() {
writeNoMediaPlaying()
return null
}
val i = curQueue.episodes.indexOf(item)
var j = 0
val i = EpisodeUtil.indexOfItemWithId(curQueue.episodes, item.id)
if (i < 0) {
if (curIndexInQueue >= 0) {
if (curIndexInQueue < curQueue.episodes.size) j = curIndexInQueue
else j = curQueue.episodes.size-1
}
else {
Logd(TAG, "getNextInQueue curMedia is not in queue ${item.title}")
writeNoMediaPlaying()
return null
}
var j = 0
if (i < curQueue.episodes.size-1) j = i+1
} else if (i < curQueue.episodes.size-1) j = i+1
val nextItem = unmanaged(curQueue.episodes[j])
if (nextItem.media == null) {
Logd(TAG, "getNextInQueue nextItem: $nextItem media is null")
@ -388,6 +401,7 @@ class PlaybackService : MediaSessionService() {
}
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) {
Logd(TAG, "getNextInQueue nextItem has no local file ${nextItem.title}")
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this@PlaybackService, nextItem.media!!).intent)
writeNoMediaPlaying()
return null
@ -441,7 +455,7 @@ class PlaybackService : MediaSessionService() {
curState.curMediaType = playable.getPlayableType().toLong()
curState.curIsVideo = playable.getMediaType() == MediaType.VIDEO
if (playable is EpisodeMedia) {
val feedId = playable.episode?.feed?.id
val feedId = playable.episodeOrFetch()?.feed?.id
if (feedId != null) curState.curFeedId = feedId
curState.curMediaId = playable.id
} else {
@ -734,7 +748,7 @@ class PlaybackService : MediaSessionService() {
}
private fun skipIntro(playable: Playable) {
val item = (playable as? EpisodeMedia)?.episode ?: currentitem ?: return
val item = (playable as? EpisodeMedia)?.episodeOrFetch() ?: currentitem ?: return
val feed = item.feed
val preferences = feed?.preferences
@ -911,7 +925,7 @@ class PlaybackService : MediaSessionService() {
mPlayer?.playMediaObject(media, stream, startWhenPrepared = true, true)
recreateMediaSessionIfNeeded()
val episode = (media as? EpisodeMedia)?.episode
// val episode = (media as? EpisodeMedia)?.episode
// if (curMedia is EpisodeMedia && episode != null) addToQueue(true, episode)
}
@ -963,6 +977,7 @@ class PlaybackService : MediaSessionService() {
if (event.action == FlowEvent.QueueEvent.Action.REMOVED) {
for (e in event.episodes) {
if (e.id == curEpisode?.id) {
Logd(TAG, "onQueueEvent: ending playback ${curEpisode?.title}")
mPlayer?.endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true)
break
}
@ -975,7 +990,7 @@ class PlaybackService : MediaSessionService() {
// }
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
val item = (curMedia as? EpisodeMedia)?.episode ?: currentitem
val item = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: currentitem
if (item?.feed?.id == event.feed.id) {
item.feed = null
// seems no need to pause??
@ -1028,7 +1043,7 @@ class PlaybackService : MediaSessionService() {
private fun skipEndingIfNecessary() {
val remainingTime = curDuration - curPosition
val item = (curMedia as? EpisodeMedia)?.episode ?: currentitem ?: return
val item = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: currentitem ?: return
val skipEnd = item.feed?.preferences?.endingSkip?:0
val skipEndMS = skipEnd * 1000
@ -1061,7 +1076,7 @@ class PlaybackService : MediaSessionService() {
playable.setLastPlayedTime(System.currentTimeMillis())
if (playable is EpisodeMedia) {
val item = playable.episode
val item = playable.episodeOrFetch()
if (item != null && item.isNew) item.playState = Episode.UNPLAYED
if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition)
playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition)
@ -1249,8 +1264,9 @@ class PlaybackService : MediaSessionService() {
fun updateVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
val playable = curMedia
if (playable is EpisodeMedia) {
if (playable.episode?.feed?.id == feedId) {
playable.episode!!.feed!!.preferences?.volumeAdaptionSetting = volumeAdaptionSetting
val item_ = playable.episodeOrFetch()
if (item_?.feed?.id == feedId) {
item_.feed!!.preferences?.volumeAdaptionSetting = volumeAdaptionSetting
if (MediaPlayerBase.status == PlayerStatus.PLAYING) {
mediaPlayer.pause(abandonFocus = false, reinit = false)
mediaPlayer.resume()

View File

@ -56,8 +56,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba
*/
@get:Synchronized
val isSleepTimerActive: Boolean
get() = (sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture!!.isCancelled
&& !sleepTimerFuture!!.isDone) && sleepTimer!!.getWaitingTime() > 0
get() = sleepTimerFuture?.isCancelled == false && sleepTimerFuture?.isDone == false && (sleepTimer?.getWaitingTime() ?: 0) > 0
/**
* Returns the current sleep timer time or 0 if the sleep timer is not active.
@ -248,7 +247,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba
EventFlow.postEvent(FlowEvent.SleepTimerUpdatedEvent.updated(timeLeft))
while (timeLeft > 0) {
try {
Thread.sleep(UPDATE_INTERVAL)
Thread.sleep(SLEEP_TIMER_UPDATE_INTERVAL)
} catch (e: InterruptedException) {
Logd(TAG, "Thread was interrupted while waiting")
e.printStackTrace()
@ -344,16 +343,12 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba
companion object {
private val TAG: String = TaskManager::class.simpleName ?: "Anonymous"
/**
* Update interval of position saver in milliseconds.
*/
const val POSITION_SAVER_WAITING_INTERVAL: Int = 5000
/**
* Notification interval of widget updater in milliseconds.
*/
const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000
private const val SCHED_EX_POOL_SIZE = 2
private const val UPDATE_INTERVAL = 1000L
const val NOTIFICATION_THRESHOLD: Long = 10000
private const val SLEEP_TIMER_UPDATE_INTERVAL = 10000L // in millisoconds
const val POSITION_SAVER_WAITING_INTERVAL: Int = 5000 // in millisoconds
const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000 // in millisoconds
const val NOTIFICATION_THRESHOLD: Long = 10000 // in millisoconds
}
}

View File

@ -36,12 +36,12 @@ object SleepTimerPreferences {
}
@JvmStatic
fun setLastTimer(value: String?) {
fun setLastTimer(value: String?) { // in minutes
prefs!!.edit().putString(Prefs.LastValue.name, value).apply()
}
@JvmStatic
fun lastTimerValue(): String? {
fun lastTimerValue(): String? { // in minutes
return prefs!!.getString(Prefs.LastValue.name, DEFAULT_LAST_TIMER)
}

View File

@ -4,6 +4,7 @@ import ac.mdiq.podcini.storage.model.ProxyConfig
import ac.mdiq.podcini.storage.utils.FilesUtils
import ac.mdiq.podcini.storage.utils.FilesUtils.createNoMediaFile
import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.os.Build
@ -18,6 +19,7 @@ import java.net.Proxy
* init() or otherwise every public method will throw an Exception
* when called.
*/
@SuppressLint("StaticFieldLeak")
object UserPreferences {
private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous"

View File

@ -222,7 +222,7 @@ object Episodes {
fun persistEpisodeMedia(media: EpisodeMedia) : Job {
Logd(TAG, "persistEpisodeMedia called")
return runOnIOScope {
var episode = media.episode
var episode = media.episodeOrFetch()
if (episode != null) {
episode.media = media
episode = upsert(episode) {}

View File

@ -40,7 +40,7 @@ object LogsAndStats {
Logd(TAG, "getStatistics called")
val medias = realm.query(EpisodeMedia::class).find()
val groupdMedias = medias.groupBy { it.episode?.feedId ?: 0L }
val groupdMedias = medias.groupBy { it.episodeOrFetch()?.feedId ?: 0L }
val result = StatisticsResult()
result.oldestDate = Long.MAX_VALUE
for ((fid, feedMedias) in groupdMedias) {
@ -56,7 +56,7 @@ object LogsAndStats {
feedTotalTime += m.duration
if (m.lastPlayedTime in timeFilterFrom..<timeFilterTo) {
if (includeMarkedAsPlayed) {
if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || m.episode?.playState == Episode.PLAYED || m.position > 0) {
if ((m.playbackCompletionTime > 0 && m.playedDuration > 0) || m.episodeOrFetch()?.playState == Episode.PLAYED || m.position > 0) {
episodesStarted += 1
feedPlayedTime += m.duration
}

View File

@ -216,13 +216,14 @@ object Queues {
@OptIn(UnstableApi::class)
fun removeFromAllQueuesSync(vararg episodes: Episode) {
Logd(TAG, "removeFromAllQueues called ")
Logd(TAG, "removeFromAllQueuesSync called ")
val queues = realm.query(PlayQueue::class).find()
for (q in queues) {
if (q.id != curQueue.id) removeFromQueueSync(q, *episodes)
}
// ensure curQueue is last updated
removeFromQueueSync(curQueue, *episodes)
if (curQueue.size() > 0) removeFromQueueSync(curQueue, *episodes)
else upsertBlk(curQueue) { it.update() }
}
/**
@ -232,8 +233,9 @@ object Queues {
internal fun removeFromQueueSync(queue_: PlayQueue?, vararg episodes: Episode) {
Logd(TAG, "removeFromQueueSync called ")
if (episodes.isEmpty()) return
var queue = queue_ ?: curQueue
if (queue.size() == 0) return
val events: MutableList<FlowEvent.QueueEvent> = ArrayList()
val indicesToRemove: MutableList<Int> = mutableListOf()
val qItems = queue.episodes.toMutableList()
@ -269,7 +271,7 @@ object Queues {
var idsInQueuesToRemove: MutableSet<Long>
val queues = realm.query(PlayQueue::class).find()
for (q in queues) {
if (q.id == curQueue.id) continue
if (q.size() == 0 || q.id == curQueue.id) continue
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
q.idsBinList.removeAll(idsInQueuesToRemove)
@ -284,6 +286,10 @@ object Queues {
}
// ensure curQueue is last updated
val q = curQueue
if (q.size() == 0) {
upsert(q) { it.update() }
return
}
idsInQueuesToRemove = q.episodeIds.intersect(episodeIds.toSet()).toMutableSet()
if (idsInQueuesToRemove.isNotEmpty()) {
q.idsBinList.removeAll(idsInQueuesToRemove)
@ -374,7 +380,7 @@ object Queues {
}
private fun getCurrentlyPlayingPosition(queueItems: List<Episode>, currentPlaying: Playable?): Int {
if (currentPlaying !is EpisodeMedia) return -1
val curPlayingItemId = currentPlaying.episode!!.id
val curPlayingItemId = currentPlaying.episodeOrFetch()?.id
for (i in queueItems.indices) {
if (curPlayingItemId == queueItems[i].id) return i
}

View File

@ -10,7 +10,6 @@ import io.realm.kotlin.Realm
import io.realm.kotlin.RealmConfiguration
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.isManaged
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.RealmObject
import io.realm.kotlin.types.TypedRealmObject
import kotlinx.coroutines.*
@ -19,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor
object RealmDB {
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
private const val SCHEMA_VERSION_NUMBER = 16L
private const val SCHEMA_VERSION_NUMBER = 17L
private val ioScope = CoroutineScope(Dispatchers.IO)

View File

@ -1,12 +1,15 @@
package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.update
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.os.Parcel
import android.os.Parcelable
import io.realm.kotlin.ext.isManaged
import io.realm.kotlin.types.EmbeddedRealmObject
import io.realm.kotlin.types.annotations.Ignore
import io.realm.kotlin.types.annotations.Index
@ -38,11 +41,11 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
@get:JvmName("getDurationProperty")
@set:JvmName("setDurationProperty")
var duration = 0
var duration = 0 // in milliseconds
@get:JvmName("getPositionProperty")
@set:JvmName("setPositionProperty")
var position = 0 // Current position in file
var position = 0 // Current position in file, in milliseconds
@get:JvmName("getLastPlayedTimeProperty")
@set:JvmName("setLastPlayedTimeProperty")
@ -58,7 +61,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
var episode: Episode? = null
var playbackCompletionTime: Long = 0
@Ignore
var playbackCompletionDate: Date? = null
get() = field?.clone() as? Date
@ -66,6 +68,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
field = value?.clone() as? Date
this.playbackCompletionTime = value?.time ?: 0
}
var playbackCompletionTime: Long = 0
var startPosition: Int = -1
@ -76,8 +79,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
var hasEmbeddedPicture: Boolean? = null
/* Used for loading item when restoring from parcel. */
var episodeId: Long = 0
private set
// var episodeId: Long = 0
// private set
@Ignore
val isInProgress: Boolean
@ -116,7 +119,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
}
fun getHumanReadableIdentifier(): String? {
return if (episode?.title != null) episode!!.title else downloadUrl
return episode?.title ?: downloadUrl
}
/**
@ -175,7 +178,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
override fun setPosition(newPosition: Int) {
this.position = newPosition
if (newPosition > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false)
if (newPosition > 0 && episode?.isNew == true) episode!!.setPlayed(false)
}
override fun getLastPlayedTime(): Long {
@ -236,18 +239,6 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
dest.writeLong(lastPlayedTime)
}
// no longer needed
// override fun writeToPreferences(prefEditor: SharedPreferences.Editor) {
// if (episode == null) prefEditor.putLong(PREF_FEED_ID, 0L)
// else {
// val f = episode!!.feed
// if (f != null) prefEditor.putLong(PREF_FEED_ID, f.id)
// else prefEditor.putLong(PREF_FEED_ID, 0L)
// }
//
// prefEditor.putLong(PREF_MEDIA_ID, id)
// }
override fun getEpisodeTitle(): String {
return episode?.title ?: episode?.identifyingValue ?: "No title"
}
@ -364,12 +355,22 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
result = 31 * result + startPosition
result = 31 * result + playedDurationWhenStarted
result = 31 * result + (hasEmbeddedPicture?.hashCode() ?: 0)
result = 31 * result + episodeId.hashCode()
// result = 31 * result + episodeId.hashCode()
return result
}
fun getTheEpisode(): Episode? {
return if (episode != null) episode else realm.query(Episode::class).query("id == $id").first().find()
fun episodeOrFetch(): Episode? {
return if (episode != null) episode else {
var item = realm.query(Episode::class).query("id == $id").first().find()
Logd(TAG, "episodeOrFetch warning: episode of media is null: ${id} ${item?.title}")
if (item != null) {
item = upsertBlk(item) {
it.media = this@EpisodeMedia
it.media!!.episode = it
}
}
if (item == null || isManaged()) item else unmanaged(item)
}
}
companion object {
@ -408,7 +409,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
Date(inVal.readLong()),
inVal.readInt(),
inVal.readLong())
result.episodeId = itemID
// result.episodeId = itemID
return result
}

View File

@ -32,5 +32,9 @@ class PlayQueue : RealmObject {
updated = Date().time
}
fun size() : Int {
return episodeIds.size
}
constructor() {}
}

View File

@ -47,7 +47,7 @@ object ChapterUtils {
var chaptersFromDatabase: List<Chapter>? = null
var chaptersFromPodcastIndex: List<Chapter>? = null
if (playable is EpisodeMedia) {
val item = playable.episode
val item = playable.episodeOrFetch()
if (item != null) {
if (item.chapters.isNotEmpty()) chaptersFromDatabase = item.chapters
if (!item.podcastIndexChapterUrl.isNullOrEmpty()) chaptersFromPodcastIndex = loadChaptersFromUrl(item.podcastIndexChapterUrl!!, forceRefresh)

View File

@ -4,6 +4,7 @@ import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.webkit.URLUtil
@ -12,6 +13,7 @@ import org.apache.commons.io.FilenameUtils
import java.io.File
import java.io.IOException
@SuppressLint("StaticFieldLeak")
object FilesUtils {
private val TAG: String = FilesUtils::class.simpleName ?: "Anonymous"
@ -47,9 +49,9 @@ object FilesUtils {
}
fun getMediafilePath(media: EpisodeMedia): String {
val item = media.getTheEpisode() ?: return ""
Logd(TAG, "item managed: ${item?.isManaged()}")
val title = item?.feed?.title?:return ""
val item = media.episodeOrFetch() ?: return ""
Logd(TAG, "item managed: ${item.isManaged()}")
val title = item.feed?.title?:return ""
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
return getDataFolder(mediaPath).toString() + "/"
}
@ -58,10 +60,8 @@ object FilesUtils {
var titleBaseFilename = ""
// Try to generate the filename by the item title
if (media.episode?.title != null) {
val title = media.episode!!.title!!
titleBaseFilename = FileNameGenerator.generateFileName(title)
}
val item_ = media.episodeOrFetch()
if (item_?.title != null) titleBaseFilename = FileNameGenerator.generateFileName(item_.title!!)
val urlBaseFilename = URLUtil.guessFileName(media.downloadUrl, null, media.mimeType)

View File

@ -38,7 +38,7 @@ object ImageResourceUtils {
@JvmStatic
fun getFallbackImageLocation(playable: Playable): String? {
if (playable is EpisodeMedia) {
val item = playable.episode
val item = playable.episodeOrFetch()
return item?.feed?.imageUrl
} else return playable.getImageLocation()
}

View File

@ -5,6 +5,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.view.View
import android.widget.ImageView
@ -38,7 +39,7 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
null -> false
else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.downloadUrl!!)?:false
}
// Logd("ItemActionButton", "forItem: ${episode.feedId} ${episode.feed?.isLocalFeed} ${media.downloaded} ${isCurrentlyPlaying(media)} ${curMedia is EpisodeMedia} ${media.id == (curMedia as? EpisodeMedia)?.id} ${episode.title} ")
// Logd("ItemActionButton", "forItem: ${episode.feedId} ${episode.feed?.isLocalFeed} ${media.downloaded} ${isCurrentlyPlaying(media)} ${episode.title} ")
return when {
media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(episode)
isCurrentlyPlaying(media) -> PauseActionButton(episode)

View File

@ -57,7 +57,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
fun notifyMissingEpisodeMediaFile(context: Context, media: EpisodeMedia) {
Logd(TAG, "notifyMissingEpisodeMediaFile called")
Log.i(TAG, "The feedmanager was notified about a missing episode. It will update its database now.")
val episode = media.episode
val episode = media.episodeOrFetch()
if (episode != null) {
episode.media = media
episode.media?.downloaded = false

View File

@ -232,7 +232,7 @@ class VideoplayerActivity : CastEnabledActivity() {
val hasWebsiteLink = getWebsiteLinkWithFallback(media) != null
menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink)
val isItemAndHasLink = isEpisodeMedia && hasLinkToShare((media as EpisodeMedia).episode)
val isItemAndHasLink = isEpisodeMedia && hasLinkToShare((media as EpisodeMedia).episodeOrFetch())
val isItemHasDownloadLink = isEpisodeMedia && (media as EpisodeMedia?)?.downloadUrl != null
menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink)
@ -287,7 +287,7 @@ class VideoplayerActivity : CastEnabledActivity() {
// controller == null -> return false
else -> {
val media = curMedia ?: return false
val feedItem = (media as? EpisodeMedia)?.episode
val feedItem = (media as? EpisodeMedia)?.episodeOrFetch()
when {
item.itemId == R.id.add_to_favorites_item && feedItem != null -> {
setFavorite(feedItem, true)
@ -470,7 +470,7 @@ class VideoplayerActivity : CastEnabledActivity() {
return when {
media == null -> null
!media.getWebsiteLink().isNullOrBlank() -> media.getWebsiteLink()
media is EpisodeMedia -> media.episode?.getLinkWithFallback()
media is EpisodeMedia -> media.episodeOrFetch()?.getLinkWithFallback()
else -> null
}
}

View File

@ -42,7 +42,7 @@ abstract class EpisodeFilterDialog : BottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val layout = inflater.inflate(R.layout.filter_dialog, null, false)
val layout = inflater.inflate(R.layout.filter_dialog, container, false)
_binding = FilterDialogBinding.bind(layout)
rows = binding.filterRows
Logd("EpisodeFilterDialog", "fragment onCreateView")

View File

@ -46,7 +46,7 @@ class FeedFilterDialog : BottomSheetDialogFragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val layout = inflater.inflate(R.layout.filter_dialog, null, false)
val layout = inflater.inflate(R.layout.filter_dialog, container, false)
_binding = FilterDialogBinding.bind(layout)
rows = binding.filterRows
Logd("FeedFilterDialog", "fragment onCreateView")

View File

@ -2,7 +2,9 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
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.service.PlaybackService
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnable
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
@ -21,6 +23,7 @@ import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
@ -46,19 +49,15 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.*
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import java.util.concurrent.TimeUnit
import kotlin.math.*
import kotlin.time.DurationUnit
class SleepTimerDialog : DialogFragment() {
private var _binding: TimeDialogBinding? = null
private val binding get() = _binding!!
private lateinit var etxtTime: EditText
private lateinit var timeSetup: LinearLayout
private lateinit var timeDisplay: LinearLayout
private lateinit var time: TextView
private lateinit var chAutoEnable: CheckBox
@UnstableApi override fun onStart() {
@ -74,17 +73,13 @@ class SleepTimerDialog : DialogFragment() {
@UnstableApi override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = TimeDialogBinding.inflate(layoutInflater)
val content = binding.root
// val content = View.inflate(context, R.layout.time_dialog, null)
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.sleep_timer_label)
builder.setView(binding.root)
builder.setPositiveButton(R.string.close_label, null)
etxtTime = binding.etxtTime
timeSetup = binding.timeSetup
timeDisplay = binding.timeDisplay
timeDisplay.visibility = View.GONE
time = binding.time
binding.timeDisplay.visibility = View.GONE
val extendSleepFiveMinutesButton = binding.extendSleepFiveMinutesButton
extendSleepFiveMinutesButton.text = getString(R.string.extend_sleep_timer_label, 5)
val extendSleepTenMinutesButton = binding.extendSleepTenMinutesButton
@ -101,27 +96,30 @@ class SleepTimerDialog : DialogFragment() {
extendSleepTimer((20 * 1000 * 60).toLong())
}
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) etxtTime.visibility = View.GONE
else etxtTime.visibility = View.VISIBLE
}
etxtTime.setText(lastTimerValue())
etxtTime.postDelayed({
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT)
}, 100)
val cbShakeToReset = binding.cbShakeToReset
val cbVibrate = binding.cbVibrate
chAutoEnable = binding.chAutoEnable
val changeTimesButton = binding.changeTimesButton
cbShakeToReset.isChecked = shakeToReset()
cbVibrate.isChecked = vibrate()
binding.cbShakeToReset.isChecked = shakeToReset()
binding.cbVibrate.isChecked = vibrate()
chAutoEnable.setChecked(autoEnable())
changeTimesButton.isEnabled = chAutoEnable.isChecked
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setShakeToReset(isChecked)
}
cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setAutoEnable(isChecked)
changeTimesButton.isEnabled = isChecked
@ -135,23 +133,26 @@ class SleepTimerDialog : DialogFragment() {
showTimeRangeDialog(from, to)
}
val disableButton = binding.disableSleeptimerButton
disableButton.setOnClickListener {
binding.disableSleeptimerButton.setOnClickListener {
playbackService?.taskManager?.disableSleepTimer()
}
val setButton = binding.setSleeptimerButton
setButton.setOnClickListener {
binding.setSleeptimerButton.setOnClickListener {
if (!PlaybackService.isRunning) {
Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show()
return@setOnClickListener
}
try {
val time = etxtTime.getText().toString().toLong()
val time = if (binding.endEpisode.isChecked) {
val curPosition = curMedia?.getPosition() ?: 0
val duration = curMedia?.getDuration() ?: 0
val converter = TimeSpeedConverter(curSpeedMultiplier)
TimeUnit.MILLISECONDS.toMinutes(converter.convert(max((duration - curPosition).toDouble(), 0.0).toInt()).toLong()) // ms to minutes
} else etxtTime.getText().toString().toLong()
Logd(TAG, "Sleep timer set: $time")
if (time == 0L) throw NumberFormatException("Timer must not be zero")
setLastTimer(etxtTime.getText().toString())
setLastTimer(time.toString())
setSleepTimer(timerMillis())
closeKeyboard(content)
} catch (e: NumberFormatException) {
e.printStackTrace()
@ -167,12 +168,12 @@ class SleepTimerDialog : DialogFragment() {
super.onDestroyView()
}
fun extendSleepTimer(extendTime: Long) {
private fun extendSleepTimer(extendTime: Long) {
val timeLeft = playbackService?.taskManager?.sleepTimerTimeLeft ?: Playable.INVALID_TIME.toLong()
if (timeLeft != Playable.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
}
fun setSleepTimer(time: Long) {
private fun setSleepTimer(time: Long) {
playbackService?.taskManager?.setSleepTimer(time)
}
@ -224,10 +225,10 @@ class SleepTimerDialog : DialogFragment() {
}
}
fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) {
timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE
timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE
time.text = getDurationStringLong(event.getTimeLeft().toInt())
private fun timerUpdated(event: FlowEvent.SleepTimerUpdatedEvent) {
binding.timeDisplay.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE
binding.timeSetup.visibility = if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE
binding.time.text = getDurationStringLong(event.getTimeLeft().toInt())
}
private fun closeKeyboard(content: View) {
@ -254,7 +255,7 @@ class SleepTimerDialog : DialogFragment() {
private val paintSelected = Paint()
private val paintText = Paint()
private val bounds = RectF()
var touching: Int = 0
private var touching: Int = 0
init {
setup()

View File

@ -261,7 +261,7 @@ import java.util.*
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed)
if (codeArray[1]) {
val episode = (curMedia as? EpisodeMedia)?.episode ?: curEpisode
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
if (episode != null) {
var feed = episode.feed
if (feed != null) {

View File

@ -23,6 +23,7 @@ import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
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.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable
@ -229,7 +230,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
}
currentMedia = curMedia
val item = (currentMedia as? EpisodeMedia)?.episode
val item = (currentMedia as? EpisodeMedia)?.episodeOrFetch()
if (item != null) playerDetailsFragment?.setItem(item)
updateUi()
playerUI?.updateUi(currentMedia)
@ -327,8 +328,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
// Logd(TAG, "onPlayEvent ${event.episode.title}")
val media = event.media
if (currentMedia?.getIdentifier() == null || media?.getIdentifier() != currentMedia?.getIdentifier()) {
val media = event.media ?: return
if (currentMedia?.getIdentifier() == null || media.getIdentifier() != currentMedia?.getIdentifier()) {
currentMedia = media
playerUI?.updateUi(currentMedia)
playerDetailsFragment?.setItem(curEpisode!!)
@ -430,7 +431,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val isEpisodeMedia = currentMedia is EpisodeMedia
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episode else null
val item = if (isEpisodeMedia) (currentMedia as EpisodeMedia).episodeOrFetch() else null
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
val mediaType = curMedia?.getMediaType()
@ -445,7 +446,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
val media: Playable = curMedia ?: return false
val feedItem = if (media is EpisodeMedia) media.episode else null
val feedItem = if (media is EpisodeMedia) media.episodeOrFetch() else null
if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
val itemId = menuItem.itemId

View File

@ -359,7 +359,7 @@ import kotlinx.coroutines.withContext
}
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
val item = (event.media as? EpisodeMedia)?.episode ?: return
val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return
val pos = if (curIndex in 0..<episodes.size && event.media.getIdentifier() == episodes[curIndex].media?.getIdentifier() && isCurMedia(episodes[curIndex].media))
curIndex else EpisodeUtil.indexOfItemWithId(episodes, item.id)

View File

@ -194,7 +194,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
adapter.setMedia(media)
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEUTRAL).visibility = View.INVISIBLE
if ((media is EpisodeMedia) && !media.episode?.podcastIndexChapterUrl.isNullOrEmpty())
if ((media is EpisodeMedia) && !media.episodeOrFetch()?.podcastIndexChapterUrl.isNullOrEmpty())
(dialog as AlertDialog).getButton(DialogInterface.BUTTON_NEUTRAL).visibility = View.VISIBLE
val positionOfCurrentChapter = getCurrentChapter(media)

View File

@ -244,7 +244,8 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To
Log.e(TAG, "Could not find feed media for feed id: " + status.feedfileId)
return@OnClickListener
}
if (media.episode != null) DownloadActionButton(media.episode!!).onClick(context)
val item_ = media.episodeOrFetch()
if (item_ != null) DownloadActionButton(item_).onClick(context)
(context as MainActivity).showSnackbarAbovePlayer(R.string.status_downloading_label, Toast.LENGTH_SHORT)
})
}

View File

@ -7,10 +7,12 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
@ -196,6 +198,13 @@ import java.util.*
private val filesRemoved: MutableList<String> = mutableListOf()
private fun reconsile() {
runOnIOScope {
val items = realm.query(Episode::class).query("media.episode == nil").find()
Logd(TAG, "number of episode with null backlink: ${items.size}")
for (item in items) {
upsert(item) {
it.media!!.episode = it
}
}
nameEpisodeMap.clear()
episodes.forEach { e ->
var fileUrl = e.media?.fileUrl ?: return@forEach
@ -375,7 +384,7 @@ import java.util.*
}
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
val item = (event.media as? EpisodeMedia)?.episode ?: return
val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return
val pos = if (curIndex in 0..<episodes.size && event.media.getIdentifier() == episodes[curIndex].media?.getIdentifier() && isCurMedia(episodes[curIndex].media))
curIndex else EpisodeUtil.indexOfItemWithId(episodes, item.id)
@ -439,7 +448,8 @@ import java.util.*
for (url in urls) {
if (url == null) continue
val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue
if (media.episode != null) episodes.add(media.episode!!)
val item_ = media.episodeOrFetch()
if (item_ != null) episodes.add(item_)
}
return realm.copyFromRealm(episodes)
}

View File

@ -455,7 +455,7 @@ import java.util.concurrent.Semaphore
}
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
val item = (event.media as? EpisodeMedia)?.episode ?: return
val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return
if (loadItemsRunning) return
val pos = if (curIndex in 0..<episodes.size && event.media.getIdentifier() == episodes[curIndex].media?.getIdentifier() && isCurMedia(episodes[curIndex].media))
curIndex else EpisodeUtil.indexOfItemWithId(episodes, item.id)

View File

@ -239,7 +239,8 @@ import kotlin.math.min
val medias = realm.query(EpisodeMedia::class).query("lastPlayedTime > $0 AND lastPlayedTime <= $1", start, end).find()
var episodes: MutableList<Episode> = mutableListOf()
for (m in medias) {
if (m.episode != null) episodes.add(m.episode!!)
val item_ = m.episodeOrFetch()
if (item_ != null) episodes.add(item_)
}
getPermutor(sortOrder).reorder(episodes)
if (episodes.size > offset) episodes = episodes.subList(offset, min(episodes.size, offset+limit))

View File

@ -459,9 +459,7 @@ import kotlin.concurrent.Volatile
item.id = 0L
item.feed = feed
val media = item.media
if (media != null) {
media.episode = item
}
media?.episode = item
}
val fo = updateFeed(requireContext(), feed, false)
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")

View File

@ -148,7 +148,7 @@ class PlayerDetailsFragment : Fragment() {
playable = curMedia
if (playable != null && playable is EpisodeMedia) {
val episodeMedia = playable as EpisodeMedia
currentItem = episodeMedia.episode
currentItem = episodeMedia.episodeOrFetch()
showHomeText = false
homeText = null
}
@ -266,9 +266,9 @@ class PlayerDetailsFragment : Fragment() {
when {
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
playable is EpisodeMedia -> {
val fm: EpisodeMedia? = (playable as EpisodeMedia?)
val item_ = (playable as EpisodeMedia).episodeOrFetch()
// If an item has chapters but they are not loaded yet, still display the button.
chapterControlVisible = fm?.episode != null && fm.episode!!.chapters.isNotEmpty()
chapterControlVisible = !item_?.chapters.isNullOrEmpty()
}
}
val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE

View File

@ -130,8 +130,10 @@ import java.util.*
val queues = realm.query(PlayQueue::class).find()
queueNames = queues.map { it.name }.toTypedArray()
val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, null)
val spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, toolbar, false)
queueSpinner = spinnerLayout.findViewById(R.id.queue_spinner)
val params = Toolbar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
params.gravity = Gravity.CENTER_VERTICAL
toolbar.addView(spinnerLayout)
spinnerAdaptor = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, queueNames)
spinnerAdaptor.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
@ -148,7 +150,7 @@ import java.util.*
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
toolbar.inflateMenu(R.menu.queue)
refreshToolbarState()
refreshMenuItems()
binding.progressBar.visibility = View.VISIBLE
recyclerView = binding.recyclerView
@ -314,7 +316,7 @@ import java.util.*
FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return
}
adapter?.updateDragDropEnabled()
refreshToolbarState()
refreshMenuItems()
recyclerView.saveScrollPosition(TAG)
refreshInfoBar()
}
@ -407,7 +409,7 @@ import java.util.*
}
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
val item = (event.media as? EpisodeMedia)?.episode ?: return
val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return
val pos = if (curIndex in 0..<queueItems.size && event.media.getIdentifier() == queueItems[curIndex].media?.getIdentifier() && isCurMedia(queueItems[curIndex].media))
curIndex else EpisodeUtil.indexOfItemWithId(queueItems, item.id)
@ -423,7 +425,7 @@ import java.util.*
if (showBin) return
// Logd(TAG, "onPlayerStatusChanged() called with event = [$event]")
loadItems(false)
refreshToolbarState()
refreshMenuItems()
}
private fun onEpisodePlayedEvent(event: FlowEvent.EpisodePlayedEvent) {
@ -435,7 +437,7 @@ import java.util.*
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems, event.episode.id)
if (pos >= 0) queueItems[pos].setPlayed(event.episode.isPlayed())
}
refreshToolbarState()
refreshMenuItems()
}
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
@ -472,12 +474,21 @@ import java.util.*
super.onDestroyView()
}
private fun refreshToolbarState() {
val keepSorted: Boolean = isQueueKeepSorted
private fun refreshMenuItems() {
if (showBin) {
toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(false)
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(false)
toolbar.menu?.findItem(R.id.add_queue)?.setVisible(false)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(false)
toolbar.menu?.findItem(R.id.action_search)?.setVisible(false)
} else {
toolbar.menu?.findItem(R.id.action_search)?.setVisible(true)
toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(true)
toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!keepSorted)
toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!isQueueKeepSorted)
toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default")
toolbar.menu?.findItem(R.id.add_queue)?.setVisible(queueNames.size<9)
toolbar.menu?.findItem(R.id.add_queue)?.setVisible(queueNames.size < 9)
}
}
@UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean {
@ -485,6 +496,7 @@ import java.util.*
when (itemId) {
R.id.show_bin -> {
showBin = !showBin
refreshMenuItems()
if (showBin) {
item.setIcon(R.drawable.playlist_play)
speedDialView.addActionItem(addToQueueActionItem)
@ -656,7 +668,7 @@ import java.util.*
@UnstableApi private fun setQueueLocked(locked: Boolean) {
isQueueLocked = locked
refreshToolbarState()
refreshMenuItems()
adapter?.updateDragDropEnabled()
if (queueItems.size == 0) {
@ -751,7 +763,7 @@ import java.util.*
queueItems.clear()
if (showBin) {
queueItems.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
.find().sortedBy { curQueue.idsBinList.indexOf(it.id) }))
.find().sortedByDescending { curQueue.idsBinList.indexOf(it.id) }))
} else {
curQueue.episodes.clear()
curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds)
@ -770,7 +782,7 @@ import java.util.*
override fun onStartSelectMode() {
swipeActions.detach()
speedDialView.visibility = View.VISIBLE
refreshToolbarState()
refreshMenuItems()
binding.infoBar.visibility = View.GONE
}

View File

@ -295,7 +295,7 @@ import java.lang.ref.WeakReference
}
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
val item = (event.media as? EpisodeMedia)?.episode ?: return
val item = (event.media as? EpisodeMedia)?.episodeOrFetch() ?: return
val pos = if (curIndex in 0..<results.size && event.media.getIdentifier() == results[curIndex].media?.getIdentifier() && isCurMedia(results[curIndex].media))
curIndex else EpisodeUtil.indexOfItemWithId(results, item.id)

View File

@ -250,7 +250,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
lifecycleScope.launch {
try {
item = withContext(Dispatchers.IO) {
val feedItem = (curMedia as? EpisodeMedia)?.episode
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
if (feedItem != null) {
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)

View File

@ -447,7 +447,7 @@ class StatisticsFragment : Fragment() {
else {
// progress import does not include playedDuration
if (includeMarkedAsPlayed) {
if (m.playbackCompletionTime > 0 || m.episode?.playState == Episode.PLAYED)
if (m.playbackCompletionTime > 0 || m.episodeOrFetch()?.playState == Episode.PLAYED)
dur += m.duration
else if (m.position > 0) dur += m.position
} else dur += m.position

View File

@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:id="@+id/queue_title_spinner">
<Spinner
android:id="@+id/queue_spinner"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:dropDownWidth="100dp"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/text_size_large"
android:textStyle="bold"

View File

@ -20,6 +20,12 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/end_episode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/end_episode" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -68,7 +74,8 @@
android:layout_gravity="center"
android:gravity="center"
android:textSize="32sp"
android:textColor="?android:attr/textColorPrimary" />
android:textColor="?android:attr/textColorPrimary"
tools:ignore="HardcodedText"/>
<Button
android:id="@+id/disableSleeptimerButton"

View File

@ -677,9 +677,10 @@
<string name="set_sleeptimer_label">Set sleep timer</string>
<string name="disable_sleeptimer_label">Disable sleep timer</string>
<string name="extend_sleep_timer_label">+%d min</string>
<string name="end_episode">To end of episode</string>
<string name="sleep_timer_always">Always</string>
<string name="sleep_timer_label">Sleep timer</string>
<string name="time_dialog_invalid_input">Invalid input, time has to be an integer</string>
<string name="time_dialog_invalid_input">Invalid input, time has to be a positive integer</string>
<string name="shake_to_reset_label">Shake to reset</string>
<string name="timer_vibration_label">Vibrate shortly before end</string>
<string name="time_seconds">seconds</string>

View File

@ -85,7 +85,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
Mockito.`when`(feedMedia.episode?.feed?.id).thenReturn(FEED_ID + 1)
Mockito.`when`(feedMedia.episodeOrFetch()?.feed?.id).thenReturn(FEED_ID + 1)
// val volumeUpdater = PlaybackService.VolumeUpdater()
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.OFF)
@ -102,7 +102,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION)
@ -121,7 +121,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION)
@ -140,7 +140,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION)
@ -159,7 +159,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION)
@ -178,7 +178,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.LIGHT_REDUCTION)
@ -197,7 +197,7 @@ class VolumeUpdaterTest {
val feedMedia = mockFeedMedia()
Mockito.`when`(curMedia).thenReturn(feedMedia)
val feedPreferences: FeedPreferences = feedMedia.episode!!.feed!!.preferences!!
val feedPreferences: FeedPreferences = feedMedia.episodeOrFetch()!!.feed!!.preferences!!
PlaybackService.updateVolumeIfNecessary(mediaPlayer!!, FEED_ID, VolumeAdaptionSetting.HEAVY_REDUCTION)
@ -214,7 +214,7 @@ class VolumeUpdaterTest {
val feed = Mockito.mock(Feed::class.java)
val feedPreferences = Mockito.mock(FeedPreferences::class.java)
Mockito.`when`(episodeMedia.episode).thenReturn(episode)
Mockito.`when`(episodeMedia.episodeOrFetch()).thenReturn(episode)
Mockito.`when`(episode.feed).thenReturn(feed)
Mockito.`when`(feed.id).thenReturn(FEED_ID)
Mockito.`when`(feed.preferences).thenReturn(feedPreferences)

View File

@ -511,9 +511,10 @@ class DbWriterTest {
@Throws(Exception::class)
fun testAddItemToPlaybackHistoryNotPlayedYet() {
val media: EpisodeMedia = playbackHistorySetup(null)
if (media.episode != null) {
val item_ = media.episodeOrFetch()
if (item_ != null) {
runBlocking {
val job = addToHistory(media.episode!!)
val job = addToHistory(item_)
withTimeout(TIMEOUT * 1000) { job.join() }
}
}
@ -527,9 +528,10 @@ class DbWriterTest {
val oldDate: Long = 0
val media: EpisodeMedia = playbackHistorySetup(Date(oldDate))
if (media.episode != null) {
val item_ = media.episodeOrFetch()
if (item_ != null) {
runBlocking {
val job = addToHistory(media.episode!!)
val job = addToHistory(item_)
withTimeout(TIMEOUT*1000) { job.join() }
}
}

View File

@ -1,3 +1,14 @@
# 6.2.2
* added EpisodeMedia null relationship handling
* in sleep timer setting, added "to end of episode" option
* frequency of sleep timer check is reduced to every 10 seconds (from 1 second)
* in Queue bin view, items' order is changed to descending
* in Queue bin view, disabled some menu items
* refined "remove from queue" operation when an episode ended playing
* eliminated the double starts when playing the next episode in queue
* re-ensured circular queue
# 6.2.1
* likely fixed crash issue in Queue view during download

View File

@ -0,0 +1,11 @@
Version 6.2.2 brings several changes:
* added EpisodeMedia null relationship handling
* in sleep timer setting, added "to end of episode" option
* frequency of sleep timer check is reduced to every 10 seconds (from 1 second)
* in Queue bin view, items' order is changed to descending
* in Queue bin view, disabled some menu items
* refined "remove from queue" operation when an episode ended playing
* eliminated the double starts when playing the next episode in queue
* re-ensured circular queue