diff --git a/.directory b/.directory index 632a998b..84969a4e 100644 --- a/.directory +++ b/.directory @@ -1,4 +1,7 @@ [Dolphin] -Timestamp=2024,8,2,9,3,22.216 +Timestamp=2024,8,10,7,39,7.985 Version=4 ViewMode=1 + +[Settings] +HiddenFilesShown=true diff --git a/app/build.gradle b/app/build.gradle index 1d7ee5ac..2f828647 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,7 +4,7 @@ plugins { id 'org.jetbrains.kotlin.android' alias(libs.plugins.compose.compiler) id 'io.realm.kotlin' - id('com.github.triplet.play') version '3.8.3' apply false + id('com.github.triplet.play') version '3.9.0' apply false } //apply plugin: 'org.jetbrains.compose' @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020230 - versionName "6.3.6" + versionCode 3020231 + versionName "6.3.7" applicationId "ac.mdiq.podcini.R" def commit = "" @@ -102,23 +102,11 @@ android { namespace "ac.mdiq.podcini" lint { lintConfig = file("lint.xml") - - // disable "GradleDependency" checkReleaseBuilds false checkDependencies true warningsAsErrors true abortOnError true checkGeneratedSources = true - -// checkOnly += ['NewApi', 'InlinedApi', 'Performance', 'DuplicateIds'] - -// disable += ['TypographyDashes', 'TypographyQuotes', 'ObsoleteLintCustomCheck', 'CheckResult', 'UnusedAttribute', 'BatteryLife', 'InflateParams', -// 'RestrictedApi', 'TrustAllX509TrustManager', 'ExportedReceiver', 'VectorDrawableCompat', -// 'StaticFieldLeak', 'UseCompoundDrawables', 'NestedWeights', 'Overdraw', 'UselessParent', 'TextFields', -// 'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription', -// 'KeyboardInaccessibleWidget', 'LabelFor', 'SetTextI18n', 'HardcodedText', 'RelativeOverlap', -// 'RtlCompat', 'RtlHardcoded', 'MissingMediaBrowserServiceIntentFilter', 'VectorPath', -// 'InvalidPeriodicWorkRequestInterval', 'NotifyDataSetChanged', 'RtlEnabled'] disable += ['TypographyDashes', 'TypographyQuotes', 'ObsoleteLintCustomCheck', 'BatteryLife', 'ExportedReceiver', 'VectorDrawableCompat', 'NestedWeights', 'Overdraw', 'TextFields', 'AlwaysShowAction', 'Autofill', 'ClickableViewAccessibility', 'ContentDescription', diff --git a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt b/app/src/androidTest/kotlin/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt deleted file mode 100644 index dd4054ca..00000000 --- a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt +++ /dev/null @@ -1,83 +0,0 @@ -package de.test.podcini.service.playback - -import ac.mdiq.podcini.storage.model.MediaType -import ac.mdiq.podcini.storage.model.Playable -import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo -import ac.mdiq.podcini.playback.base.MediaPlayerCallback - -class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback { - private var isCancelled = false - - fun cancel() { - isCancelled = true - } - - override fun statusChanged(newInfo: MediaPlayerInfo?) { - if (isCancelled) { - return - } - originalCallback.statusChanged(newInfo) - } - - override fun shouldStop() { - if (isCancelled) return - -// originalCallback.shouldStop() - } - - override fun onMediaChanged(reloadUI: Boolean) { - if (isCancelled) { - return - } - originalCallback.onMediaChanged(reloadUI) - } - - override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) { - if (isCancelled) { - return - } - originalCallback.onPostPlayback(media, ended, skipped, playingNext) - } - - override fun onPlaybackStart(playable: Playable, position: Int) { - if (isCancelled) { - return - } - originalCallback.onPlaybackStart(playable, position) - } - - override fun onPlaybackPause(playable: Playable?, position: Int) { - if (isCancelled) { - return - } - originalCallback.onPlaybackPause(playable, position) - } - - override fun getNextInQueue(currentMedia: Playable?): Playable? { - if (isCancelled) { - return null - } - return originalCallback.getNextInQueue(currentMedia) - } - - override fun findMedia(url: String): Playable? { - if (isCancelled) { - return null - } - return originalCallback.findMedia(url) - } - - override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { - if (isCancelled) { - return - } - originalCallback.onPlaybackEnded(mediaType, stopPlaying) - } - - override fun ensureMediaInfoLoaded(media: Playable) { - if (isCancelled) { - return - } - originalCallback.ensureMediaInfoLoaded(media) - } -} diff --git a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt b/app/src/androidTest/kotlin/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt deleted file mode 100644 index b7fb6ed9..00000000 --- a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt +++ /dev/null @@ -1,40 +0,0 @@ -package de.test.podcini.service.playback - -import ac.mdiq.podcini.storage.model.MediaType -import ac.mdiq.podcini.storage.model.Playable -import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo -import ac.mdiq.podcini.playback.base.MediaPlayerCallback - -open class DefaultMediaPlayerCallback : MediaPlayerCallback { - override fun statusChanged(newInfo: MediaPlayerInfo?) { - } - - override fun shouldStop() { - } - - override fun onMediaChanged(reloadUI: Boolean) { - } - - override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) { - } - - override fun onPlaybackStart(playable: Playable, position: Int) { - } - - override fun onPlaybackPause(playable: Playable?, position: Int) { - } - - override fun getNextInQueue(currentMedia: Playable?): Playable? { - return null - } - - override fun findMedia(url: String): Playable? { - return null - } - - override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { - } - - override fun ensureMediaInfoLoaded(media: Playable) { - } -} diff --git a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt index 36641a4c..3dd6a50a 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt @@ -2,6 +2,7 @@ package de.test.podcini.service.playback import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo +import ac.mdiq.podcini.playback.base.MediaPlayerCallback import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.service.LocalMediaPlayer import ac.mdiq.podcini.storage.model.* @@ -798,6 +799,72 @@ class MediaPlayerBaseTest { } private class UnexpectedStateChange(status: PlayerStatus) : AssertionFailedError("Unexpected state change: $status") + + open class DefaultMediaPlayerCallback : MediaPlayerCallback { + override fun statusChanged(newInfo: MediaPlayerInfo?) {} + override fun shouldStop() {} + override fun onMediaChanged(reloadUI: Boolean) {} + override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) {} + override fun onPlaybackStart(playable: Playable, position: Int) {} + override fun onPlaybackPause(playable: Playable?, position: Int) {} + override fun getNextInQueue(currentMedia: Playable?): Playable? { + return null + } + override fun findMedia(url: String): Playable? { + return null + } + override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) {} + override fun ensureMediaInfoLoaded(media: Playable) {} + } + + class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback { + private var isCancelled = false + + fun cancel() { + isCancelled = true + } + override fun statusChanged(newInfo: MediaPlayerInfo?) { + if (isCancelled) return + originalCallback.statusChanged(newInfo) + } + override fun shouldStop() { + if (isCancelled) return +// originalCallback.shouldStop() + } + override fun onMediaChanged(reloadUI: Boolean) { + if (isCancelled) return + originalCallback.onMediaChanged(reloadUI) + } + override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) { + if (isCancelled) return + originalCallback.onPostPlayback(media, ended, skipped, playingNext) + } + override fun onPlaybackStart(playable: Playable, position: Int) { + if (isCancelled) return + originalCallback.onPlaybackStart(playable, position) + } + override fun onPlaybackPause(playable: Playable?, position: Int) { + if (isCancelled) return + originalCallback.onPlaybackPause(playable, position) + } + override fun getNextInQueue(currentMedia: Playable?): Playable? { + if (isCancelled) return null + return originalCallback.getNextInQueue(currentMedia) + } + override fun findMedia(url: String): Playable? { + if (isCancelled) return null + return originalCallback.findMedia(url) + } + override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { + if (isCancelled) return + originalCallback.onPlaybackEnded(mediaType, stopPlaying) + } + override fun ensureMediaInfoLoaded(media: Playable) { + if (isCancelled) return + originalCallback.ensureMediaInfoLoaded(media) + } + } + companion object { private const val PLAYABLE_DEST_URL = "psmptestfile.mp3" private const val LATCH_TIMEOUT_SECONDS = 3 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 9e91f219..7d9aa4d9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -14,6 +14,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.storage.database.Episodes import ac.mdiq.podcini.storage.database.LogsAndStats import ac.mdiq.podcini.storage.database.Queues +import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.DownloadResult import ac.mdiq.podcini.storage.model.Episode @@ -60,7 +61,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) if (ignoreConstraints) workRequest.setConstraints(Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) else workRequest.setConstraints(constraints) - if (item.media?.downloadUrl != null) WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.downloadUrl!!, ExistingWorkPolicy.KEEP, workRequest.build()) } @@ -144,7 +144,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { Logd(TAG, "starting doWork") ClientConfigurator.initialize(applicationContext) val mediaId = inputData.getLong(WORK_DATA_MEDIA_ID, 0) - val media = Episodes.getEpisodeMedia(mediaId) + val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find() if (media == null) { Log.e(TAG, "media is null for mediaId: $mediaId") return Result.failure() @@ -220,8 +220,13 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { } if (dest.exists()) { try { - media.setfileUrlOrNull(request.destination) - Episodes.persistEpisodeMedia(media) + var episode = realm.query(Episode::class).query("id == ${media.id}").first().find() + if (episode != null) { + episode = upsertBlk(episode) { + it.media?.setfileUrlOrNull(request.destination) + } + EventFlow.postEvent(FlowEvent.EpisodeMediaEvent.updated(episode)) + } else Log.e(TAG, "performDownload media.episode is null") } catch (e: Exception) { Log.e(TAG, "performDownload Exception in writeFileUrl: " + e.message) } @@ -340,79 +345,53 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult, private val request: DownloadRequest) : Runnable { @UnstableApi override fun run() { - val media = Episodes.getEpisodeMedia(request.feedfileId) + var item = realm.query(Episode::class).query("id == ${request.feedfileId}").first().find() + if (item == null) { + Log.e(TAG, "Could not find downloaded episode object in database") + return + } + val media = item.media if (media == null) { Log.e(TAG, "Could not find downloaded media object in database") return } - // media.setDownloaded modifies played state - var item = media.episodeOrFetch() - val broadcastUnreadStateUpdate = item?.isNew == true -// media.downloaded = true - media.setIsDownloaded() -// 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(false) // enforce check - // check if file has chapters - 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 { - MediaMetadataRetrieverCompat().use { mmr -> - mmr.setDataSource(media.fileUrl) - durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) - if (durationStr != null) media.setDuration(durationStr!!.toInt()) - Logd(TAG, "Duration of file is " + media.getDuration()) - } - } catch (e: NumberFormatException) { - Logd(TAG, "Invalid file duration: $durationStr") - } catch (e: Exception) { - Log.e(TAG, "Get duration failed", e) - media.setDuration(30000) - } -// val item = media.episodeOrFetch() - item?.media = media - try { - // we've received the media, we don't want to autodownload it again - if (item != null) { - item = upsertBlk(item) { - it.disableAutoDownload() + val broadcastUnreadStateUpdate = item.isNew + item = upsertBlk(item) { + it.media?.setIsDownloaded() + it.media?.setfileUrlOrNull(request.destination) + if (request.destination != null) it.media?.size = File(request.destination).length() + it.media?.checkEmbeddedPicture(false) // enforce check + if (it.chapters.isEmpty()) it.media?.setChapters(ChapterUtils.loadChaptersFromMediaFile(it.media!!, context)) + if (it.podcastIndexChapterUrl != null) ChapterUtils.loadChaptersFromUrl(it.podcastIndexChapterUrl!!, false) + var durationStr: String? = null + try { + MediaMetadataRetrieverCompat().use { mmr -> + if (it.media != null) mmr.setDataSource(it.media!!.fileUrl) + durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + if (durationStr != null) it.media?.setDuration(durationStr!!.toInt()) } - EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item)) - Logd(TAG, "persisting episode downloaded ${item.title} ${item.media?.fileUrl} ${item.media?.downloaded} ${item.isNew}") - // setFeedItem() signals that the item has been updated, - // so we do it after the enclosing media has been updated above, - // to ensure subscribers will get the updated EpisodeMedia as well -// Episodes.persistEpisode(item) -// TODO: should use different event? - if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item)) + } catch (e: NumberFormatException) { + Logd(TAG, "Invalid file duration: $durationStr") + } catch (e: Exception) { + Log.e(TAG, "Get duration failed", e) + it.media?.setDuration(30000) } - } catch (e: InterruptedException) { - Log.e(TAG, "MediaHandlerThread was interrupted") - } catch (e: ExecutionException) { - Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message) - updatedStatus = DownloadResult(media.id, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"") + it.disableAutoDownload() } - if (needSynch() && item != null) { - val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD) - .currentTimestamp() - .build() + EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(item)) +// TODO: should use different event? + if (broadcastUnreadStateUpdate) EventFlow.postEvent(FlowEvent.EpisodePlayedEvent(item)) + if (needSynch()) { + Logd(TAG, "enqueue synch") + val action = EpisodeAction.Builder(item, EpisodeAction.DOWNLOAD).currentTimestamp().build() SynchronizationQueueSink.enqueueEpisodeActionIfSyncActive(context, action) } - } - - companion object { - private val TAG: String = MediaDownloadedHandler::class.simpleName ?: "Anonymous" + Logd(TAG, "media.episode.isNew: ${item.isNew} ${item.playState}") } } companion object { - private val TAG: String = EpisodeDownloadWorker::class.simpleName ?: "Anonymous" private val notificationProgress: MutableMap = HashMap() } } - } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt index 11d39eea..7bd940dd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt @@ -143,15 +143,13 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) { } catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) } - if (cancelled) { - onCancelled() - } else { + if (cancelled) onCancelled() + else { // check if size specified in the response header is the same as the size of the // written file. This check cannot be made if compression was used when { !isGzip && downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.soFar != downloadRequest.size -> { - onFail(DownloadError.ERROR_IO_WRONG_SIZE, - "Download completed but size: ${downloadRequest.soFar} does not equal expected size ${downloadRequest.size}") + onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: ${downloadRequest.soFar} does not equal expected size ${downloadRequest.size}") return } downloadRequest.size > 0 && downloadRequest.soFar == 0L -> { @@ -208,9 +206,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) { Log.e(TAG, e.toString()) if (e.message != null && e.message!!.contains("PROTOCOL_ERROR")) { // Apparently some servers announce they support SPDY but then actually don't. - httpClient = httpClient.newBuilder() - .protocols(listOf(Protocol.HTTP_1_1)) - .build() + httpClient = httpClient.newBuilder().protocols(listOf(Protocol.HTTP_1_1)).build() return httpClient.newCall(httpReq.build()).execute() } else { throw e diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt index 1b92dc40..2e991281 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/SyncService.kt @@ -289,8 +289,6 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont updatedItems.add(result.second) } removeFromQueue(*updatedItems.toTypedArray()) -// loadAdditionalFeedItemListData(updatedItems) -// persistEpisodes(updatedItems) runOnIOScope { for (episode in updatedItems) { upsert(episode) {} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt index 70e59d5b..b68c33b9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt @@ -10,6 +10,8 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.persistEpisode +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 import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded @@ -315,7 +317,7 @@ import kotlin.math.min override fun processEpisodeAction(action: EpisodeAction): Pair? { val guid = if (isValidGuid(action.guid)) action.guid else null - val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"") + var feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"", false) if (feedItem == null) { Logd(TAG, "Unknown feed item: $action") return null @@ -328,23 +330,23 @@ import kotlin.math.min var idRemove: Long? = null Logd(TAG, "processEpisodeAction ${feedItem.media!!.getLastPlayedTime()} ${(action.timestamp?.time?:0L)} ${action.position} ${feedItem.title}") if (feedItem.media!!.getLastPlayedTime() < (action.timestamp?.time?:0L)) { - feedItem.media!!.startPosition = action.started * 1000 - feedItem.media!!.setPosition(action.position * 1000) - feedItem.media!!.playedDuration = action.playedDuration * 1000 - feedItem.media!!.setLastPlayedTime(action.timestamp!!.time) - feedItem.isFavorite = action.isFavorite - feedItem.playState = action.playState - if (hasAlmostEnded(feedItem.media!!)) { - Logd(TAG, "Marking as played") - feedItem.setPlayed(true) - feedItem.media!!.setPosition(0) - idRemove = feedItem.id - } else Logd(TAG, "Setting position") -// persistFeedMediaPlaybackInfo(feedItem.media) - persistEpisode(feedItem) + feedItem = upsertBlk(feedItem) { + it.media!!.startPosition = action.started * 1000 + it.media!!.setPosition(action.position * 1000) + it.media!!.playedDuration = action.playedDuration * 1000 + it.media!!.setLastPlayedTime(action.timestamp!!.time) + it.isFavorite = action.isFavorite + it.playState = action.playState + if (hasAlmostEnded(it.media!!)) { + Logd(TAG, "Marking as played") + it.setPlayed(true) + it.media!!.setPosition(0) + idRemove = it.id + } else Logd(TAG, "Setting position") + } + EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(feedItem)) } else Logd(TAG, "local is newer, no change") - - return if (idRemove != null) Pair(idRemove, feedItem) else null + return if (idRemove != null) Pair(idRemove!!, feedItem) else null } override fun logout() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt index a6d2a363..b7adccb3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/InTheatre.kt @@ -48,12 +48,8 @@ object InTheatre { CoroutineScope(Dispatchers.IO).launch { Logd(TAG, "starting curQueue") var curQueue_ = realm.query(PlayQueue::class).sort("updated", Sort.DESCENDING).first().find() - if (curQueue_ != null) { - curQueue = curQueue_ -// curQueue.episodes.addAll(realm.copyFromRealm(realm.query(Episode::class, "id IN $0", curQueue.episodeIds) -// .find().sortedBy { curQueue.episodeIds.indexOf(it.id) })) - } else { - Logd(TAG, "creating new curQueue") + if (curQueue_ != null) curQueue = curQueue_ + else { for (i in 0..4) { curQueue_ = PlayQueue() if (i == 0) { @@ -72,7 +68,6 @@ object InTheatre { Logd(TAG, "starting curState") var curState_ = realm.query(CurrentState::class).first().find() -// if (curState_ != null) curState = unmanaged(curState_) if (curState_ != null) curState = curState_ else { Logd(TAG, "creating new curState") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 8e42c059..5c6d466d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -262,7 +262,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont */ @Synchronized protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int = Playable.INVALID_TIME) { - Logd(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus) + Log.d(TAG, "${this.javaClass.simpleName}: Setting player status to $newStatus") this.oldStatus = status status = newStatus if (newMedia != null) setPlayable(newMedia) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index 7abe6984..59110b11 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -307,7 +307,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP override fun resume() { if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - Logd(TAG, "Resuming/Starting playback") + Log.d(TAG, "Resuming/Starting playback") acquireWifiLockIfNecessary() setPlaybackParams(getCurrentPlaybackSpeed(curMedia), UserPreferences.isSkipSilence) setVolume(1.0f, 1.0f) @@ -646,13 +646,11 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP else -> bufferingUpdateListener?.accept(BUFFERING_ENDED) } } - override fun onIsPlayingChanged(isPlaying: Boolean) { val stat = if (isPlaying) PlayerStatus.PLAYING else PlayerStatus.PAUSED setPlayerStatus(stat, curMedia) - Logd(TAG, "onIsPlayingChanged $isPlaying") + Log.d(TAG, "onIsPlayingChanged $isPlaying") } - override fun onPlayerError(error: PlaybackException) { Logd(TAG, "onPlayerError ${error.message}") if (wasDownloadBlocked(error)) audioErrorListener?.accept(context.getString(R.string.download_error_blocked)) @@ -663,12 +661,10 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP audioErrorListener?.accept((if (cause != null) cause.message else error.message) ?:"no message") } } - override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) { Logd(TAG, "onPositionDiscontinuity $oldPosition $newPosition $reason") if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run() } - override fun onAudioSessionIdChanged(audioSessionId: Int) { Logd(TAG, "onAudioSessionIdChanged $audioSessionId") initLoudnessEnhancer(audioSessionId) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 29066f18..9bd8bfe8 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -136,9 +136,8 @@ class PlaybackService : MediaSessionService() { val status = intent.getStringExtra("media_connection_status") val isConnectedToCar = "media_connected" == status Logd(TAG, "Received Auto Connection update: $status") - if (!isConnectedToCar) { - Logd(TAG, "Car was unplugged during playback.") - } else { + if (!isConnectedToCar) Logd(TAG, "Car was unplugged during playback.") + else { val playerStatus = MediaPlayerBase.status when (playerStatus) { PlayerStatus.PAUSED, PlayerStatus.PREPARED -> mPlayer?.resume() @@ -229,8 +228,7 @@ class PlaybackService : MediaSessionService() { Log.d(TAG, "statusChanged called ${newInfo?.playerStatus}") if (newInfo != null) { when (newInfo.playerStatus) { - PlayerStatus.INITIALIZED -> - if (mPlayer != null) writeMediaPlaying(mPlayer!!.playerInfo.playable, mPlayer!!.playerInfo.playerStatus) + PlayerStatus.INITIALIZED -> if (mPlayer != null) writeMediaPlaying(mPlayer!!.playerInfo.playable, mPlayer!!.playerInfo.playerStatus) PlayerStatus.PREPARED -> { if (mPlayer != null) writeMediaPlaying(mPlayer!!.playerInfo.playable, mPlayer!!.playerInfo.playerStatus) if (newInfo.playable != null) taskManager.startChapterLoader(newInfo.playable!!) @@ -382,8 +380,7 @@ class PlaybackService : MediaSessionService() { val i = EpisodeUtil.indexOfItemWithId(eList, item.id) Logd(TAG, "getNextInQueue current i: $i curIndexInQueue: $curIndexInQueue") if (i < 0) { - if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) j = curIndexInQueue - else j = eList.size-1 + j = if (curIndexInQueue >= 0 && curIndexInQueue < eList.size) curIndexInQueue else eList.size-1 } else if (i < eList.size-1) j = i+1 Logd(TAG, "getNextInQueue next j: $j") @@ -411,17 +408,15 @@ class PlaybackService : MediaSessionService() { EventFlow.postEvent(FlowEvent.PlayEvent(nextItem)) return if (nextItem.media == null) null else unmanaged(nextItem.media!!) } - + // only used in test override fun findMedia(url: String): Playable? { val item = getEpisodeByGuidOrUrl(null, url) return item?.media } - override fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) { Logd(TAG, "onPlaybackEnded mediaType: $mediaType stopPlaying: $stopPlaying") clearCurTempSpeed() if (stopPlaying) taskManager.cancelPositionSaver() - if (mediaType == null) sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0) else sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, when { @@ -694,9 +689,15 @@ class PlaybackService : MediaSessionService() { val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false + val keyEvent: KeyEvent? = if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) { + intent?.getParcelableExtra(EXTRA_KEY_EVENT, KeyEvent::class.java) + } else { + @Suppress("DEPRECATION") + intent?.getParcelableExtra(EXTRA_KEY_EVENT) + } val playable = curMedia - Log.d(TAG, "onStartCommand flags=$flags startId=$startId keycode=$keycode customAction=$customAction hardwareButton=$hardwareButton action=${intent?.action.toString()} ${playable?.getEpisodeTitle()}") + Log.d(TAG, "onStartCommand flags=$flags startId=$startId keycode=$keycode keyEvent=$keyEvent customAction=$customAction hardwareButton=$hardwareButton action=${intent?.action.toString()} ${playable?.getEpisodeTitle()}") if (keycode == -1 && playable == null && customAction == null) { Log.e(TAG, "onStartCommand PlaybackService was started with no arguments, return") return START_NOT_STICKY @@ -797,7 +798,7 @@ class PlaybackService : MediaSessionService() { * Handles media button events. return: keycode was handled */ private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean { - Logd(TAG, "Handling keycode: $keycode") + Log.d(TAG, "Handling keycode: $keycode") val info = mPlayer?.playerInfo val status = info?.playerStatus when (keycode) { @@ -1064,12 +1065,6 @@ class PlaybackService : MediaSessionService() { playable.setLastPlayedTime(System.currentTimeMillis()) if (playable is EpisodeMedia) { -// 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) -// persistEpisode(item, true) - var item = realm.query(Episode::class, "id == ${playable.id}").first().find() if (item != null) { item = upsertBlk(item) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index c511fb82..baedef7d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -23,8 +23,6 @@ import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName import ac.mdiq.podcini.storage.utils.FilesUtils.getDataFolder import ac.mdiq.podcini.ui.activity.OpmlImportActivity import ac.mdiq.podcini.ui.activity.PreferenceActivity -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment -import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion import ac.mdiq.podcini.util.Logd import android.app.Activity.RESULT_OK import android.app.ProgressDialog @@ -934,29 +932,31 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { val action = readFromJsonObject(jsonAction) ?: continue Logd(TAG, "processing action: $action") val result = processEpisodeAction(action) ?: continue - upsertBlk(result.second) {} +// upsertBlk(result.second) {} } } private fun processEpisodeAction(action: EpisodeAction): Pair? { val guid = if (isValidGuid(action.guid)) action.guid else null - val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"") ?: return null + var feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"", false) ?: return null if (feedItem.media == null) { Logd(TAG, "Feed item has no media: $action") return null } var idRemove = 0L - feedItem.media!!.startPosition = action.started * 1000 - feedItem.media!!.setPosition(action.position * 1000) - feedItem.media!!.playedDuration = action.playedDuration * 1000 - feedItem.media!!.setLastPlayedTime(action.timestamp!!.time) - feedItem.isFavorite = action.isFavorite - feedItem.playState = action.playState - if (hasAlmostEnded(feedItem.media!!)) { - Logd(TAG, "Marking as played: $action") - feedItem.setPlayed(true) - feedItem.media!!.setPosition(0) - idRemove = feedItem.id - } else Logd(TAG, "Setting position: $action") + feedItem = upsertBlk(feedItem) { + it.media!!.startPosition = action.started * 1000 + it.media!!.setPosition(action.position * 1000) + it.media!!.playedDuration = action.playedDuration * 1000 + it.media!!.setLastPlayedTime(action.timestamp!!.time) + it.isFavorite = action.isFavorite + it.playState = action.playState + if (hasAlmostEnded(it.media!!)) { + Logd(TAG, "Marking as played: $action") + it.setPlayed(true) + it.media!!.setPosition(0) + idRemove = it.id + } else Logd(TAG, "Setting position: $action") + } return Pair(idRemove, feedItem) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 858084ce..6e8115db 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -80,16 +80,18 @@ object Episodes { * @return The FeedItem or null if the FeedItem could not be found. * Does NOT load additional attributes like feed or queue state. */ - fun getEpisodeByGuidOrUrl(guid: String?, episodeUrl: String): Episode? { + fun getEpisodeByGuidOrUrl(guid: String?, episodeUrl: String, copy: Boolean = true): Episode? { Logd(TAG, "getEpisodeByGuidOrUrl called $guid $episodeUrl") val episode = if (guid != null) realm.query(Episode::class).query("identifier == $0", guid).first().find() else realm.query(Episode::class).query("media.downloadUrl == $0", episodeUrl).first().find() + if (!copy) return episode return if (episode != null) realm.copyFromRealm(episode) else null } - fun getEpisodeMedia(mediaId: Long): EpisodeMedia? { + fun getEpisodeMedia(mediaId: Long, copy: Boolean = true): EpisodeMedia? { Logd(TAG, "getEpisodeMedia called $mediaId") val media = realm.query(EpisodeMedia::class).query("id == $0", mediaId).first().find() + if (!copy) return media return if (media != null) realm.copyFromRealm(media) else null } @@ -217,6 +219,7 @@ object Episodes { // } // } +// only used in tests fun persistEpisodeMedia(media: EpisodeMedia) : Job { Logd(TAG, "persistEpisodeMedia called") return runOnIOScope { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index df9b350b..fc992f5e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -148,15 +148,14 @@ object Queues { suspend fun addToQueueSync(markAsUnplayed: Boolean, episode: Episode, queue_: PlayQueue? = null) { Logd(TAG, "addToQueueSync( ... ) called") - val queue = queue_ ?: curQueue + if (queue.episodeIds.contains(episode.id)) return + val currentlyPlaying = curMedia val positionCalculator = EnqueuePositionPolicy(enqueueLocation) var insertPosition = positionCalculator.calcPosition(queue.episodes, currentlyPlaying) Logd(TAG, "addToQueueSync insertPosition: $insertPosition") - if (queue.episodeIds.contains(episode.id)) return - val queueNew = upsert(queue) { if (!it.episodeIds.contains(episode.id)) it.episodeIds.add(insertPosition, episode.id) insertPosition++ diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt index f3cb5600..58694c31 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DownloadLogDetailsDialog.kt @@ -1,11 +1,11 @@ package ac.mdiq.podcini.ui.dialog import ac.mdiq.podcini.R -import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.Feeds.getFeed +import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.DownloadResult -import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.EpisodeMedia +import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.util.error.DownloadErrorLabel.from import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent @@ -23,7 +23,7 @@ class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : Mater var url = "unknown" when (status.feedfileType) { EpisodeMedia.FEEDFILETYPE_FEEDMEDIA -> { - val media = getEpisodeMedia(status.feedfileId) + val media = realm.query(EpisodeMedia::class).query("id == $0", status.feedfileId).first().find() if (media != null) url = media.downloadUrl?:"" } Feed.FEEDFILETYPE_FEED -> { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt index 50d123b3..f3da23f0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadLogFragment.kt @@ -5,13 +5,14 @@ import ac.mdiq.podcini.databinding.DownloadLogFragmentBinding import ac.mdiq.podcini.databinding.DownloadlogItemBinding import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.feed.FeedUpdateManager -import ac.mdiq.podcini.storage.database.Episodes.getEpisodeMedia import ac.mdiq.podcini.storage.database.Feeds.getFeed import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.model.DownloadResult +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.storage.utils.DownloadResultComparator import ac.mdiq.podcini.ui.actions.actionbutton.DownloadActionButton import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.dialog.DownloadLogDetailsDialog @@ -21,7 +22,6 @@ import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.error.DownloadErrorLabel import ac.mdiq.podcini.util.event.EventFlow import ac.mdiq.podcini.util.event.FlowEvent -import ac.mdiq.podcini.storage.utils.DownloadResultComparator import android.app.Activity import android.content.Context import android.os.Bundle @@ -237,17 +237,12 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To }) } EpisodeMedia.FEEDFILETYPE_FEEDMEDIA -> { - holder.secondaryActionButton.setOnClickListener(View.OnClickListener { + holder.secondaryActionButton.setOnClickListener { holder.secondaryActionButton.visibility = View.INVISIBLE - val media: EpisodeMedia? = getEpisodeMedia(status.feedfileId) - if (media == null) { - Log.e(TAG, "Could not find feed media for feed id: " + status.feedfileId) - return@OnClickListener - } - val item_ = media.episodeOrFetch() + val item_ = realm.query(Episode::class).query("id == $0", status.feedfileId).first().find() if (item_ != null) DownloadActionButton(item_).onClick(context) (context as MainActivity).showSnackbarAbovePlayer(R.string.status_downloading_label, Toast.LENGTH_SHORT) - }) + } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index 51d432dd..531bfdd1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -136,7 +136,7 @@ import java.util.* }) speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem -> adapter.selectedItems.let { - EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(it.filterIsInstance()) + EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(it) } adapter.endSelectMode() true diff --git a/build.gradle b/build.gradle index a59d2b1e..1f03c6a9 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { mavenCentral() gradlePluginPortal() } - ext.kotlin_version = '2.0.0' + ext.kotlin_version = '2.0.10' dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.android.tools.build:gradle:8.5.2' diff --git a/changelog.md b/changelog.md index 37dd1e68..58f1636a 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +# 6.3.7 + +* 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 + # 6.3.6 * upgraded gradle to 8.9 and Android Gradle Plugin to 8.5.2 diff --git a/fastlane/metadata/android/en-US/changelogs/3020231.txt b/fastlane/metadata/android/en-US/changelogs/3020231.txt new file mode 100644 index 00000000..0e3f254f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020231.txt @@ -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