6.0.3 commit
This commit is contained in:
parent
85407855d7
commit
1afc48290d
78
.tx/config
78
.tx/config
|
@ -2,46 +2,46 @@
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[o:podcini:p:podcini:r:core-values]
|
[o:podcini:p:podcini:r:core-values]
|
||||||
file_filter = ui/i18n/src/main/res/values-<lang>/strings.xml
|
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||||
source_file = ui/i18n/src/main/res/values/strings.xml
|
source_file = app/src/main/res/values/strings.xml
|
||||||
source_lang = en
|
source_lang = en
|
||||||
trans.ar = ui/i18n/src/main/res/values-ar/strings.xml
|
trans.ar = app/src/main/res/values-ar/strings.xml
|
||||||
trans.ast_ES = ui/i18n/src/main/res/values-ast/strings.xml
|
trans.ast_ES = app/src/main/res/values-ast/strings.xml
|
||||||
trans.br = ui/i18n/src/main/res/values-br/strings.xml
|
trans.br = app/src/main/res/values-br/strings.xml
|
||||||
trans.ca = ui/i18n/src/main/res/values-ca/strings.xml
|
trans.ca = app/src/main/res/values-ca/strings.xml
|
||||||
trans.cs_CZ = ui/i18n/src/main/res/values-cs/strings.xml
|
trans.cs_CZ = app/src/main/res/values-cs/strings.xml
|
||||||
trans.da = ui/i18n/src/main/res/values-da/strings.xml
|
trans.da = app/src/main/res/values-da/strings.xml
|
||||||
trans.de = ui/i18n/src/main/res/values-de/strings.xml
|
trans.de = app/src/main/res/values-de/strings.xml
|
||||||
trans.es = ui/i18n/src/main/res/values-es/strings.xml
|
trans.es = app/src/main/res/values-es/strings.xml
|
||||||
trans.et = ui/i18n/src/main/res/values-et/strings.xml
|
trans.et = app/src/main/res/values-et/strings.xml
|
||||||
trans.eu = ui/i18n/src/main/res/values-eu/strings.xml
|
trans.eu = app/src/main/res/values-eu/strings.xml
|
||||||
trans.fa = ui/i18n/src/main/res/values-fa/strings.xml
|
trans.fa = app/src/main/res/values-fa/strings.xml
|
||||||
trans.fi = ui/i18n/src/main/res/values-fi/strings.xml
|
trans.fi = app/src/main/res/values-fi/strings.xml
|
||||||
trans.fr = ui/i18n/src/main/res/values-fr/strings.xml
|
trans.fr = app/src/main/res/values-fr/strings.xml
|
||||||
trans.gl = ui/i18n/src/main/res/values-gl/strings.xml
|
trans.gl = app/src/main/res/values-gl/strings.xml
|
||||||
trans.he_IL = ui/i18n/src/main/res/values-iw/strings.xml
|
trans.he_IL = app/src/main/res/values-iw/strings.xml
|
||||||
trans.hi_IN = ui/i18n/src/main/res/values-hi/strings.xml
|
trans.hi_IN = app/src/main/res/values-hi/strings.xml
|
||||||
trans.hu = ui/i18n/src/main/res/values-hu/strings.xml
|
trans.hu = app/src/main/res/values-hu/strings.xml
|
||||||
trans.id = ui/i18n/src/main/res/values-in/strings.xml
|
trans.id = app/src/main/res/values-in/strings.xml
|
||||||
trans.it_IT = ui/i18n/src/main/res/values-it/strings.xml
|
trans.it_IT = app/src/main/res/values-it/strings.xml
|
||||||
trans.ja = ui/i18n/src/main/res/values-ja/strings.xml
|
trans.ja = app/src/main/res/values-ja/strings.xml
|
||||||
trans.ko = ui/i18n/src/main/res/values-ko/strings.xml
|
trans.ko = app/src/main/res/values-ko/strings.xml
|
||||||
trans.lt = ui/i18n/src/main/res/values-lt/strings.xml
|
trans.lt = app/src/main/res/values-lt/strings.xml
|
||||||
trans.nb_NO = ui/i18n/src/main/res/values-nb/strings.xml
|
trans.nb_NO = app/src/main/res/values-nb/strings.xml
|
||||||
trans.nl = ui/i18n/src/main/res/values-nl/strings.xml
|
trans.nl = app/src/main/res/values-nl/strings.xml
|
||||||
trans.pl_PL = ui/i18n/src/main/res/values-pl/strings.xml
|
trans.pl_PL = app/src/main/res/values-pl/strings.xml
|
||||||
trans.pt = ui/i18n/src/main/res/values-pt/strings.xml
|
trans.pt = app/src/main/res/values-pt/strings.xml
|
||||||
trans.pt_BR = ui/i18n/src/main/res/values-pt-rBR/strings.xml
|
trans.pt_BR = app/src/main/res/values-pt-rBR/strings.xml
|
||||||
trans.ro_RO = ui/i18n/src/main/res/values-ro/strings.xml
|
trans.ro_RO = app/src/main/res/values-ro/strings.xml
|
||||||
trans.ru_RU = ui/i18n/src/main/res/values-ru/strings.xml
|
trans.ru_RU = app/src/main/res/values-ru/strings.xml
|
||||||
trans.sk = ui/i18n/src/main/res/values-sk/strings.xml
|
trans.sk = app/src/main/res/values-sk/strings.xml
|
||||||
trans.sl_SI = ui/i18n/src/main/res/values-sl/strings.xml
|
trans.sl_SI = app/src/main/res/values-sl/strings.xml
|
||||||
trans.sv_SE = ui/i18n/src/main/res/values-sv/strings.xml
|
trans.sv_SE = app/src/main/res/values-sv/strings.xml
|
||||||
trans.tr = ui/i18n/src/main/res/values-tr/strings.xml
|
trans.tr = app/src/main/res/values-tr/strings.xml
|
||||||
trans.uk_UA = ui/i18n/src/main/res/values-uk/strings.xml
|
trans.uk_UA = app/src/main/res/values-uk/strings.xml
|
||||||
trans.zh_CN = ui/i18n/src/main/res/values-zh-rCN/strings.xml
|
trans.zh_CN = app/src/main/res/values-zh-rCN/strings.xml
|
||||||
trans.zh_HK = ui/i18n/src/main/res/values-zh-rHK/strings.xml
|
trans.zh_HK = app/src/main/res/values-zh-rHK/strings.xml
|
||||||
trans.zh_TW = ui/i18n/src/main/res/values-zh-rTW/strings.xml
|
trans.zh_TW = app/src/main/res/values-zh-rTW/strings.xml
|
||||||
|
|
||||||
[o:podcini:p:podcini:r:description]
|
[o:podcini:p:podcini:r:description]
|
||||||
file_filter = app/src/main/play/listings/<lang>/full-description.txt
|
file_filter = app/src/main/play/listings/<lang>/full-description.txt
|
||||||
|
|
|
@ -24,7 +24,7 @@ How to submit a feature request
|
||||||
Translating Podcini
|
Translating Podcini
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
If you would like to translate the app into another language or improve an existing translation, you can visit the [Transifex project page](to be announced/). From there, you can either join a language team if it already exists or create a new language team.
|
If you would like to translate the app into another language or improve an existing translation, you can visit the [Transifex project page](https://app.transifex.com/xilinjia/podcini/dashboard/). From there, you can either join a language team if it already exists or create a new language team.
|
||||||
|
|
||||||
Submit a pull request
|
Submit a pull request
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
@ -125,8 +125,8 @@ android {
|
||||||
buildConfig true
|
buildConfig true
|
||||||
}
|
}
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
versionCode 3020202
|
versionCode 3020203
|
||||||
versionName "6.0.2"
|
versionName "6.0.3"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
|
|
@ -2,8 +2,8 @@ package de.test.podcini.service.playback
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerCallback
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||||
|
|
||||||
class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback {
|
class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback {
|
||||||
private var isCancelled = false
|
private var isCancelled = false
|
||||||
|
|
|
@ -2,8 +2,8 @@ package de.test.podcini.service.playback
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerCallback
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||||
|
|
||||||
open class DefaultMediaPlayerCallback : MediaPlayerCallback {
|
open class DefaultMediaPlayerCallback : MediaPlayerCallback {
|
||||||
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
||||||
|
|
|
@ -100,7 +100,7 @@ class MediaPlayerBaseTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun writeTestPlayable(downloadUrl: String?, fileUrl: String?): Playable {
|
private fun writeTestPlayable(downloadUrl: String?, fileUrl: String?): Playable {
|
||||||
val f = Feed(0, null, "f", "l", "d", null, null, null, null, "i", null, null, "l", false)
|
val f = Feed(0, null, "f", "l", "d", null, null, null, null, "i", null, null, "l")
|
||||||
val prefs = FeedPreferences(f.id, false, FeedPreferences.AutoDeleteAction.NEVER,
|
val prefs = FeedPreferences(f.id, false, FeedPreferences.AutoDeleteAction.NEVER,
|
||||||
VolumeAdaptionSetting.OFF, null, null)
|
VolumeAdaptionSetting.OFF, null, null)
|
||||||
f.preferences = prefs
|
f.preferences = prefs
|
||||||
|
|
|
@ -62,7 +62,7 @@ class TaskManagerTest {
|
||||||
|
|
||||||
private fun writeTestQueue(pref: String): List<Episode>? {
|
private fun writeTestQueue(pref: String): List<Episode>? {
|
||||||
val NUM_ITEMS = 10
|
val NUM_ITEMS = 10
|
||||||
val f = Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url", false)
|
val f = Feed(0, null, "title", "link", "d", null, null, null, null, "id", null, "null", "url")
|
||||||
f.episodes.clear()
|
f.episodes.clear()
|
||||||
for (i in 0 until NUM_ITEMS) {
|
for (i in 0 until NUM_ITEMS) {
|
||||||
f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PLAYED, f))
|
f.episodes.add(Episode(0, pref + i, pref + i, "link", Date(), Episode.PLAYED, f))
|
||||||
|
|
|
@ -4,8 +4,8 @@ import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
||||||
import ac.mdiq.podcini.storage.database.Episodes
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.downloadAlgorithm
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
@ -47,7 +47,7 @@ class AutoDownloadTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
// setDownloadAlgorithm(Episodes.AutomaticDownloadAlgorithm())
|
// setDownloadAlgorithm(Episodes.AutomaticDownloadAlgorithm())
|
||||||
downloadAlgorithm = Episodes.AutomaticDownloadAlgorithm()
|
downloadAlgorithm = AutoDownloads.AutoDownloadAlgorithm()
|
||||||
EspressoTestUtils.tryKillPlaybackService()
|
EspressoTestUtils.tryKillPlaybackService()
|
||||||
stubFeedsServer!!.tearDown()
|
stubFeedsServer!!.tearDown()
|
||||||
}
|
}
|
||||||
|
@ -103,11 +103,11 @@ class AutoDownloadTest {
|
||||||
// .until { item.media!!.id == currentlyPlayingFeedMediaId }
|
// .until { item.media!!.id == currentlyPlayingFeedMediaId }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class StubDownloadAlgorithm : Episodes.AutomaticDownloadAlgorithm() {
|
private class StubDownloadAlgorithm : AutoDownloads.AutoDownloadAlgorithm() {
|
||||||
var currentlyPlayingAtDownload: Long = -1
|
var currentlyPlayingAtDownload: Long = -1
|
||||||
private set
|
private set
|
||||||
|
|
||||||
override fun autoDownloadEpisodeMedia(context: Context): Runnable? {
|
override fun autoDownloadEpisodeMedia(context: Context): Runnable {
|
||||||
return Runnable {
|
return Runnable {
|
||||||
if (currentlyPlayingAtDownload == -1L) {
|
if (currentlyPlayingAtDownload == -1L) {
|
||||||
// currentlyPlayingAtDownload = currentlyPlayingFeedMediaId
|
// currentlyPlayingAtDownload = currentlyPlayingFeedMediaId
|
||||||
|
|
|
@ -12,11 +12,11 @@ import androidx.test.filters.LargeTest
|
||||||
import androidx.test.rule.ActivityTestRule
|
import androidx.test.rule.ActivityTestRule
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APCleanupAlgorithm
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APCleanupAlgorithm
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APNullCleanupAlgorithm
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APNullCleanupAlgorithm
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APQueueCleanupAlgorithm
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APQueueCleanupAlgorithm
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.build
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.build
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.ExceptFavoriteCleanupAlgorithm
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.ExceptFavoriteCleanupAlgorithm
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
|
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
||||||
|
|
|
@ -107,7 +107,7 @@ class UITestUtils(private val context: Context) {
|
||||||
for (i in 0 until NUM_FEEDS) {
|
for (i in 0 until NUM_FEEDS) {
|
||||||
val feed = Feed(0, null, "Title $i", "http://example.com/$i", "Description of feed $i",
|
val feed = Feed(0, null, "Title $i", "http://example.com/$i", "Description of feed $i",
|
||||||
"http://example.com/pay/feed$i", "author $i", "en", Feed.TYPE_RSS2, "feed$i", null, null,
|
"http://example.com/pay/feed$i", "author $i", "en", Feed.TYPE_RSS2, "feed$i", null, null,
|
||||||
"http://example.com/feed/src/$i", false)
|
"http://example.com/feed/src/$i")
|
||||||
|
|
||||||
// create items
|
// create items
|
||||||
val items: MutableList<Episode> = ArrayList()
|
val items: MutableList<Episode> = ArrayList()
|
||||||
|
@ -147,11 +147,8 @@ class UITestUtils(private val context: Context) {
|
||||||
/**
|
/**
|
||||||
* Adds feeds, images and episodes to the local database. This method will also call addHostedFeedData if it has not
|
* Adds feeds, images and episodes to the local database. This method will also call addHostedFeedData if it has not
|
||||||
* been called yet.
|
* been called yet.
|
||||||
*
|
|
||||||
* Adds one item of each feed to the queue and to the playback history.
|
* Adds one item of each feed to the queue and to the playback history.
|
||||||
*
|
|
||||||
* This method should NOT be called if the testing class wants to download the hosted feed data.
|
* This method should NOT be called if the testing class wants to download the hosted feed data.
|
||||||
*
|
|
||||||
* @param downloadEpisodes true if episodes should also be marked as downloaded.
|
* @param downloadEpisodes true if episodes should also be marked as downloaded.
|
||||||
*/
|
*/
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
|
@ -161,13 +158,10 @@ class UITestUtils(private val context: Context) {
|
||||||
// might be a flaky test, this is actually not that severe
|
// might be a flaky test, this is actually not that severe
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!feedDataHosted) {
|
if (!feedDataHosted) addHostedFeedData()
|
||||||
addHostedFeedData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val queue: MutableList<Episode> = ArrayList()
|
val queue: MutableList<Episode> = ArrayList()
|
||||||
for (feed in hostedFeeds) {
|
for (feed in hostedFeeds) {
|
||||||
feed.downloaded = (true)
|
|
||||||
if (downloadEpisodes) {
|
if (downloadEpisodes) {
|
||||||
for (item in feed.episodes) {
|
for (item in feed.episodes) {
|
||||||
if (item.media != null) {
|
if (item.media != null) {
|
||||||
|
@ -191,7 +185,7 @@ class UITestUtils(private val context: Context) {
|
||||||
// adapter.setCompleteFeed(*hostedFeeds.toTypedArray<Feed>())
|
// adapter.setCompleteFeed(*hostedFeeds.toTypedArray<Feed>())
|
||||||
// adapter.setQueue(queue)
|
// adapter.setQueue(queue)
|
||||||
// adapter.close()
|
// adapter.close()
|
||||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(hostedFeeds))
|
// EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.UNKNOWN, hostedFeeds))
|
||||||
EventFlow.postEvent(FlowEvent.QueueEvent.setQueue(queue))
|
EventFlow.postEvent(FlowEvent.QueueEvent.setQueue(queue))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.playback.cast
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerCallback
|
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub implementation of CastPsmp for Free build flavour
|
* Stub implementation of CastPsmp for Free build flavour
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:backupAgent=".storage.backup.OpmlBackupAgent"
|
android:backupAgent=".preferences.OpmlBackupAgent"
|
||||||
android:restoreAnyVersion="true"
|
android:restoreAnyVersion="true"
|
||||||
android:theme="@style/Theme.Podcini.Splash"
|
android:theme="@style/Theme.Podcini.Splash"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.preferences.PreferenceUpgrader
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.ui.activity.SplashActivity
|
import ac.mdiq.podcini.ui.activity.SplashActivity
|
||||||
import ac.mdiq.podcini.util.SPAUtil
|
import ac.mdiq.podcini.util.SPAUtil
|
||||||
import ac.mdiq.podcini.util.config.ApplicationCallbacksImpl
|
import ac.mdiq.podcini.util.config.ApplicationCallbacks
|
||||||
import ac.mdiq.podcini.util.config.ClientConfig
|
import ac.mdiq.podcini.util.config.ClientConfig
|
||||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||||
import ac.mdiq.podcini.util.error.CrashReportWriter
|
import ac.mdiq.podcini.util.error.CrashReportWriter
|
||||||
|
@ -49,6 +49,12 @@ class PodciniApp : Application() {
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ApplicationCallbacksImpl : ApplicationCallbacks {
|
||||||
|
override fun getApplicationInstance(): Application {
|
||||||
|
return PodciniApp.getInstance()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private lateinit var singleton: PodciniApp
|
private lateinit var singleton: PodciniApp
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.autodownloadEpisodeMedia
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
|
|
|
@ -152,7 +152,7 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
||||||
feed.downloadUrl != null -> prepareUrl(feed.downloadUrl!!)
|
feed.downloadUrl != null -> prepareUrl(feed.downloadUrl!!)
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
this.title = feed.getHumanReadableIdentifier()
|
this.title = feed.getTextIdentifier()
|
||||||
this.feedfileId = feed.id
|
this.feedfileId = feed.id
|
||||||
this.feedfileType = feed.getTypeAsInt()
|
this.feedfileType = feed.getTypeAsInt()
|
||||||
arguments.putInt(REQUEST_ARG_PAGE_NR, feed.pageNr)
|
arguments.putInt(REQUEST_ARG_PAGE_NR, feed.pageNr)
|
||||||
|
|
|
@ -14,6 +14,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isFeedRefreshAllowed
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable
|
import ac.mdiq.podcini.net.utils.NetworkUtils.networkAvailable
|
||||||
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.database.Episodes
|
import ac.mdiq.podcini.storage.database.Episodes
|
||||||
import ac.mdiq.podcini.storage.database.Feeds
|
import ac.mdiq.podcini.storage.database.Feeds
|
||||||
import ac.mdiq.podcini.storage.database.LogsAndStats
|
import ac.mdiq.podcini.storage.database.LogsAndStats
|
||||||
|
@ -23,7 +24,6 @@ import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||||
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
||||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||||
import ac.mdiq.podcini.util.error.InvalidFeedException
|
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
@ -129,14 +129,11 @@ object FeedUpdateManager {
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||||
// private val newEpisodesNotification = NewEpisodesNotification()
|
|
||||||
private val notificationManager = NotificationManagerCompat.from(context)
|
private val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
ClientConfigurator.initialize(applicationContext)
|
ClientConfigurator.initialize(applicationContext)
|
||||||
// newEpisodesNotification.loadCountersBeforeRefresh()
|
|
||||||
|
|
||||||
val toUpdate: MutableList<Feed>
|
val toUpdate: MutableList<Feed>
|
||||||
val feedId = inputData.getLong(EXTRA_FEED_ID, -1L)
|
val feedId = inputData.getLong(EXTRA_FEED_ID, -1L)
|
||||||
var allAreLocal = true
|
var allAreLocal = true
|
||||||
|
@ -158,7 +155,6 @@ object FeedUpdateManager {
|
||||||
toUpdate.add(feed) // Needs to be updatable, so no singletonList
|
toUpdate.add(feed) // Needs to be updatable, so no singletonList
|
||||||
force = true
|
force = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputData.getBoolean(EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) {
|
if (!inputData.getBoolean(EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) {
|
||||||
if (!networkAvailable() || !isFeedRefreshAllowed) {
|
if (!networkAvailable() || !isFeedRefreshAllowed) {
|
||||||
Logd(TAG, "Blocking automatic update")
|
Logd(TAG, "Blocking automatic update")
|
||||||
|
@ -166,12 +162,10 @@ object FeedUpdateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshFeeds(toUpdate, force)
|
refreshFeeds(toUpdate, force)
|
||||||
|
|
||||||
notificationManager.cancel(R.id.notification_updating_feeds)
|
notificationManager.cancel(R.id.notification_updating_feeds)
|
||||||
Episodes.autodownloadEpisodeMedia(applicationContext)
|
autodownloadEpisodeMedia(applicationContext)
|
||||||
return Result.success()
|
return Result.success()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotification(toUpdate: List<Feed?>?): Notification {
|
private fun createNotification(toUpdate: List<Feed?>?): Notification {
|
||||||
val context = applicationContext
|
val context = applicationContext
|
||||||
var contentText = ""
|
var contentText = ""
|
||||||
|
@ -190,11 +184,9 @@ object FeedUpdateManager {
|
||||||
.addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
|
.addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
|
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
|
||||||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
private fun refreshFeeds(toUpdate: MutableList<Feed>, force: Boolean) {
|
private fun refreshFeeds(toUpdate: MutableList<Feed>, force: Boolean) {
|
||||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this.applicationContext,
|
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this.applicationContext,
|
||||||
|
@ -211,10 +203,8 @@ object FeedUpdateManager {
|
||||||
// Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
// Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
while (toUpdate.isNotEmpty()) {
|
while (toUpdate.isNotEmpty()) {
|
||||||
if (isStopped) return
|
if (isStopped) return
|
||||||
|
|
||||||
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
||||||
val feed = unmanagedCopy(toUpdate[0])
|
val feed = unmanagedCopy(toUpdate[0])
|
||||||
try {
|
try {
|
||||||
|
@ -230,18 +220,15 @@ object FeedUpdateManager {
|
||||||
toUpdate.removeAt(0)
|
toUpdate.removeAt(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun refreshFeed(feed: Feed, force: Boolean) {
|
fun refreshFeed(feed: Feed, force: Boolean) {
|
||||||
val nextPage = (inputData.getBoolean(EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
|
val nextPage = (inputData.getBoolean(EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
|
||||||
if (nextPage) feed.pageNr += 1
|
if (nextPage) feed.pageNr += 1
|
||||||
|
|
||||||
val builder = create(feed)
|
val builder = create(feed)
|
||||||
builder.setForce(force || feed.lastUpdateFailed)
|
builder.setForce(force || feed.lastUpdateFailed)
|
||||||
if (nextPage) builder.source = feed.nextPageLink
|
if (nextPage) builder.source = feed.nextPageLink
|
||||||
val request = builder.build()
|
val request = builder.build()
|
||||||
|
|
||||||
val downloader = DefaultDownloaderFactory().create(request) ?: throw Exception("Unable to create downloader")
|
val downloader = DefaultDownloaderFactory().create(request) ?: throw Exception("Unable to create downloader")
|
||||||
downloader.call()
|
downloader.call()
|
||||||
if (!downloader.result.isSuccessful) {
|
if (!downloader.result.isSuccessful) {
|
||||||
|
@ -251,24 +238,18 @@ object FeedUpdateManager {
|
||||||
LogsAndStats.addDownloadStatus(downloader.result)
|
LogsAndStats.addDownloadStatus(downloader.result)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val feedSyncTask = FeedSyncTask(applicationContext, request)
|
val feedSyncTask = FeedSyncTask(applicationContext, request)
|
||||||
val success = feedSyncTask.run()
|
val success = feedSyncTask.run()
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
Logd(TAG, "update failed: unsuccessful")
|
Logd(TAG, "update failed: unsuccessful")
|
||||||
Feeds.persistFeedLastUpdateFailed(feed, true)
|
Feeds.persistFeedLastUpdateFailed(feed, true)
|
||||||
LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.feedfileId == null) return // No download logs for new subscriptions
|
if (request.feedfileId == null) return // No download logs for new subscriptions
|
||||||
|
|
||||||
// we create a 'successful' download log if the feed's last refresh failed
|
// we create a 'successful' download log if the feed's last refresh failed
|
||||||
val log = LogsAndStats.getFeedDownloadLog(request.feedfileId)
|
val log = LogsAndStats.getFeedDownloadLog(request.feedfileId)
|
||||||
if (log.isNotEmpty() && !log[0].isSuccessful) LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
if (log.isNotEmpty() && !log[0].isSuccessful) LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||||
|
|
||||||
// newEpisodesNotification.showIfNeeded(applicationContext, feedSyncTask.savedFeed!!)
|
|
||||||
if (!request.source.isNullOrEmpty()) {
|
if (!request.source.isNullOrEmpty()) {
|
||||||
when {
|
when {
|
||||||
!downloader.permanentRedirectUrl.isNullOrEmpty() -> Feeds.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
|
!downloader.permanentRedirectUrl.isNullOrEmpty() -> Feeds.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
|
||||||
|
@ -289,20 +270,16 @@ object FeedUpdateManager {
|
||||||
DownloadError.ERROR_REQUEST_ERROR, Date(), "Unknown error: Status not set")
|
DownloadError.ERROR_REQUEST_ERROR, Date(), "Unknown error: Status not set")
|
||||||
}
|
}
|
||||||
override fun call(): FeedHandlerResult? {
|
override fun call(): FeedHandlerResult? {
|
||||||
Logd(TAG, "in call()")
|
Logd(TAG, "in FeedParserTask call()")
|
||||||
val feed = Feed(request.source, request.lastModified)
|
val feed = Feed(request.source, request.lastModified)
|
||||||
feed.fileUrl = request.destination
|
feed.fileUrl = request.destination
|
||||||
feed.id = request.feedfileId
|
feed.id = request.feedfileId
|
||||||
feed.downloaded = true
|
|
||||||
if (feed.preferences == null) feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL,
|
if (feed.preferences == null) feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL,
|
||||||
VolumeAdaptionSetting.OFF, request.username, request.password)
|
VolumeAdaptionSetting.OFF, request.username, request.password)
|
||||||
|
|
||||||
if (request.arguments != null) feed.pageNr = request.arguments.getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)
|
if (request.arguments != null) feed.pageNr = request.arguments.getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)
|
||||||
|
|
||||||
var reason: DownloadError? = null
|
var reason: DownloadError? = null
|
||||||
var reasonDetailed: String? = null
|
var reasonDetailed: String? = null
|
||||||
val feedHandler = FeedHandler()
|
val feedHandler = FeedHandler()
|
||||||
|
|
||||||
var result: FeedHandlerResult? = null
|
var result: FeedHandlerResult? = null
|
||||||
try {
|
try {
|
||||||
result = feedHandler.parseFeed(feed)
|
result = feedHandler.parseFeed(feed)
|
||||||
|
@ -344,31 +321,33 @@ object FeedUpdateManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isSuccessful) {
|
if (isSuccessful) {
|
||||||
downloadStatus = DownloadResult(feed.id, feed.getHumanReadableIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
|
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
|
||||||
return result
|
return result
|
||||||
} else {
|
} else {
|
||||||
downloadStatus = DownloadResult(feed.id, feed.getHumanReadableIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND,
|
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND, isSuccessful, reasonDetailed?:"")
|
||||||
isSuccessful, reasonDetailed?:"")
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the feed was parsed correctly.
|
* Checks if the feed was parsed correctly.
|
||||||
*/
|
*/
|
||||||
@Throws(InvalidFeedException::class)
|
@Throws(InvalidFeedException::class)
|
||||||
private fun checkFeedData(feed: Feed) {
|
private fun checkFeedData(feed: Feed) {
|
||||||
if (feed.title == null) throw InvalidFeedException("Feed has no title")
|
if (feed.title == null) throw InvalidFeedException("Feed has no title")
|
||||||
checkFeedItems(feed)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(InvalidFeedException::class)
|
|
||||||
private fun checkFeedItems(feed: Feed) {
|
|
||||||
for (item in feed.episodes) {
|
for (item in feed.episodes) {
|
||||||
if (item.title == null) throw InvalidFeedException("Item has no title: $item")
|
if (item.title == null) throw InvalidFeedException("Item has no title: $item")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown if a feed has invalid attribute values.
|
||||||
|
*/
|
||||||
|
class InvalidFeedException(message: String?) : Exception(message) {
|
||||||
|
companion object {
|
||||||
|
private const val serialVersionUID = 1L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = FeedParserTask::class.simpleName ?: "Anonymous"
|
private val TAG: String = FeedParserTask::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
@ -379,6 +358,10 @@ object FeedUpdateManager {
|
||||||
private set
|
private set
|
||||||
private val task = FeedParserTask(request)
|
private val task = FeedParserTask(request)
|
||||||
private var feedHandlerResult: FeedHandlerResult? = null
|
private var feedHandlerResult: FeedHandlerResult? = null
|
||||||
|
val downloadStatus: DownloadResult
|
||||||
|
get() = task.downloadStatus
|
||||||
|
val redirectUrl: String
|
||||||
|
get() = feedHandlerResult?.redirectUrl?:""
|
||||||
|
|
||||||
fun run(): Boolean {
|
fun run(): Boolean {
|
||||||
feedHandlerResult = task.call()
|
feedHandlerResult = task.call()
|
||||||
|
@ -386,12 +369,6 @@ object FeedUpdateManager {
|
||||||
savedFeed = Feeds.updateFeed(context, feedHandlerResult!!.feed, false)
|
savedFeed = Feeds.updateFeed(context, feedHandlerResult!!.feed, false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadStatus: DownloadResult
|
|
||||||
get() = task.downloadStatus
|
|
||||||
|
|
||||||
val redirectUrl: String
|
|
||||||
get() = feedHandlerResult?.redirectUrl?:""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -252,11 +252,11 @@ open class SyncService(context: Context, params: WorkerParameters) : Worker(cont
|
||||||
val guid = if (isValidGuid(action.guid)) action.guid else null
|
val guid = if (isValidGuid(action.guid)) action.guid else null
|
||||||
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
||||||
if (feedItem == null) {
|
if (feedItem == null) {
|
||||||
Log.i(TAG, "Unknown feed item: $action")
|
Logd(TAG, "Unknown feed item: $action")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (feedItem.media == null) {
|
if (feedItem.media == null) {
|
||||||
Log.i(TAG, "Feed item has no media: $action")
|
Logd(TAG, "Feed item has no media: $action")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
var idRemove: Long? = null
|
var idRemove: Long? = null
|
||||||
|
|
|
@ -157,7 +157,7 @@ import kotlin.math.min
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.w(TAG, "port $hostPort in use, ignored")
|
Logd(TAG, "port $hostPort in use, ignored")
|
||||||
loginFail = true
|
loginFail = true
|
||||||
}
|
}
|
||||||
EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "5"))
|
EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "5"))
|
||||||
|
@ -316,11 +316,11 @@ import kotlin.math.min
|
||||||
val guid = if (isValidGuid(action.guid)) action.guid else null
|
val guid = if (isValidGuid(action.guid)) action.guid else null
|
||||||
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
||||||
if (feedItem == null) {
|
if (feedItem == null) {
|
||||||
Log.i(TAG, "Unknown feed item: $action")
|
Logd(TAG, "Unknown feed item: $action")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (feedItem.media == null) {
|
if (feedItem.media == null) {
|
||||||
Log.i(TAG, "Feed item has no media: $action")
|
Logd(TAG, "Feed item has no media: $action")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// feedItem.media = getFeedMedia(feedItem.media!!.id)
|
// feedItem.media = getFeedMedia(feedItem.media!!.id)
|
||||||
|
|
|
@ -8,10 +8,14 @@ import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
|
import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
|
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||||
|
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
|
@ -39,6 +43,71 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
private var initialized = false
|
private var initialized = false
|
||||||
private var eventsRegistered = false
|
private var eventsRegistered = false
|
||||||
|
|
||||||
|
private val mConnection: ServiceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||||
|
if (service is LocalBinder) {
|
||||||
|
playbackService = service.service
|
||||||
|
onPlaybackServiceConnected()
|
||||||
|
if (!released) {
|
||||||
|
queryService()
|
||||||
|
Logd(TAG, "Connection to Service established")
|
||||||
|
} else Logd(TAG, "Connection to playback service has been established, but controller has already been released")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
playbackService = null
|
||||||
|
initialized = false
|
||||||
|
Logd(TAG, "Disconnected from Service")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var prevStatus = PlayerStatus.STOPPED
|
||||||
|
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
Logd(TAG, "BroadcastReceiver onReceive")
|
||||||
|
if (playbackService != null && mPlayerInfo != null) {
|
||||||
|
val info = mPlayerInfo!!
|
||||||
|
Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
|
||||||
|
if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
|
||||||
|
MediaPlayerBase.status = info.playerStatus
|
||||||
|
prevStatus = MediaPlayerBase.status
|
||||||
|
curMedia = info.playable
|
||||||
|
handleStatus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logd(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
|
||||||
|
if (isRunning) bindToService()
|
||||||
|
else {
|
||||||
|
MediaPlayerBase.status = PlayerStatus.STOPPED
|
||||||
|
handleStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
|
||||||
|
val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
|
||||||
|
if (code == -1 || type == -1) {
|
||||||
|
Logd(TAG, "Bad arguments. Won't handle intent")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
|
||||||
|
if (playbackService == null && isRunning) {
|
||||||
|
bindToService()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mediaInfoLoaded = false
|
||||||
|
queryService()
|
||||||
|
}
|
||||||
|
PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun init() {
|
fun init() {
|
||||||
Logd(TAG, "controller init")
|
Logd(TAG, "controller init")
|
||||||
|
@ -46,7 +115,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
eventsRegistered = true
|
eventsRegistered = true
|
||||||
}
|
}
|
||||||
if (PlaybackService.isRunning) initServiceRunning()
|
if (isRunning) initServiceRunning()
|
||||||
else updatePlayButtonShowsPlay(true)
|
else updatePlayButtonShowsPlay(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,76 +202,11 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
*/
|
*/
|
||||||
private fun bindToService() {
|
private fun bindToService() {
|
||||||
Logd(TAG, "Trying to connect to service")
|
Logd(TAG, "Trying to connect to service")
|
||||||
check(PlaybackService.isRunning) { "Trying to bind but service is not running" }
|
check(isRunning) { "Trying to bind but service is not running" }
|
||||||
val bound = activity.bindService(Intent(activity, PlaybackService::class.java), mConnection, 0)
|
val bound = activity.bindService(Intent(activity, PlaybackService::class.java), mConnection, 0)
|
||||||
Logd(TAG, "Result for service binding: $bound")
|
Logd(TAG, "Result for service binding: $bound")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mConnection: ServiceConnection = object : ServiceConnection {
|
|
||||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
|
||||||
if (service is LocalBinder) {
|
|
||||||
playbackService = service.service
|
|
||||||
onPlaybackServiceConnected()
|
|
||||||
if (!released) {
|
|
||||||
queryService()
|
|
||||||
Logd(TAG, "Connection to Service established")
|
|
||||||
} else Log.i(TAG, "Connection to playback service has been established, but controller has already been released")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
|
||||||
playbackService = null
|
|
||||||
initialized = false
|
|
||||||
Logd(TAG, "Disconnected from Service")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var prevStatus = PlayerStatus.STOPPED
|
|
||||||
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
Logd(TAG, "BroadcastReceiver onReceive")
|
|
||||||
if (playbackService != null && mPlayerInfo != null) {
|
|
||||||
val info = mPlayerInfo!!
|
|
||||||
Logd(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${curMedia?.getIdentifier()} ${info.playable?.getIdentifier()}.")
|
|
||||||
if (prevStatus != info.playerStatus || curMedia == null || curMedia!!.getIdentifier() != info.playable?.getIdentifier()) {
|
|
||||||
MediaPlayerBase.status = info.playerStatus
|
|
||||||
prevStatus = MediaPlayerBase.status
|
|
||||||
curMedia = info.playable
|
|
||||||
handleStatus()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
|
|
||||||
if (PlaybackService.isRunning) bindToService()
|
|
||||||
else {
|
|
||||||
MediaPlayerBase.status = PlayerStatus.STOPPED
|
|
||||||
handleStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val notificationReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
val type = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_TYPE, -1)
|
|
||||||
val code = intent.getIntExtra(PlaybackService.EXTRA_NOTIFICATION_CODE, -1)
|
|
||||||
if (code == -1 || type == -1) {
|
|
||||||
Logd(TAG, "Bad arguments. Won't handle intent")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (type) {
|
|
||||||
PlaybackService.NOTIFICATION_TYPE_RELOAD -> {
|
|
||||||
if (playbackService == null && PlaybackService.isRunning) {
|
|
||||||
bindToService()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mediaInfoLoaded = false
|
|
||||||
queryService()
|
|
||||||
}
|
|
||||||
PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END -> onPlaybackEnd()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onPlaybackEnd() {}
|
open fun onPlaybackEnd() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -258,7 +262,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
if (curMedia == null) return
|
if (curMedia == null) return
|
||||||
if (playbackService == null) {
|
if (playbackService == null) {
|
||||||
PlaybackServiceStarter(activity, curMedia!!).start()
|
PlaybackServiceStarter(activity, curMedia!!).start()
|
||||||
Log.w(TAG, "playbackservice was null, restarted!")
|
// Log.w(TAG, "playbackservice was null, restarted!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,26 +270,26 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
if (curMedia == null) return
|
if (curMedia == null) return
|
||||||
if (playbackService == null) {
|
if (playbackService == null) {
|
||||||
PlaybackServiceStarter(activity, curMedia!!).start()
|
PlaybackServiceStarter(activity, curMedia!!).start()
|
||||||
Log.w(TAG, "playbackservice was null, restarted!")
|
Logd(TAG, "playbackservice was null, restarted!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
when (MediaPlayerBase.status) {
|
when (MediaPlayerBase.status) {
|
||||||
PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
|
PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
|
||||||
PlayerStatus.PLAYING -> {
|
PlayerStatus.PLAYING -> {
|
||||||
playbackService?.mediaPlayer?.pause(true, reinit = false)
|
playbackService?.mPlayer?.pause(true, reinit = false)
|
||||||
playbackService?.isSpeedForward = false
|
playbackService?.isSpeedForward = false
|
||||||
playbackService?.isFallbackSpeed = false
|
playbackService?.isFallbackSpeed = false
|
||||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
||||||
}
|
}
|
||||||
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||||
playbackService?.mediaPlayer?.resume()
|
playbackService?.mPlayer?.resume()
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!))
|
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!))
|
||||||
}
|
}
|
||||||
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
|
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
|
||||||
PlayerStatus.INITIALIZED -> {
|
PlayerStatus.INITIALIZED -> {
|
||||||
if (playbackService != null) isStartWhenPrepared = true
|
if (playbackService != null) isStartWhenPrepared = true
|
||||||
playbackService?.mediaPlayer?.prepare()
|
playbackService?.mPlayer?.prepare()
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -300,35 +304,35 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
|
|
||||||
var playbackService: PlaybackService? = null
|
var playbackService: PlaybackService? = null
|
||||||
|
|
||||||
val position: Int
|
val curPosition: Int
|
||||||
get() = playbackService?.currentPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
||||||
|
|
||||||
val duration: Int
|
val duration: Int
|
||||||
get() = playbackService?.duration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||||
|
|
||||||
val curSpeedMultiplier: Float
|
val curSpeedMultiplier: Float
|
||||||
get() = playbackService?.currentPlaybackSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
||||||
|
|
||||||
val isPlayingVideoLocally: Boolean
|
val isPlayingVideoLocally: Boolean
|
||||||
get() = when {
|
get() = when {
|
||||||
PlaybackService.isCasting -> false
|
isCasting -> false
|
||||||
playbackService != null -> currentMediaType == MediaType.VIDEO
|
playbackService != null -> currentMediaType == MediaType.VIDEO
|
||||||
else -> curMedia?.getMediaType() == MediaType.VIDEO
|
else -> curMedia?.getMediaType() == MediaType.VIDEO
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isStartWhenPrepared: Boolean
|
private var isStartWhenPrepared: Boolean
|
||||||
get() = playbackService?.mediaPlayer?.startWhenPrepared?.get() ?: false
|
get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
|
||||||
set(s) {
|
set(s) {
|
||||||
playbackService?.mediaPlayer?.startWhenPrepared?.set(s)
|
playbackService?.mPlayer?.startWhenPrepared?.set(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val mPlayerInfo: MediaPlayerInfo?
|
private val mPlayerInfo: MediaPlayerInfo?
|
||||||
get() = playbackService?.mediaPlayer?.playerInfo
|
get() = playbackService?.mPlayer?.playerInfo
|
||||||
|
|
||||||
fun seekTo(time: Int) {
|
fun seekTo(time: Int) {
|
||||||
if (playbackService != null) {
|
if (playbackService != null) {
|
||||||
playbackService!!.mediaPlayer?.seekTo(time)
|
playbackService!!.mPlayer?.seekTo(time)
|
||||||
if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(time, duration))
|
// if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, time, duration))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,24 +341,24 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
when (MediaPlayerBase.status) {
|
when (MediaPlayerBase.status) {
|
||||||
PlayerStatus.PLAYING -> {
|
PlayerStatus.PLAYING -> {
|
||||||
MediaPlayerBase.status = PlayerStatus.FALLBACK
|
MediaPlayerBase.status = PlayerStatus.FALLBACK
|
||||||
fallbackSpeed_(speed)
|
setToFallback(speed)
|
||||||
}
|
}
|
||||||
PlayerStatus.FALLBACK -> {
|
PlayerStatus.FALLBACK -> {
|
||||||
MediaPlayerBase.status = PlayerStatus.PLAYING
|
MediaPlayerBase.status = PlayerStatus.PLAYING
|
||||||
fallbackSpeed_(speed)
|
setToFallback(speed)
|
||||||
}
|
}
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fallbackSpeed_(speed: Float) {
|
private fun setToFallback(speed: Float) {
|
||||||
if (playbackService?.mediaPlayer == null || playbackService!!.isSpeedForward) return
|
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
|
||||||
|
|
||||||
if (!playbackService!!.isFallbackSpeed) {
|
if (!playbackService!!.isFallbackSpeed) {
|
||||||
playbackService!!.normalSpeed = playbackService!!.mediaPlayer!!.getPlaybackSpeed()
|
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
|
||||||
playbackService!!.mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||||
} else playbackService!!.mediaPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||||
|
|
||||||
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
|
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
|
||||||
}
|
}
|
||||||
|
@ -362,5 +366,28 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
||||||
fun sleepTimerActive(): Boolean {
|
fun sleepTimerActive(): Boolean {
|
||||||
return playbackService?.taskManager?.isSleepTimerActive ?: false
|
return playbackService?.taskManager?.isSleepTimerActive ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an intent which starts an audio- or videoplayer, depending on the
|
||||||
|
* type of media that is being played. If the playbackservice is not
|
||||||
|
* running, the type of the last played media will be looked up.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPlayerActivityIntent(context: Context): Intent {
|
||||||
|
val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
|
||||||
|
else curState.curIsVideo
|
||||||
|
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
|
||||||
|
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as [.getPlayerActivityIntent], but here the type of activity
|
||||||
|
* depends on the medaitype that is provided as an argument.
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
|
||||||
|
return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
|
||||||
|
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,10 +27,6 @@ object InTheatre {
|
||||||
}
|
}
|
||||||
|
|
||||||
var curMedia: Playable? = null
|
var curMedia: Playable? = null
|
||||||
// get() {
|
|
||||||
// if (field == null) field = loadPlayableFromPreferences()
|
|
||||||
// return field
|
|
||||||
// }
|
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
if (field is EpisodeMedia) {
|
if (field is EpisodeMedia) {
|
||||||
|
|
|
@ -49,18 +49,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
status = PlayerStatus.STOPPED
|
status = PlayerStatus.STOPPED
|
||||||
}
|
}
|
||||||
|
|
||||||
// fun startWhenPrepared.get(): Boolean {
|
|
||||||
// return startWhenPrepared.get()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// fun startWhenPrepared.set(startWhenPrepared: Boolean) {
|
|
||||||
// this.startWhenPrepared.set(startWhenPrepared)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// open fun getPlayable(): Playable? {
|
|
||||||
// return curMedia
|
|
||||||
// }
|
|
||||||
|
|
||||||
protected open fun setPlayable(playable: Playable?) {
|
protected open fun setPlayable(playable: Playable?) {
|
||||||
if (playable != null && playable !== curMedia) {
|
if (playable != null && playable !== curMedia) {
|
||||||
curMedia = playable
|
curMedia = playable
|
||||||
|
@ -154,12 +142,15 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seek a specific position from the current position
|
* Seek a specific position from the current position
|
||||||
* @param d offset from current position (positive or negative)
|
* @param delta offset from current position (positive or negative)
|
||||||
*/
|
*/
|
||||||
fun seekDelta(d: Int) {
|
fun seekDelta(delta: Int) {
|
||||||
val currentPosition = getPosition()
|
val curPosition = getPosition()
|
||||||
if (currentPosition != Playable.INVALID_TIME) seekTo(currentPosition + d)
|
if (curPosition != Playable.INVALID_TIME) {
|
||||||
else Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta")
|
val prevMedia = curMedia
|
||||||
|
seekTo(curPosition + delta)
|
||||||
|
}
|
||||||
|
else Log.e(TAG, "seekDelta getPosition() returned INVALID_TIME in seekDelta")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -268,18 +259,15 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
@Synchronized
|
@Synchronized
|
||||||
protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int = Playable.INVALID_TIME) {
|
protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int = Playable.INVALID_TIME) {
|
||||||
Logd(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
|
Logd(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
|
||||||
|
|
||||||
this.oldStatus = status
|
this.oldStatus = status
|
||||||
status = newStatus
|
status = newStatus
|
||||||
if (newMedia != null) setPlayable(newMedia)
|
if (newMedia != null) setPlayable(newMedia)
|
||||||
|
|
||||||
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
|
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
|
||||||
when {
|
when {
|
||||||
oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING -> callback.onPlaybackPause(newMedia, position)
|
oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING -> callback.onPlaybackPause(newMedia, position)
|
||||||
oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING -> callback.onPlaybackStart(newMedia, position)
|
oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING -> callback.onPlaybackStart(newMedia, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
|
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,29 +277,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MediaPlayerCallback {
|
|
||||||
fun statusChanged(newInfo: MediaPlayerInfo?)
|
|
||||||
|
|
||||||
// TODO: not used
|
|
||||||
fun shouldStop() {}
|
|
||||||
|
|
||||||
fun onMediaChanged(reloadUI: Boolean)
|
|
||||||
|
|
||||||
fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean)
|
|
||||||
|
|
||||||
fun onPlaybackStart(playable: Playable, position: Int)
|
|
||||||
|
|
||||||
fun onPlaybackPause(playable: Playable?, position: Int)
|
|
||||||
|
|
||||||
fun getNextInQueue(currentMedia: Playable?): Playable?
|
|
||||||
|
|
||||||
fun findMedia(url: String): Playable?
|
|
||||||
|
|
||||||
fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean)
|
|
||||||
|
|
||||||
fun ensureMediaInfoLoaded(media: Playable)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
|
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -354,7 +319,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
val newPosition = currentPosition - rewindTime.toInt()
|
val newPosition = currentPosition - rewindTime.toInt()
|
||||||
return max(newPosition.toDouble(), 0.0).toInt()
|
return max(newPosition.toDouble(), 0.0).toInt()
|
||||||
} else return currentPosition
|
} else return currentPosition
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -363,19 +327,11 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getCurrentPlaybackSpeed(media: Playable?): Float {
|
fun getCurrentPlaybackSpeed(media: Playable?): Float {
|
||||||
var playbackSpeed = FeedPreferences.SPEED_USE_GLOBAL
|
var playbackSpeed = FeedPreferences.SPEED_USE_GLOBAL
|
||||||
var mediaType: MediaType? = null
|
val mediaType: MediaType? = media?.getMediaType()
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
mediaType = media.getMediaType()
|
|
||||||
playbackSpeed = curState.curTempSpeed
|
playbackSpeed = curState.curTempSpeed
|
||||||
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
|
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
|
||||||
val item = media.episode
|
if (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
|
||||||
if (item != null) {
|
|
||||||
val feed = item.feed
|
|
||||||
if (feed?.preferences != null) {
|
|
||||||
playbackSpeed = feed.preferences!!.playSpeed
|
|
||||||
Logd(TAG, "using feed speed $playbackSpeed")
|
|
||||||
} else Logd(TAG, "Can not get feed specific playback speed: $feed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
|
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package ac.mdiq.podcini.playback.base
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||||
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
|
|
||||||
|
interface MediaPlayerCallback {
|
||||||
|
fun statusChanged(newInfo: MediaPlayerInfo?)
|
||||||
|
|
||||||
|
// TODO: not used
|
||||||
|
fun shouldStop() {}
|
||||||
|
|
||||||
|
fun onMediaChanged(reloadUI: Boolean)
|
||||||
|
|
||||||
|
fun onPostPlayback(playable: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean)
|
||||||
|
|
||||||
|
fun onPlaybackStart(playable: Playable, position: Int)
|
||||||
|
|
||||||
|
fun onPlaybackPause(playable: Playable?, position: Int)
|
||||||
|
|
||||||
|
fun getNextInQueue(currentMedia: Playable?): Playable?
|
||||||
|
|
||||||
|
fun findMedia(url: String): Playable?
|
||||||
|
|
||||||
|
fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean)
|
||||||
|
|
||||||
|
fun ensureMediaInfoLoaded(media: Playable)
|
||||||
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
package ac.mdiq.podcini.playback.base
|
package ac.mdiq.podcini.playback.base
|
||||||
|
|
||||||
enum class PlayerStatus(private val statusValue: Int) {
|
enum class PlayerStatus(private val statusValue: Int) {
|
||||||
INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
|
|
||||||
ERROR(-1),
|
ERROR(-1),
|
||||||
PREPARING(19),
|
INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
|
||||||
PAUSED(30),
|
|
||||||
FALLBACK(35),
|
|
||||||
PLAYING(40),
|
|
||||||
STOPPED(5),
|
STOPPED(5),
|
||||||
|
INITIALIZING(9), // playback service is loading the Playable's metadata
|
||||||
|
INITIALIZED(10), // playback service was started, data source of media player was set
|
||||||
|
PREPARING(19),
|
||||||
PREPARED(20),
|
PREPARED(20),
|
||||||
SEEKING(29),
|
SEEKING(29),
|
||||||
INITIALIZING(9), // playback service is loading the Playable's metadata
|
PAUSED(30),
|
||||||
INITIALIZED(10); // playback service was started, data source of media player was set
|
FALLBACK(35),
|
||||||
|
PLAYING(40);
|
||||||
|
|
||||||
fun isAtLeast(other: PlayerStatus?): Boolean {
|
fun isAtLeast(other: PlayerStatus?): Boolean {
|
||||||
return other == null || this.statusValue >= other.statusValue
|
return other == null || this.statusValue >= other.statusValue
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package ac.mdiq.podcini.playback.service
|
package ac.mdiq.podcini.playback.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.BuildConfig
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
|
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
|
||||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
import ac.mdiq.podcini.net.download.service.PodciniHttpClient
|
||||||
|
@ -8,6 +7,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.wasDownloadBlocked
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
|
@ -58,7 +58,6 @@ import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages the MediaPlayer object of the PlaybackService.
|
* Manages the MediaPlayer object of the PlaybackService.
|
||||||
*/
|
*/
|
||||||
|
@ -82,13 +81,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
|
|
||||||
private val formats: List<Format>
|
private val formats: List<Format>
|
||||||
get() {
|
get() {
|
||||||
val formats: MutableList<Format> = arrayListOf()
|
val formats_: MutableList<Format> = arrayListOf()
|
||||||
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList()
|
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList()
|
||||||
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
|
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
|
||||||
for (i in 0 until trackGroups.length) {
|
for (i in 0 until trackGroups.length) {
|
||||||
formats.add(trackGroups[i].getFormat(0))
|
formats_.add(trackGroups[i].getFormat(0))
|
||||||
}
|
}
|
||||||
return formats
|
return formats_
|
||||||
}
|
}
|
||||||
|
|
||||||
private val audioRendererIndex: Int
|
private val audioRendererIndex: Int
|
||||||
|
@ -139,7 +138,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
private fun prepareWR() {
|
private fun prepareWR() {
|
||||||
Logd(TAG, "prepareWR() called")
|
Logd(TAG, "prepareWR() called")
|
||||||
if (mediaSource == null && mediaItem == null) return
|
if (mediaSource == null && mediaItem == null) return
|
||||||
|
|
||||||
if (mediaSource != null) exoPlayer?.setMediaSource(mediaSource!!, false)
|
if (mediaSource != null) exoPlayer?.setMediaSource(mediaSource!!, false)
|
||||||
else exoPlayer?.setMediaItem(mediaItem!!)
|
else exoPlayer?.setMediaItem(mediaItem!!)
|
||||||
exoPlayer?.prepare()
|
exoPlayer?.prepare()
|
||||||
|
@ -164,7 +162,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
exoPlayer?.setAudioAttributes(b.build(), true)
|
exoPlayer?.setAudioAttributes(b.build(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun metadata(p: Playable): MediaMetadata {
|
private fun buildMetadata(p: Playable): MediaMetadata {
|
||||||
val builder = MediaMetadata.Builder()
|
val builder = MediaMetadata.Builder()
|
||||||
.setArtist(p.getFeedTitle())
|
.setArtist(p.getFeedTitle())
|
||||||
.setTitle(p.getEpisodeTitle())
|
.setTitle(p.getEpisodeTitle())
|
||||||
|
@ -208,18 +206,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
|
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
|
||||||
* not do anything.
|
* not do anything.
|
||||||
* Whether playback starts immediately depends on the given parameters. See below for more details.
|
* Whether playback starts immediately depends on the given parameters. See below for more details.
|
||||||
*
|
|
||||||
* States:
|
* States:
|
||||||
* During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
|
* During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters.
|
||||||
*
|
|
||||||
* If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
|
* If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If
|
||||||
* 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
|
* 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state.
|
||||||
*
|
|
||||||
* If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
|
* If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object
|
||||||
* will enter the ERROR state.
|
* will enter the ERROR state.
|
||||||
*
|
|
||||||
* This method is executed on an internal executor service.
|
* This method is executed on an internal executor service.
|
||||||
*
|
|
||||||
* @param playable The Playable object that is supposed to be played. This parameter must not be null.
|
* @param playable The Playable object that is supposed to be played. This parameter must not be null.
|
||||||
* @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
|
* @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via
|
||||||
* getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
|
* getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by
|
||||||
|
@ -230,32 +223,28 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
|
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
|
||||||
*/
|
*/
|
||||||
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
|
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean, forceReset: Boolean) {
|
||||||
Logd(TAG, "playMediaObject $forceReset $stream $startWhenPrepared $prepareImmediately $status ${playable.getEpisodeTitle()} ")
|
Logd(TAG, "playMediaObject status=$status stream=$stream startWhenPrepared=$startWhenPrepared prepareImmediately=$prepareImmediately forceReset=$forceReset ${playable.getEpisodeTitle()} ")
|
||||||
if (curMedia != null) {
|
if (curMedia != null) {
|
||||||
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
|
if (!forceReset && curMedia!!.getIdentifier() == prevMedia?.getIdentifier() && status == PlayerStatus.PLAYING) {
|
||||||
// episode is already playing -> ignore method call
|
|
||||||
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
|
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
|
||||||
return
|
return
|
||||||
} else {
|
|
||||||
Logd(TAG, "playMediaObject starts new media ${curMedia!!.getIdentifier()} ${prevMedia?.getIdentifier()} $status")
|
|
||||||
// set temporarily to pause in order to update list with current position
|
|
||||||
if (status == PlayerStatus.PLAYING) {
|
|
||||||
val pos = curMedia?.getPosition() ?: -1
|
|
||||||
seekTo(pos)
|
|
||||||
callback.onPlaybackPause(curMedia, pos)
|
|
||||||
}
|
|
||||||
// stop playback of this episode
|
|
||||||
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 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) {
|
||||||
|
val pos = curMedia?.getPosition() ?: -1
|
||||||
|
seekTo(pos)
|
||||||
|
callback.onPlaybackPause(curMedia, pos)
|
||||||
|
}
|
||||||
|
// stop playback of this episode
|
||||||
|
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
|
curMedia = playable
|
||||||
this.isStreaming = stream
|
this.isStreaming = stream
|
||||||
mediaType = curMedia!!.getMediaType()
|
mediaType = curMedia!!.getMediaType()
|
||||||
|
@ -263,7 +252,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
createMediaPlayer()
|
createMediaPlayer()
|
||||||
this.startWhenPrepared.set(startWhenPrepared)
|
this.startWhenPrepared.set(startWhenPrepared)
|
||||||
setPlayerStatus(PlayerStatus.INITIALIZING, curMedia)
|
setPlayerStatus(PlayerStatus.INITIALIZING, curMedia)
|
||||||
val metadata = metadata(curMedia!!)
|
val metadata = buildMetadata(curMedia!!)
|
||||||
try {
|
try {
|
||||||
callback.ensureMediaInfoLoaded(curMedia!!)
|
callback.ensureMediaInfoLoaded(curMedia!!)
|
||||||
callback.onMediaChanged(false)
|
callback.onMediaChanged(false)
|
||||||
|
@ -313,7 +302,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
val newPosition = calculatePositionWithRewind(curMedia!!.getPosition(), curMedia!!.getLastPlayedTime())
|
val newPosition = calculatePositionWithRewind(curMedia!!.getPosition(), curMedia!!.getLastPlayedTime())
|
||||||
seekTo(newPosition)
|
seekTo(newPosition)
|
||||||
}
|
}
|
||||||
// play()
|
|
||||||
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
||||||
exoPlayer?.play()
|
exoPlayer?.play()
|
||||||
// Can't set params when paused - so always set it on start in case they changed
|
// Can't set params when paused - so always set it on start in case they changed
|
||||||
|
@ -331,9 +319,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
setPlayerStatus(PlayerStatus.PAUSED, curMedia, getPosition())
|
setPlayerStatus(PlayerStatus.PAUSED, curMedia, getPosition())
|
||||||
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, Action.END))
|
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, Action.END))
|
||||||
if (isStreaming && reinit) reinit()
|
if (isStreaming && reinit) reinit()
|
||||||
} else {
|
} else Logd(TAG, "Ignoring call to pause: Player is in $status state")
|
||||||
Logd(TAG, "Ignoring call to pause: Player is in $status state")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun prepare() {
|
override fun prepare() {
|
||||||
|
@ -346,7 +332,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
if (curMedia != null) {
|
if (curMedia != null) {
|
||||||
val pos = curMedia!!.getPosition()
|
val pos = curMedia!!.getPosition()
|
||||||
if (pos > 0) seekTo(pos)
|
if (pos > 0) seekTo(pos)
|
||||||
if (curMedia!!.getDuration() <= 0) {
|
if (curMedia != null && curMedia!!.getDuration() <= 0) {
|
||||||
Logd(TAG, "Setting duration of media")
|
Logd(TAG, "Setting duration of media")
|
||||||
curMedia!!.setDuration(if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt())
|
curMedia!!.setDuration(if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt())
|
||||||
}
|
}
|
||||||
|
@ -367,21 +353,24 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(t0: Int) {
|
override fun seekTo(t: Int) {
|
||||||
var t = t0
|
var t = t
|
||||||
if (t < 0) t = 0
|
if (t < 0) t = 0
|
||||||
Logd(TAG, "seekTo() called")
|
Logd(TAG, "seekTo() called $t")
|
||||||
|
|
||||||
if (t >= getDuration()) {
|
if (t >= getDuration()) {
|
||||||
Logd(TAG, "Seek reached end of file, skipping to next episode")
|
Logd(TAG, "Seek reached end of file, skipping to next episode")
|
||||||
exoPlayer?.seekTo(t.toLong())
|
exoPlayer?.seekTo(t.toLong()) // can set curMedia to null
|
||||||
|
if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, t, curMedia!!.getDuration()))
|
||||||
audioSeekCompleteListener?.run()
|
audioSeekCompleteListener?.run()
|
||||||
endPlayback(true, wasSkipped = true, true, toStoppedState = true)
|
endPlayback(true, wasSkipped = true, true, toStoppedState = true)
|
||||||
|
t = getPosition()
|
||||||
// return
|
// return
|
||||||
}
|
}
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||||
|
Logd(TAG, "seekTo() called $t")
|
||||||
if (seekLatch != null && seekLatch!!.count > 0) {
|
if (seekLatch != null && seekLatch!!.count > 0) {
|
||||||
try {
|
try {
|
||||||
seekLatch!!.await(3, TimeUnit.SECONDS)
|
seekLatch!!.await(3, TimeUnit.SECONDS)
|
||||||
|
@ -391,8 +380,9 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
}
|
}
|
||||||
seekLatch = CountDownLatch(1)
|
seekLatch = CountDownLatch(1)
|
||||||
statusBeforeSeeking = status
|
statusBeforeSeeking = status
|
||||||
setPlayerStatus(PlayerStatus.SEEKING, curMedia, getPosition())
|
setPlayerStatus(PlayerStatus.SEEKING, curMedia, t)
|
||||||
exoPlayer?.seekTo(t.toLong())
|
exoPlayer?.seekTo(t.toLong())
|
||||||
|
if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, t, curMedia!!.getDuration()))
|
||||||
audioSeekCompleteListener?.run()
|
audioSeekCompleteListener?.run()
|
||||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) curMedia?.setPosition(t)
|
if (statusBeforeSeeking == PlayerStatus.PREPARED) curMedia?.setPosition(t)
|
||||||
try {
|
try {
|
||||||
|
@ -411,25 +401,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDuration(): Int {
|
override fun getDuration(): Int {
|
||||||
var retVal = Playable.INVALID_TIME
|
return curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||||
if (status == PlayerStatus.PLAYING || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED)
|
|
||||||
retVal = if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt()
|
|
||||||
if (retVal <= 0) {
|
|
||||||
val playableDur = curMedia?.getDuration() ?: -1
|
|
||||||
if (playableDur > 0) retVal = playableDur
|
|
||||||
}
|
|
||||||
return retVal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPosition(): Int {
|
override fun getPosition(): Int {
|
||||||
var retVal = Playable.INVALID_TIME
|
var retVal = Playable.INVALID_TIME
|
||||||
// Log.d(TAG, "getPosition() ${playable?.getIdentifier()} $status")
|
|
||||||
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
||||||
|
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
|
||||||
if (retVal <= 0) {
|
|
||||||
val playablePos = curMedia?.getPosition() ?: -1
|
|
||||||
if (playablePos >= 0) retVal = playablePos
|
|
||||||
}
|
|
||||||
return retVal
|
return retVal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,16 +439,14 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
volumeRight *= adaptionFactor
|
volumeRight *= adaptionFactor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (volumeLeft > 1) {
|
if (volumeLeft > 1) {
|
||||||
exoPlayer!!.volume = 1f
|
exoPlayer?.volume = 1f
|
||||||
loudnessEnhancer?.setEnabled(true)
|
loudnessEnhancer?.setEnabled(true)
|
||||||
loudnessEnhancer?.setTargetGain((1000 * (volumeLeft - 1)).toInt())
|
loudnessEnhancer?.setTargetGain((1000 * (volumeLeft - 1)).toInt())
|
||||||
} else {
|
} else {
|
||||||
exoPlayer!!.volume = volumeLeft
|
exoPlayer?.volume = volumeLeft
|
||||||
loudnessEnhancer?.setEnabled(false)
|
loudnessEnhancer?.setEnabled(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Logd(TAG, "Media player volume was set to $volumeLeft $volumeRight")
|
Logd(TAG, "Media player volume was set to $volumeLeft $volumeRight")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -543,6 +519,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createMediaPlayer() {
|
override fun createMediaPlayer() {
|
||||||
|
Logd(TAG, "createMediaPlayer()")
|
||||||
release()
|
release()
|
||||||
if (curMedia == null) {
|
if (curMedia == null) {
|
||||||
status = PlayerStatus.STOPPED
|
status = PlayerStatus.STOPPED
|
||||||
|
@ -559,21 +536,19 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
// we're relying on the position stored in the Playable object for post-playback processing
|
// we're relying on the position stored in the Playable object for post-playback processing
|
||||||
val position = getPosition()
|
val position = getPosition()
|
||||||
if (position >= 0) curMedia?.setPosition(position)
|
if (position >= 0) curMedia?.setPosition(position)
|
||||||
Logd(TAG, "endPlayback $hasEnded $wasSkipped $shouldContinue $toStoppedState")
|
Logd(TAG, "endPlayback hasEnded=$hasEnded wasSkipped=$wasSkipped shouldContinue=$shouldContinue toStoppedState=$toStoppedState")
|
||||||
|
|
||||||
val currentMedia = curMedia
|
val currentMedia = curMedia
|
||||||
var nextMedia: Playable? = null
|
var nextMedia: Playable? = null
|
||||||
if (shouldContinue) {
|
if (shouldContinue) {
|
||||||
// Load next episode if previous episode was in the queue and if there
|
// Load next episode if previous episode was in the queue and if there is an episode in the queue left.
|
||||||
// is an episode in the queue left.
|
|
||||||
// Start playback immediately if continuous playback is enabled
|
// Start playback immediately if continuous playback is enabled
|
||||||
nextMedia = callback.getNextInQueue(currentMedia)
|
nextMedia = callback.getNextInQueue(currentMedia)
|
||||||
if (nextMedia != null) {
|
if (nextMedia != null) {
|
||||||
Logd(TAG, "has nextMedia. call callback.onPlaybackEnded false")
|
Logd(TAG, "has nextMedia. call callback.onPlaybackEnded false")
|
||||||
// curMedia = null
|
if (wasSkipped) setPlayerStatus(PlayerStatus.INDETERMINATE, null)
|
||||||
callback.onPlaybackEnded(nextMedia.getMediaType(), false)
|
callback.onPlaybackEnded(nextMedia.getMediaType(), false)
|
||||||
// setting media to null signals to playMediaObject() that
|
// setting media to null signals to playMediaObject that we're taking care of post-playback processing
|
||||||
// we're taking care of post-playback processing
|
|
||||||
curMedia = null
|
curMedia = null
|
||||||
playMediaObject(nextMedia, !nextMedia.localFileAvailable(), isPlaying, isPlaying)
|
playMediaObject(nextMedia, !nextMedia.localFileAvailable(), isPlaying, isPlaying)
|
||||||
}
|
}
|
||||||
|
@ -585,7 +560,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
callback.onPlaybackEnded(null, true)
|
callback.onPlaybackEnded(null, true)
|
||||||
curMedia = null
|
curMedia = null
|
||||||
exoPlayer?.stop()
|
exoPlayer?.stop()
|
||||||
// stop()
|
|
||||||
releaseWifiLockIfNecessary()
|
releaseWifiLockIfNecessary()
|
||||||
if (status == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
|
if (status == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
|
||||||
else Logd(TAG, "Ignored call to stop: Current player state is: $status")
|
else Logd(TAG, "Ignored call to stop: Current player state is: $status")
|
||||||
|
@ -689,7 +663,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
|
|
||||||
const val BUFFERING_STARTED: Int = -1
|
const val BUFFERING_STARTED: Int = -1
|
||||||
const val BUFFERING_ENDED: Int = -2
|
const val BUFFERING_ENDED: Int = -2
|
||||||
const val ERROR_CODE_OFFSET: Int = 1000
|
|
||||||
|
|
||||||
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
private var httpDataSourceFactory: OkHttpDataSource.Factory? = null
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -42,11 +42,10 @@ class QuickSettingsTileService : TileService() {
|
||||||
return super.onBind(intent)
|
return super.onBind(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateTile() {
|
private fun updateTile() {
|
||||||
val qsTile = qsTile
|
val qsTile = qsTile
|
||||||
if (qsTile == null) Logd(TAG, "Ignored call to update QS tile: getQsTile() returned null.")
|
if (qsTile == null) Logd(TAG, "Ignored call to update QS tile: getQsTile() returned null.")
|
||||||
else {
|
else {
|
||||||
// val isPlaying = PlaybackService.isRunning && MediaPlayerBase.status == PlayerStatus.PLAYING
|
|
||||||
val isPlaying = (PlaybackService.isRunning && curState.curPlayerStatus == PLAYER_STATUS_PLAYING)
|
val isPlaying = (PlaybackService.isRunning && curState.curPlayerStatus == PLAYER_STATUS_PLAYING)
|
||||||
qsTile.state = if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
qsTile.state = if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||||
qsTile.updateTile()
|
qsTile.updateTile()
|
||||||
|
|
|
@ -351,7 +351,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba
|
||||||
/**
|
/**
|
||||||
* Notification interval of widget updater in milliseconds.
|
* Notification interval of widget updater in milliseconds.
|
||||||
*/
|
*/
|
||||||
const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 1000
|
const val WIDGET_UPDATER_NOTIFICATION_INTERVAL: Int = 5000
|
||||||
private const val SCHED_EX_POOL_SIZE = 2
|
private const val SCHED_EX_POOL_SIZE = 2
|
||||||
private const val UPDATE_INTERVAL = 1000L
|
private const val UPDATE_INTERVAL = 1000L
|
||||||
const val NOTIFICATION_THRESHOLD: Long = 10000
|
const val NOTIFICATION_THRESHOLD: Long = 10000
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ac.mdiq.podcini.preferences
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.Writer
|
||||||
|
|
||||||
|
interface ExportWriter {
|
||||||
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
|
fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context)
|
||||||
|
|
||||||
|
fun fileExtension(): String?
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
package ac.mdiq.podcini.storage.backup
|
package ac.mdiq.podcini.preferences
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isAutoBackupOPML
|
import ac.mdiq.podcini.preferences.UserPreferences.isAutoBackupOPML
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.transport.OpmlReader
|
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
|
||||||
import ac.mdiq.podcini.storage.transport.OpmlWriter
|
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.app.backup.BackupAgentHelper
|
import android.app.backup.BackupAgentHelper
|
||||||
import android.app.backup.BackupDataInputStream
|
import android.app.backup.BackupDataInputStream
|
||||||
|
@ -45,7 +45,7 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
||||||
*/
|
*/
|
||||||
private var mChecksum: ByteArray = byteArrayOf()
|
private var mChecksum: ByteArray = byteArrayOf()
|
||||||
|
|
||||||
override fun performBackup(oldState: ParcelFileDescriptor, data: BackupDataOutput, newState: ParcelFileDescriptor) {
|
override fun performBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput, newState: ParcelFileDescriptor) {
|
||||||
Logd(TAG, "Performing backup")
|
Logd(TAG, "Performing backup")
|
||||||
val byteStream = ByteArrayOutputStream()
|
val byteStream = ByteArrayOutputStream()
|
||||||
var digester: MessageDigest? = null
|
var digester: MessageDigest? = null
|
||||||
|
@ -66,17 +66,14 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
||||||
if (digester != null) {
|
if (digester != null) {
|
||||||
val newChecksum = digester.digest()
|
val newChecksum = digester.digest()
|
||||||
Logd(TAG, "New checksum: " + BigInteger(1, newChecksum).toString(16))
|
Logd(TAG, "New checksum: " + BigInteger(1, newChecksum).toString(16))
|
||||||
|
|
||||||
// Get the old checksum
|
// Get the old checksum
|
||||||
if (oldState != null) {
|
if (oldState != null) {
|
||||||
val inState = FileInputStream(oldState.fileDescriptor)
|
val inState = FileInputStream(oldState.fileDescriptor)
|
||||||
val len = inState.read()
|
val len = inState.read()
|
||||||
|
|
||||||
if (len != -1) {
|
if (len != -1) {
|
||||||
val oldChecksum = ByteArray(len)
|
val oldChecksum = ByteArray(len)
|
||||||
IOUtils.read(inState, oldChecksum, 0, len)
|
IOUtils.read(inState, oldChecksum, 0, len)
|
||||||
Logd(TAG, "Old checksum: " + BigInteger(1, oldChecksum).toString(16))
|
Logd(TAG, "Old checksum: " + BigInteger(1, oldChecksum).toString(16))
|
||||||
|
|
||||||
if (oldChecksum.contentEquals(newChecksum)) {
|
if (oldChecksum.contentEquals(newChecksum)) {
|
||||||
Logd(TAG, "Checksums are the same; won't backup")
|
Logd(TAG, "Checksums are the same; won't backup")
|
||||||
return
|
return
|
||||||
|
@ -99,22 +96,18 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun restoreEntity(data: BackupDataInputStream) {
|
@OptIn(UnstableApi::class) override fun restoreEntity(data: BackupDataInputStream) {
|
||||||
Logd(TAG, "Backup restore")
|
Logd(TAG, "Backup restore")
|
||||||
|
|
||||||
if (OPML_ENTITY_KEY != data.key) {
|
if (OPML_ENTITY_KEY != data.key) {
|
||||||
Logd(TAG, "Unknown entity key: " + data.key)
|
Logd(TAG, "Unknown entity key: " + data.key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var digester: MessageDigest? = null
|
var digester: MessageDigest? = null
|
||||||
var reader: Reader
|
var reader: Reader
|
||||||
|
|
||||||
try {
|
try {
|
||||||
digester = MessageDigest.getInstance("MD5")
|
digester = MessageDigest.getInstance("MD5")
|
||||||
reader = InputStreamReader(DigestInputStream(data, digester), Charset.forName("UTF-8"))
|
reader = InputStreamReader(DigestInputStream(data, digester), Charset.forName("UTF-8"))
|
||||||
} catch (e: NoSuchAlgorithmException) {
|
} catch (e: NoSuchAlgorithmException) {
|
||||||
reader = InputStreamReader(data, Charset.forName("UTF-8"))
|
reader = InputStreamReader(data, Charset.forName("UTF-8"))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val opmlElements = OpmlReader().readDocument(reader)
|
val opmlElements = OpmlReader().readDocument(reader)
|
||||||
mChecksum = digester?.digest()?: byteArrayOf()
|
mChecksum = digester?.digest()?: byteArrayOf()
|
||||||
|
@ -139,7 +132,6 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the new state description, which is the checksum of the OPML file.
|
* Writes the new state description, which is the checksum of the OPML file.
|
||||||
*
|
|
||||||
* @param newState
|
* @param newState
|
||||||
* @param checksum
|
* @param checksum
|
||||||
*/
|
*/
|
|
@ -0,0 +1,153 @@
|
||||||
|
package ac.mdiq.podcini.preferences
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
|
import ac.mdiq.podcini.util.DateFormatter.formatRfc822Date
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Xml
|
||||||
|
import org.xmlpull.v1.XmlPullParser
|
||||||
|
import org.xmlpull.v1.XmlPullParserException
|
||||||
|
import org.xmlpull.v1.XmlPullParserFactory
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.Reader
|
||||||
|
import java.io.Writer
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class OpmlTransporter {
|
||||||
|
|
||||||
|
/** Represents a single feed in an OPML file. */
|
||||||
|
class OpmlElement {
|
||||||
|
@JvmField
|
||||||
|
var text: String? = null
|
||||||
|
var xmlUrl: String? = null
|
||||||
|
var htmlUrl: String? = null
|
||||||
|
var type: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Contains symbols for reading and writing OPML documents. */
|
||||||
|
private object OpmlSymbols {
|
||||||
|
const val OPML: String = "opml"
|
||||||
|
const val OUTLINE: String = "outline"
|
||||||
|
const val TEXT: String = "text"
|
||||||
|
const val XMLURL: String = "xmlUrl"
|
||||||
|
const val HTMLURL: String = "htmlUrl"
|
||||||
|
const val TYPE: String = "type"
|
||||||
|
const val VERSION: String = "version"
|
||||||
|
const val DATE_CREATED: String = "dateCreated"
|
||||||
|
const val HEAD: String = "head"
|
||||||
|
const val BODY: String = "body"
|
||||||
|
const val TITLE: String = "title"
|
||||||
|
const val XML_FEATURE_INDENT_OUTPUT: String = "http://xmlpull.org/v1/doc/features.html#indent-output"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes OPML documents. */
|
||||||
|
class OpmlWriter : ExportWriter {
|
||||||
|
/**
|
||||||
|
* Takes a list of feeds and a writer and writes those into an OPML
|
||||||
|
* document.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
|
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||||
|
val xs = Xml.newSerializer()
|
||||||
|
xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true)
|
||||||
|
xs.setOutput(writer)
|
||||||
|
|
||||||
|
xs.startDocument(ENCODING, false)
|
||||||
|
xs.startTag(null, OpmlSymbols.OPML)
|
||||||
|
xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION)
|
||||||
|
|
||||||
|
xs.startTag(null, OpmlSymbols.HEAD)
|
||||||
|
xs.startTag(null, OpmlSymbols.TITLE)
|
||||||
|
xs.text(OPML_TITLE)
|
||||||
|
xs.endTag(null, OpmlSymbols.TITLE)
|
||||||
|
xs.startTag(null, OpmlSymbols.DATE_CREATED)
|
||||||
|
xs.text(formatRfc822Date(Date()))
|
||||||
|
xs.endTag(null, OpmlSymbols.DATE_CREATED)
|
||||||
|
xs.endTag(null, OpmlSymbols.HEAD)
|
||||||
|
|
||||||
|
xs.startTag(null, OpmlSymbols.BODY)
|
||||||
|
for (feed in feeds!!) {
|
||||||
|
xs.startTag(null, OpmlSymbols.OUTLINE)
|
||||||
|
xs.attribute(null, OpmlSymbols.TEXT, feed!!.title)
|
||||||
|
xs.attribute(null, OpmlSymbols.TITLE, feed.title)
|
||||||
|
if (feed.type != null) xs.attribute(null, OpmlSymbols.TYPE, feed.type)
|
||||||
|
xs.attribute(null, OpmlSymbols.XMLURL, feed.downloadUrl)
|
||||||
|
if (feed.link != null) xs.attribute(null, OpmlSymbols.HTMLURL, feed.link)
|
||||||
|
xs.endTag(null, OpmlSymbols.OUTLINE)
|
||||||
|
}
|
||||||
|
xs.endTag(null, OpmlSymbols.BODY)
|
||||||
|
xs.endTag(null, OpmlSymbols.OPML)
|
||||||
|
xs.endDocument()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileExtension(): String {
|
||||||
|
return "opml"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = OpmlWriter::class.simpleName ?: "Anonymous"
|
||||||
|
private const val ENCODING = "UTF-8"
|
||||||
|
private const val OPML_VERSION = "2.0"
|
||||||
|
private const val OPML_TITLE = "Podcini Subscriptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads OPML documents. */
|
||||||
|
class OpmlReader {
|
||||||
|
// ATTRIBUTES
|
||||||
|
private var isInOpml = false
|
||||||
|
private var elementList: ArrayList<OpmlElement>? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an Opml document and returns a list of all OPML elements it can find
|
||||||
|
* @throws IOException
|
||||||
|
* @throws XmlPullParserException
|
||||||
|
*/
|
||||||
|
@Throws(XmlPullParserException::class, IOException::class)
|
||||||
|
fun readDocument(reader: Reader?): ArrayList<OpmlElement> {
|
||||||
|
elementList = ArrayList()
|
||||||
|
val factory = XmlPullParserFactory.newInstance()
|
||||||
|
factory.isNamespaceAware = true
|
||||||
|
val xpp = factory.newPullParser()
|
||||||
|
xpp.setInput(reader)
|
||||||
|
var eventType = xpp.eventType
|
||||||
|
|
||||||
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||||
|
when (eventType) {
|
||||||
|
XmlPullParser.START_DOCUMENT -> Logd(TAG, "Reached beginning of document")
|
||||||
|
XmlPullParser.START_TAG -> when {
|
||||||
|
xpp.name == OpmlSymbols.OPML -> {
|
||||||
|
isInOpml = true
|
||||||
|
Logd(TAG, "Reached beginning of OPML tree.")
|
||||||
|
}
|
||||||
|
isInOpml && xpp.name == OpmlSymbols.OUTLINE -> {
|
||||||
|
// TODO: check more about this, java.io.IOException: Underlying input stream returned zero bytes
|
||||||
|
val element = OpmlElement()
|
||||||
|
element.text = xpp.getAttributeValue(null, OpmlSymbols.TITLE) ?: xpp.getAttributeValue(null, OpmlSymbols.TEXT)
|
||||||
|
element.xmlUrl = xpp.getAttributeValue(null, OpmlSymbols.XMLURL)
|
||||||
|
element.htmlUrl = xpp.getAttributeValue(null, OpmlSymbols.HTMLURL)
|
||||||
|
element.type = xpp.getAttributeValue(null, OpmlSymbols.TYPE)
|
||||||
|
if (element.xmlUrl != null) {
|
||||||
|
if (element.text == null) element.text = element.xmlUrl
|
||||||
|
elementList!!.add(element)
|
||||||
|
} else Logd(TAG, "Skipping element because of missing xml url")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: on first install app: java.io.IOException: Underlying input stream returned zero bytes
|
||||||
|
try {
|
||||||
|
eventType = xpp.next()
|
||||||
|
} catch(e: Exception) {
|
||||||
|
Log.e(TAG, "xpp.next() invalid: $e")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return elementList!!
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = OpmlReader::class.simpleName ?: "Anonymous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ object UserPreferences {
|
||||||
const val PREF_TINTED_COLORS: String = "prefTintedColors"
|
const val PREF_TINTED_COLORS: String = "prefTintedColors"
|
||||||
const val PREF_HIDDEN_DRAWER_ITEMS: String = "prefHiddenDrawerItems"
|
const val PREF_HIDDEN_DRAWER_ITEMS: String = "prefHiddenDrawerItems"
|
||||||
const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder"
|
const val PREF_DRAWER_FEED_ORDER: String = "prefDrawerFeedOrder"
|
||||||
|
const val PREF_FEED_GRID_LAYOUT: String = "prefFeedGridLayout"
|
||||||
const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator"
|
const val PREF_DRAWER_FEED_COUNTER: String = "prefDrawerFeedIndicator"
|
||||||
const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify"
|
const val PREF_EXPANDED_NOTIFICATION: String = "prefExpandNotify"
|
||||||
private const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
|
private const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
|
||||||
|
@ -239,6 +240,9 @@ object UserPreferences {
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val useGridLayout: Boolean
|
||||||
|
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return `true` if episodes should use their own cover, `false` otherwise
|
* @return `true` if episodes should use their own cover, `false` otherwise
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,20 +1,28 @@
|
||||||
package ac.mdiq.podcini.preferences.fragments
|
package ac.mdiq.podcini.preferences.fragments
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.BuildConfig
|
||||||
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
|
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.net.sync.SyncService.Companion.isValidGuid
|
||||||
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||||
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
|
||||||
|
import ac.mdiq.podcini.net.sync.model.SyncServiceException
|
||||||
|
import ac.mdiq.podcini.preferences.ExportWriter
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
|
import ac.mdiq.podcini.preferences.UserPreferences.getDataFolder
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.transport.DatabaseTransporter
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.transport.PreferencesTransporter
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.transport.ExportWriter
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.transport.EpisodeProgressReader
|
import ac.mdiq.podcini.preferences.OpmlTransporter.*
|
||||||
import ac.mdiq.podcini.storage.transport.EpisodesProgressWriter
|
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||||
import ac.mdiq.podcini.storage.transport.FavoritesWriter
|
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||||
import ac.mdiq.podcini.storage.transport.HtmlWriter
|
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||||
import ac.mdiq.podcini.storage.transport.OpmlWriter
|
|
||||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
@ -23,16 +31,20 @@ import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.text.format.Formatter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.core.app.ShareCompat.IntentBuilder
|
import androidx.core.app.ShareCompat.IntentBuilder
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
@ -40,7 +52,12 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.apache.commons.io.FileUtils
|
||||||
|
import org.apache.commons.io.IOUtils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.json.JSONArray
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -87,10 +104,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.import_export_pref)
|
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.import_export_pref)
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun onStop() {
|
|
||||||
// super.onStop()
|
|
||||||
// }
|
|
||||||
|
|
||||||
private fun dateStampFilename(fname: String): String {
|
private fun dateStampFilename(fname: String): String {
|
||||||
return String.format(fname, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()))
|
return String.format(fname, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()))
|
||||||
}
|
}
|
||||||
|
@ -136,7 +149,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
exportPreferences()
|
exportPreferences()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||||
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
|
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
|
||||||
true
|
true
|
||||||
|
@ -166,7 +178,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
try {
|
try {
|
||||||
val output = worker.exportFile()
|
val output = worker.exportFile()
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
showExportSuccessSnackbar(output?.uri, exportType.contentType)
|
showExportSuccessSnackbar(output.uri, exportType.contentType)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showExportErrorDialog(e)
|
showExportErrorDialog(e)
|
||||||
|
@ -412,7 +424,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class DocumentFileExportWorker(private val exportWriter: ExportWriter, private val context: Context, private val outputFileUri: Uri) {
|
class DocumentFileExportWorker(private val exportWriter: ExportWriter, private val context: Context, private val outputFileUri: Uri) {
|
||||||
|
|
||||||
suspend fun exportFile(): DocumentFile {
|
suspend fun exportFile(): DocumentFile {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val output = DocumentFile.fromSingleUri(context, outputFileUri)
|
val output = DocumentFile.fromSingleUri(context, outputFileUri)
|
||||||
|
@ -429,20 +440,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw e
|
throw e
|
||||||
} finally {
|
} finally {
|
||||||
if (writer != null) {
|
if (writer != null) try { writer.close() } catch (e: IOException) { throw e }
|
||||||
try {
|
if (outputStream != null) try { outputStream.close() } catch (e: IOException) { throw e }
|
||||||
writer.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (outputStream != null) {
|
|
||||||
try {
|
|
||||||
outputStream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,15 +451,13 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
* Writes an OPML file into the export directory in the background.
|
* Writes an OPML file into the export directory in the background.
|
||||||
*/
|
*/
|
||||||
class ExportWorker private constructor(private val exportWriter: ExportWriter, private val output: File, private val context: Context) {
|
class ExportWorker private constructor(private val exportWriter: ExportWriter, private val output: File, private val context: Context) {
|
||||||
|
|
||||||
constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR),
|
constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR),
|
||||||
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context)
|
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context)
|
||||||
|
|
||||||
suspend fun exportFile(): File? {
|
suspend fun exportFile(): File? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
if (output.exists()) {
|
if (output.exists()) {
|
||||||
val success = output.delete()
|
val success = output.delete()
|
||||||
Log.w(TAG, "Overwriting previously exported file: $success")
|
Logd(TAG, "Overwriting previously exported file: $success")
|
||||||
}
|
}
|
||||||
|
|
||||||
var writer: OutputStreamWriter? = null
|
var writer: OutputStreamWriter? = null
|
||||||
|
@ -476,7 +473,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val EXPORT_DIR = "export/"
|
private const val EXPORT_DIR = "export/"
|
||||||
private val TAG: String = ExportWorker::class.simpleName ?: "Anonymous"
|
private val TAG: String = ExportWorker::class.simpleName ?: "Anonymous"
|
||||||
|
@ -484,6 +480,397 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object PreferencesTransporter {
|
||||||
|
private val TAG: String = PreferencesTransporter::class.simpleName ?: "Anonymous"
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun exportToDocument(uri: Uri, context: Context) {
|
||||||
|
try {
|
||||||
|
val chosenDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Destination directory is not valid")
|
||||||
|
val exportSubDir = chosenDir.createDirectory("Podcini-Prefs") ?: throw IOException("Error creating subdirectory Podcini-Prefs")
|
||||||
|
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
|
||||||
|
file.name.startsWith("shared_prefs")
|
||||||
|
}?.firstOrNull()
|
||||||
|
if (sharedPreferencesDir != null) {
|
||||||
|
sharedPreferencesDir.listFiles()!!.forEach { file ->
|
||||||
|
val destFile = exportSubDir.createFile("text/xml", file.name)
|
||||||
|
if (destFile != null) copyFile(file, destFile, context)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.e("Error", "shared_prefs directory not found")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
throw e
|
||||||
|
} finally { }
|
||||||
|
}
|
||||||
|
private fun copyFile(sourceFile: File, destFile: DocumentFile, context: Context) {
|
||||||
|
try {
|
||||||
|
val inputStream = FileInputStream(sourceFile)
|
||||||
|
val outputStream = context.contentResolver.openOutputStream(destFile.uri)
|
||||||
|
if (outputStream != null) copyStream(inputStream, outputStream)
|
||||||
|
inputStream.close()
|
||||||
|
outputStream?.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("Error", "Error copying file: $e")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun copyFile(sourceFile: DocumentFile, destFile: File, context: Context) {
|
||||||
|
try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(sourceFile.uri)
|
||||||
|
val outputStream = FileOutputStream(destFile)
|
||||||
|
if (inputStream != null) copyStream(inputStream, outputStream)
|
||||||
|
inputStream?.close()
|
||||||
|
outputStream.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("Error", "Error copying file: $e")
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
|
||||||
|
val buffer = ByteArray(1024)
|
||||||
|
var bytesRead: Int
|
||||||
|
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun importBackup(uri: Uri, context: Context) {
|
||||||
|
try {
|
||||||
|
val exportedDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Backup directory is not valid")
|
||||||
|
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
|
||||||
|
file.name.startsWith("shared_prefs")
|
||||||
|
}?.firstOrNull()
|
||||||
|
if (sharedPreferencesDir != null) {
|
||||||
|
sharedPreferencesDir.listFiles()?.forEach { file ->
|
||||||
|
// val prefName = file.name.substring(0, file.name.lastIndexOf('.'))
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
} else Log.e("Error", "shared_prefs directory not found")
|
||||||
|
val files = exportedDir.listFiles()
|
||||||
|
var hasPodciniRPrefs = false
|
||||||
|
for (file in files) {
|
||||||
|
if (file?.isFile == true && file.name?.endsWith(".xml") == true && file.name!!.contains("podcini.R")) {
|
||||||
|
hasPodciniRPrefs = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (file in files) {
|
||||||
|
if (file?.isFile == true && file.name?.endsWith(".xml") == true) {
|
||||||
|
var destName = file.name!!
|
||||||
|
// contains info on existing widgets, no need to import
|
||||||
|
if (destName.contains("PlayerWidgetPrefs")) continue
|
||||||
|
// for importing from Podcini version 5 and below
|
||||||
|
if (!hasPodciniRPrefs) {
|
||||||
|
when {
|
||||||
|
destName.contains("podcini") -> destName = destName.replace("podcini", "podcini.R")
|
||||||
|
destName.contains("EpisodeItemListRecyclerView") -> destName = destName.replace("EpisodeItemListRecyclerView", "EpisodesRecyclerView")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when {
|
||||||
|
// for debug version importing release version
|
||||||
|
BuildConfig.DEBUG && !destName.contains(".debug") -> destName = destName.replace("podcini.R", "podcini.R.debug")
|
||||||
|
// for release version importing debug version
|
||||||
|
!BuildConfig.DEBUG && destName.contains(".debug") -> destName = destName.replace(".debug", "")
|
||||||
|
}
|
||||||
|
val destFile = File(sharedPreferencesDir, destName)
|
||||||
|
copyFile(file, destFile, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
throw e
|
||||||
|
} finally { }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object DatabaseTransporter {
|
||||||
|
private val TAG: String = DatabaseTransporter::class.simpleName ?: "Anonymous"
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun exportToDocument(uri: Uri?, context: Context) {
|
||||||
|
var pfd: ParcelFileDescriptor? = null
|
||||||
|
var fileOutputStream: FileOutputStream? = null
|
||||||
|
try {
|
||||||
|
pfd = context.contentResolver.openFileDescriptor(uri!!, "wt")
|
||||||
|
fileOutputStream = FileOutputStream(pfd!!.fileDescriptor)
|
||||||
|
exportToStream(fileOutputStream, context)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(fileOutputStream)
|
||||||
|
if (pfd != null) try { pfd.close() } catch (e: IOException) { Logd(TAG, "Unable to close ParcelFileDescriptor") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun exportToStream(outFileStream: FileOutputStream, context: Context) {
|
||||||
|
var src: FileChannel? = null
|
||||||
|
var dst: FileChannel? = null
|
||||||
|
try {
|
||||||
|
val realmPath = realm.configuration.path
|
||||||
|
Logd(TAG, "exportToStream realmPath: $realmPath")
|
||||||
|
val currentDB = File(realmPath)
|
||||||
|
if (currentDB.exists()) {
|
||||||
|
src = FileInputStream(currentDB).channel
|
||||||
|
dst = outFileStream.channel
|
||||||
|
val srcSize = src.size()
|
||||||
|
dst.transferFrom(src, 0, srcSize)
|
||||||
|
val newDstSize = dst.size()
|
||||||
|
if (newDstSize != srcSize)
|
||||||
|
throw IOException(String.format("Unable to write entire database. Expected to write %s, but wrote %s.", Formatter.formatShortFileSize(context, srcSize), Formatter.formatShortFileSize(context, newDstSize)))
|
||||||
|
} else {
|
||||||
|
throw IOException("Can not access current database")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(src)
|
||||||
|
IOUtils.closeQuietly(dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun importBackup(inputUri: Uri?, context: Context) {
|
||||||
|
val TEMP_DB_NAME = "temp.realm"
|
||||||
|
var inputStream: InputStream? = null
|
||||||
|
try {
|
||||||
|
val tempDB = context.getDatabasePath(TEMP_DB_NAME)
|
||||||
|
inputStream = context.contentResolver.openInputStream(inputUri!!)
|
||||||
|
FileUtils.copyInputStreamToFile(inputStream, tempDB)
|
||||||
|
val realmPath = realm.configuration.path
|
||||||
|
val currentDB = File(realmPath)
|
||||||
|
val success = currentDB.delete()
|
||||||
|
if (!success) throw IOException("Unable to delete old database")
|
||||||
|
FileUtils.moveFile(tempDB, currentDB)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads OPML documents. */
|
||||||
|
object EpisodeProgressReader {
|
||||||
|
private const val TAG = "EpisodeProgressReader"
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
fun readDocument(reader: Reader) {
|
||||||
|
val jsonString = reader.readText()
|
||||||
|
val jsonArray = JSONArray(jsonString)
|
||||||
|
val remoteActions = mutableListOf<EpisodeAction>()
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonAction = jsonArray.getJSONObject(i)
|
||||||
|
Logd(TAG, "Loaded EpisodeActions message: $i $jsonAction")
|
||||||
|
val action = readFromJsonObject(jsonAction) ?: continue
|
||||||
|
remoteActions.add(action)
|
||||||
|
}
|
||||||
|
if (remoteActions.isEmpty()) return
|
||||||
|
val updatedItems: MutableList<Episode> = ArrayList()
|
||||||
|
for (action in remoteActions) {
|
||||||
|
Logd(TAG, "processing action: $action")
|
||||||
|
val result = processEpisodeAction(action) ?: continue
|
||||||
|
updatedItems.add(result.second)
|
||||||
|
}
|
||||||
|
// loadAdditionalFeedItemListData(updatedItems)
|
||||||
|
// need to do it the sync way
|
||||||
|
for (episode in updatedItems) upsertBlk(episode) {}
|
||||||
|
Logd(TAG, "Parsing finished.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
private fun processEpisodeAction(action: EpisodeAction): Pair<Long, Episode>? {
|
||||||
|
val guid = if (isValidGuid(action.guid)) action.guid else null
|
||||||
|
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
||||||
|
if (feedItem == null) {
|
||||||
|
Logd(TAG, "Unknown feed item: $action")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (feedItem.media == null) {
|
||||||
|
Logd(TAG, "Feed item has no media: $action")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
var idRemove = 0L
|
||||||
|
feedItem.media!!.setPosition(action.position * 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")
|
||||||
|
return Pair(idRemove, feedItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes saved favorites to file. */
|
||||||
|
class EpisodesProgressWriter : ExportWriter {
|
||||||
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
|
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||||
|
Logd(TAG, "Starting to write document")
|
||||||
|
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
|
||||||
|
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PAUSED), SortOrder.DATE_NEW_OLD)
|
||||||
|
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PLAYED), SortOrder.DATE_NEW_OLD)
|
||||||
|
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
||||||
|
val comItems = mutableSetOf<Episode>()
|
||||||
|
comItems.addAll(pausedItems)
|
||||||
|
comItems.addAll(readItems)
|
||||||
|
comItems.addAll(favoriteItems)
|
||||||
|
Logd(TAG, "Save state for all " + comItems.size + " played episodes")
|
||||||
|
for (item in comItems) {
|
||||||
|
val media = item.media ?: continue
|
||||||
|
val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
|
||||||
|
.timestamp(Date(media.getLastPlayedTime()))
|
||||||
|
.started(media.getPosition() / 1000)
|
||||||
|
.position(media.getPosition() / 1000)
|
||||||
|
.total(media.getDuration() / 1000)
|
||||||
|
.isFavorite(item.isFavorite)
|
||||||
|
.playState(item.playState)
|
||||||
|
.build()
|
||||||
|
queuedEpisodeActions.add(played)
|
||||||
|
}
|
||||||
|
if (queuedEpisodeActions.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
Logd(TAG, "Saving ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
|
||||||
|
val list = JSONArray()
|
||||||
|
for (episodeAction in queuedEpisodeActions) {
|
||||||
|
val obj = episodeAction.writeToJsonObject()
|
||||||
|
if (obj != null) {
|
||||||
|
Logd(TAG, "saving EpisodeAction: $obj")
|
||||||
|
list.put(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writer?.write(list.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
throw SyncServiceException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Logd(TAG, "Finished writing document")
|
||||||
|
}
|
||||||
|
override fun fileExtension(): String {
|
||||||
|
return "json"
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "EpisodesProgressWriter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes saved favorites to file. */
|
||||||
|
class FavoritesWriter : ExportWriter {
|
||||||
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
|
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||||
|
Logd(TAG, "Starting to write document")
|
||||||
|
val templateStream = context!!.assets.open("html-export-template.html")
|
||||||
|
var template = IOUtils.toString(templateStream, UTF_8)
|
||||||
|
template = template.replace("\\{TITLE\\}".toRegex(), "Favorites")
|
||||||
|
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
val favTemplateStream = context.assets.open(FAVORITE_TEMPLATE)
|
||||||
|
val favTemplate = IOUtils.toString(favTemplateStream, UTF_8)
|
||||||
|
val feedTemplateStream = context.assets.open(FEED_TEMPLATE)
|
||||||
|
val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8)
|
||||||
|
val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
||||||
|
val favoritesByFeed = buildFeedMap(allFavorites)
|
||||||
|
writer!!.append(templateParts[0])
|
||||||
|
for (feedId in favoritesByFeed.keys) {
|
||||||
|
val favorites: List<Episode> = favoritesByFeed[feedId]!!
|
||||||
|
writer.append("<li><div>\n")
|
||||||
|
writeFeed(writer, favorites[0].feed, feedTemplate)
|
||||||
|
writer.append("<ul>\n")
|
||||||
|
for (item in favorites) writeFavoriteItem(writer, item, favTemplate)
|
||||||
|
writer.append("</ul></div></li>\n")
|
||||||
|
}
|
||||||
|
writer.append(templateParts[1])
|
||||||
|
Logd(TAG, "Finished writing document")
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Group favorite episodes by feed, sorting them by publishing date in descending order.
|
||||||
|
* @param favoritesList `List` of all favorite episodes.
|
||||||
|
* @return A `Map` favorite episodes, keyed by feed ID.
|
||||||
|
*/
|
||||||
|
private fun buildFeedMap(favoritesList: List<Episode>): Map<Long, MutableList<Episode>> {
|
||||||
|
val feedMap: MutableMap<Long, MutableList<Episode>> = TreeMap()
|
||||||
|
for (item in favoritesList) {
|
||||||
|
var feedEpisodes = feedMap[item.feedId]
|
||||||
|
if (feedEpisodes == null) {
|
||||||
|
feedEpisodes = ArrayList()
|
||||||
|
if (item.feedId != null) feedMap[item.feedId!!] = feedEpisodes
|
||||||
|
}
|
||||||
|
feedEpisodes.add(item)
|
||||||
|
}
|
||||||
|
return feedMap
|
||||||
|
}
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeFeed(writer: Writer?, feed: Feed?, feedTemplate: String) {
|
||||||
|
val feedInfo = feedTemplate
|
||||||
|
.replace("{FEED_IMG}", feed!!.imageUrl!!)
|
||||||
|
.replace("{FEED_TITLE}", feed.title!!)
|
||||||
|
.replace("{FEED_LINK}", feed.link!!)
|
||||||
|
.replace("{FEED_WEBSITE}", feed.downloadUrl!!)
|
||||||
|
writer!!.append(feedInfo)
|
||||||
|
}
|
||||||
|
@Throws(IOException::class)
|
||||||
|
private fun writeFavoriteItem(writer: Writer?, item: Episode, favoriteTemplate: String) {
|
||||||
|
var favItem = favoriteTemplate.replace("{FAV_TITLE}", item.title!!.trim { it <= ' ' })
|
||||||
|
favItem = if (item.link != null) favItem.replace("{FAV_WEBSITE}", item.link!!)
|
||||||
|
else favItem.replace("{FAV_WEBSITE}", "")
|
||||||
|
favItem =
|
||||||
|
if (item.media != null && item.media!!.downloadUrl != null) favItem.replace("{FAV_MEDIA}", item.media!!.downloadUrl!!)
|
||||||
|
else favItem.replace("{FAV_MEDIA}", "")
|
||||||
|
writer!!.append(favItem)
|
||||||
|
}
|
||||||
|
override fun fileExtension(): String {
|
||||||
|
return "html"
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = FavoritesWriter::class.simpleName ?: "Anonymous"
|
||||||
|
private const val FAVORITE_TEMPLATE = "html-export-favorites-item-template.html"
|
||||||
|
private const val FEED_TEMPLATE = "html-export-feed-template.html"
|
||||||
|
private const val UTF_8 = "UTF-8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes HTML documents. */
|
||||||
|
class HtmlWriter : ExportWriter {
|
||||||
|
/**
|
||||||
|
* Takes a list of feeds and a writer and writes those into an HTML document.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||||
|
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||||
|
Logd(TAG, "Starting to write document")
|
||||||
|
|
||||||
|
val templateStream = context!!.assets.open("html-export-template.html")
|
||||||
|
var template = IOUtils.toString(templateStream, "UTF-8")
|
||||||
|
template = template.replace("\\{TITLE\\}".toRegex(), "Subscriptions")
|
||||||
|
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
|
|
||||||
|
writer!!.append(templateParts[0])
|
||||||
|
for (feed in feeds!!) {
|
||||||
|
writer.append("<li><div><img src=\"")
|
||||||
|
writer.append(feed!!.imageUrl)
|
||||||
|
writer.append("\" /><p>")
|
||||||
|
writer.append(feed.title)
|
||||||
|
writer.append(" <span><a href=\"")
|
||||||
|
writer.append(feed.link)
|
||||||
|
writer.append("\">Website</a> • <a href=\"")
|
||||||
|
writer.append(feed.downloadUrl)
|
||||||
|
writer.append("\">Feed</a></span></p></div></li>\n")
|
||||||
|
}
|
||||||
|
writer.append(templateParts[1])
|
||||||
|
Logd(TAG, "Finished writing document")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun fileExtension(): String {
|
||||||
|
return "html"
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = HtmlWriter::class.simpleName ?: "Anonymous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = ImportExportPreferencesFragment::class.simpleName ?: "Anonymous"
|
private val TAG: String = ImportExportPreferencesFragment::class.simpleName ?: "Anonymous"
|
||||||
private const val PREF_OPML_EXPORT = "prefOpmlExport"
|
private const val PREF_OPML_EXPORT = "prefOpmlExport"
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
package ac.mdiq.podcini.receiver
|
package ac.mdiq.podcini.receiver
|
||||||
|
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions.Companion.SWIPE_ACTIONS_PREF_NAME
|
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.appwidget.AppWidgetProvider
|
import android.appwidget.AppWidgetProvider
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.content.SharedPreferences
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class PlayerWidget : AppWidgetProvider() {
|
class PlayerWidget : AppWidgetProvider() {
|
||||||
override fun onEnabled(context: Context) {
|
override fun onEnabled(context: Context) {
|
||||||
super.onEnabled(context)
|
super.onEnabled(context)
|
||||||
|
getSharedPrefs(context)
|
||||||
Logd(TAG, "Widget enabled")
|
Logd(TAG, "Widget enabled")
|
||||||
setEnabled(context, true)
|
setEnabled(true)
|
||||||
WidgetUpdaterWorker.enqueueWork(context)
|
WidgetUpdaterWorker.enqueueWork(context)
|
||||||
scheduleWorkaround(context)
|
scheduleWorkaround(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
||||||
|
getSharedPrefs(context)
|
||||||
WidgetUpdaterWorker.enqueueWork(context)
|
WidgetUpdaterWorker.enqueueWork(context)
|
||||||
|
|
||||||
if (!prefs!!.getBoolean(KEY_WORKAROUND_ENABLED, false)) {
|
if (!prefs!!.getBoolean(KEY_WORKAROUND_ENABLED, false)) {
|
||||||
|
@ -36,7 +36,7 @@ class PlayerWidget : AppWidgetProvider() {
|
||||||
override fun onDisabled(context: Context) {
|
override fun onDisabled(context: Context) {
|
||||||
super.onDisabled(context)
|
super.onDisabled(context)
|
||||||
Logd(TAG, "Widget disabled")
|
Logd(TAG, "Widget disabled")
|
||||||
setEnabled(context, false)
|
setEnabled(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||||
|
@ -57,7 +57,7 @@ class PlayerWidget : AppWidgetProvider() {
|
||||||
super.onDeleted(context, appWidgetIds)
|
super.onDeleted(context, appWidgetIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setEnabled(context: Context, enabled: Boolean) {
|
private fun setEnabled(enabled: Boolean) {
|
||||||
prefs!!.edit().putBoolean(KEY_ENABLED, enabled).apply()
|
prefs!!.edit().putBoolean(KEY_ENABLED, enabled).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,8 +91,7 @@ class PlayerWidget : AppWidgetProvider() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun isEnabled(context: Context): Boolean {
|
fun isEnabled(): Boolean {
|
||||||
// val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
return prefs!!.getBoolean(KEY_ENABLED, false)
|
return prefs!!.getBoolean(KEY_ENABLED, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import androidx.media3.common.util.UnstableApi
|
||||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.autodownloadEpisodeMedia
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
|
||||||
// modified from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
|
// modified from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
|
||||||
|
|
|
@ -20,7 +20,19 @@ import kotlinx.coroutines.runBlocking
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
object EpisodeCleanupAlgorithmFactory {
|
object AutoCleanups {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
|
||||||
|
* 'playbackCompletionDate'-value will be deleted first.
|
||||||
|
* This method should NOT be executed on the GUI thread.
|
||||||
|
* @param context Used for accessing the DB.
|
||||||
|
*/
|
||||||
|
// only used in tests
|
||||||
|
fun performAutoCleanup(context: Context) {
|
||||||
|
build().performCleanup(context)
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun build(): EpisodeCleanupAlgorithm {
|
fun build(): EpisodeCleanupAlgorithm {
|
||||||
if (!isEnableAutodownload) return APNullCleanupAlgorithm()
|
if (!isEnableAutodownload) return APNullCleanupAlgorithm()
|
||||||
|
@ -37,18 +49,24 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
* A cleanup algorithm that removes any item that isn't a favorite but only if space is needed.
|
* A cleanup algorithm that removes any item that isn't a favorite but only if space is needed.
|
||||||
*/
|
*/
|
||||||
class ExceptFavoriteCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
class ExceptFavoriteCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
||||||
|
private val candidates: List<Episode>
|
||||||
|
get() {
|
||||||
|
val candidates: MutableList<Episode> = ArrayList()
|
||||||
|
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD)
|
||||||
|
for (item in downloadedItems) {
|
||||||
|
if (item.media != null && item.media!!.downloaded && !item.isFavorite) candidates.add(item)
|
||||||
|
}
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* The maximum number of episodes that could be cleaned up.
|
* The maximum number of episodes that could be cleaned up.
|
||||||
*
|
|
||||||
* @return the number of episodes that *could* be cleaned up, if needed
|
* @return the number of episodes that *could* be cleaned up, if needed
|
||||||
*/
|
*/
|
||||||
override fun getReclaimableItems(): Int {
|
override fun getReclaimableItems(): Int {
|
||||||
return candidates.size
|
return candidates.size
|
||||||
}
|
}
|
||||||
|
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numToRemove: Int): Int {
|
||||||
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numberOfEpisodesToDelete: Int): Int {
|
|
||||||
var candidates = candidates
|
var candidates = candidates
|
||||||
|
|
||||||
// in the absence of better data, we'll sort by item publication date
|
// in the absence of better data, we'll sort by item publication date
|
||||||
candidates = candidates.sortedWith { lhs: Episode, rhs: Episode ->
|
candidates = candidates.sortedWith { lhs: Episode, rhs: Episode ->
|
||||||
val l = lhs.getPubDate()
|
val l = lhs.getPubDate()
|
||||||
|
@ -56,9 +74,7 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
if (l != null && r != null) return@sortedWith l.compareTo(r)
|
if (l != null && r != null) return@sortedWith l.compareTo(r)
|
||||||
else return@sortedWith lhs.id.compareTo(rhs.id) // No date - compare by id which should be always incremented
|
else return@sortedWith lhs.id.compareTo(rhs.id) // No date - compare by id which should be always incremented
|
||||||
}
|
}
|
||||||
|
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||||
val delete = if (candidates.size > numberOfEpisodesToDelete) candidates.subList(0, numberOfEpisodesToDelete) else candidates
|
|
||||||
|
|
||||||
for (item in delete) {
|
for (item in delete) {
|
||||||
if (item.media == null) continue
|
if (item.media == null) continue
|
||||||
try {
|
try {
|
||||||
|
@ -69,23 +85,10 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val counter = delete.size
|
val counter = delete.size
|
||||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numberOfEpisodesToDelete))
|
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||||
|
|
||||||
return counter
|
return counter
|
||||||
}
|
}
|
||||||
|
|
||||||
private val candidates: List<Episode>
|
|
||||||
get() {
|
|
||||||
val candidates: MutableList<Episode> = ArrayList()
|
|
||||||
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.DOWNLOADED), SortOrder.DATE_NEW_OLD)
|
|
||||||
for (item in downloadedItems) {
|
|
||||||
if (item.media != null && item.media!!.downloaded && !item.isFavorite) candidates.add(item)
|
|
||||||
}
|
|
||||||
return candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
public override fun getDefaultCleanupParameter(): Int {
|
public override fun getDefaultCleanupParameter(): Int {
|
||||||
val cacheSize = episodeCacheSize
|
val cacheSize = episodeCacheSize
|
||||||
if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||||
|
@ -94,7 +97,6 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
@ -105,47 +107,6 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
* but only if space is needed.
|
* but only if space is needed.
|
||||||
*/
|
*/
|
||||||
class APQueueCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
class APQueueCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
||||||
/**
|
|
||||||
* @return the number of episodes that *could* be cleaned up, if needed
|
|
||||||
*/
|
|
||||||
override fun getReclaimableItems(): Int {
|
|
||||||
return candidates.size
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numberOfEpisodesToDelete: Int): Int {
|
|
||||||
var candidates = candidates
|
|
||||||
|
|
||||||
// in the absence of better data, we'll sort by item publication date
|
|
||||||
candidates = candidates.sortedWith { lhs: Episode, rhs: Episode ->
|
|
||||||
var l = lhs.getPubDate()
|
|
||||||
var r = rhs.getPubDate()
|
|
||||||
|
|
||||||
if (l == null) l = Date()
|
|
||||||
if (r == null) r = Date()
|
|
||||||
|
|
||||||
l.compareTo(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
val delete = if (candidates.size > numberOfEpisodesToDelete) candidates.subList(0, numberOfEpisodesToDelete) else candidates
|
|
||||||
|
|
||||||
for (item in delete) {
|
|
||||||
if (item.media == null) continue
|
|
||||||
try {
|
|
||||||
runBlocking { deleteMediaOfEpisode(context, item).join() }
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val counter = delete.size
|
|
||||||
|
|
||||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numberOfEpisodesToDelete))
|
|
||||||
|
|
||||||
return counter
|
|
||||||
}
|
|
||||||
|
|
||||||
private val candidates: List<Episode>
|
private val candidates: List<Episode>
|
||||||
get() {
|
get() {
|
||||||
val candidates: MutableList<Episode> = ArrayList()
|
val candidates: MutableList<Episode> = ArrayList()
|
||||||
|
@ -157,11 +118,40 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
}
|
}
|
||||||
return candidates
|
return candidates
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @return the number of episodes that *could* be cleaned up, if needed
|
||||||
|
*/
|
||||||
|
override fun getReclaimableItems(): Int {
|
||||||
|
return candidates.size
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numToRemove: Int): Int {
|
||||||
|
var candidates = candidates
|
||||||
|
// in the absence of better data, we'll sort by item publication date
|
||||||
|
candidates = candidates.sortedWith { lhs: Episode, rhs: Episode ->
|
||||||
|
var l = lhs.getPubDate()
|
||||||
|
var r = rhs.getPubDate()
|
||||||
|
if (l == null) l = Date()
|
||||||
|
if (r == null) r = Date()
|
||||||
|
l.compareTo(r)
|
||||||
|
}
|
||||||
|
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||||
|
for (item in delete) {
|
||||||
|
if (item.media == null) continue
|
||||||
|
try {
|
||||||
|
runBlocking { deleteMediaOfEpisode(context, item).join() }
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val counter = delete.size
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||||
|
return counter
|
||||||
|
}
|
||||||
public override fun getDefaultCleanupParameter(): Int {
|
public override fun getDefaultCleanupParameter(): Int {
|
||||||
return getNumEpisodesToCleanup(0)
|
return getNumEpisodesToCleanup(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
@ -171,20 +161,17 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
* A cleanup algorithm that never removes anything
|
* A cleanup algorithm that never removes anything
|
||||||
*/
|
*/
|
||||||
class APNullCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
class APNullCleanupAlgorithm : EpisodeCleanupAlgorithm() {
|
||||||
public override fun performCleanup(context: Context, parameter: Int): Int {
|
public override fun performCleanup(context: Context, numToRemove: Int): Int {
|
||||||
// never clean anything up
|
// never clean anything up
|
||||||
Log.i(TAG, "performCleanup: Not removing anything")
|
Log.i(TAG, "performCleanup: Not removing anything")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public override fun getDefaultCleanupParameter(): Int {
|
public override fun getDefaultCleanupParameter(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getReclaimableItems(): Int {
|
override fun getReclaimableItems(): Int {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = APNullCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
private val TAG: String = APNullCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
@ -197,49 +184,6 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
* Fractional for number of hours, e.g., 0.5 = 12 hours, 0.0416 = 1 hour. */
|
* Fractional for number of hours, e.g., 0.5 = 12 hours, 0.0416 = 1 hour. */
|
||||||
|
|
||||||
class APCleanupAlgorithm(@JvmField @get:VisibleForTesting val numberOfHoursAfterPlayback: Int) : EpisodeCleanupAlgorithm() {
|
class APCleanupAlgorithm(@JvmField @get:VisibleForTesting val numberOfHoursAfterPlayback: Int) : EpisodeCleanupAlgorithm() {
|
||||||
/**
|
|
||||||
* @return the number of episodes that *could* be cleaned up, if needed
|
|
||||||
*/
|
|
||||||
override fun getReclaimableItems(): Int {
|
|
||||||
return candidates.size
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numberOfEpisodesToDelete: Int): Int {
|
|
||||||
val candidates = candidates.toMutableList()
|
|
||||||
|
|
||||||
candidates.sortWith { lhs: Episode, rhs: Episode ->
|
|
||||||
var l = lhs.media!!.playbackCompletionDate
|
|
||||||
var r = rhs.media!!.playbackCompletionDate
|
|
||||||
|
|
||||||
if (l == null) l = Date()
|
|
||||||
if (r == null) r = Date()
|
|
||||||
|
|
||||||
l.compareTo(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
val delete = if (candidates.size > numberOfEpisodesToDelete) candidates.subList(0, numberOfEpisodesToDelete) else candidates
|
|
||||||
|
|
||||||
for (item in delete) {
|
|
||||||
try {
|
|
||||||
runBlocking { deleteMediaOfEpisode(context, item).join() }
|
|
||||||
} catch (e: InterruptedException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
} catch (e: ExecutionException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val counter = delete.size
|
|
||||||
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numberOfEpisodesToDelete))
|
|
||||||
|
|
||||||
return counter
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
fun calcMostRecentDateForDeletion(currentDate: Date): Date {
|
|
||||||
return minusHours(currentDate, numberOfHoursAfterPlayback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val candidates: List<Episode>
|
private val candidates: List<Episode>
|
||||||
get() {
|
get() {
|
||||||
val candidates: MutableList<Episode> = ArrayList()
|
val candidates: MutableList<Episode> = ArrayList()
|
||||||
|
@ -249,27 +193,54 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
for (item in downloadedItems) {
|
for (item in downloadedItems) {
|
||||||
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && item.isPlayed() && !item.isFavorite) {
|
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && item.isPlayed() && !item.isFavorite) {
|
||||||
val media = item.media
|
val media = item.media
|
||||||
// make sure this candidate was played at least the proper amount of days prior
|
// make sure this candidate was played at least the proper amount of days prior to now
|
||||||
// to now
|
if (media?.playbackCompletionDate != null && media.playbackCompletionDate!!.before(mostRecentDateForDeletion)) candidates.add(item)
|
||||||
if (media?.playbackCompletionDate != null && media.playbackCompletionDate!!.before(mostRecentDateForDeletion))
|
|
||||||
candidates.add(item)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return candidates
|
return candidates
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* @return the number of episodes that *could* be cleaned up, if needed
|
||||||
|
*/
|
||||||
|
override fun getReclaimableItems(): Int {
|
||||||
|
return candidates.size
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class) public override fun performCleanup(context: Context, numToRemove: Int): Int {
|
||||||
|
val candidates = candidates.toMutableList()
|
||||||
|
candidates.sortWith { lhs: Episode, rhs: Episode ->
|
||||||
|
var l = lhs.media!!.playbackCompletionDate
|
||||||
|
var r = rhs.media!!.playbackCompletionDate
|
||||||
|
if (l == null) l = Date()
|
||||||
|
if (r == null) r = Date()
|
||||||
|
l.compareTo(r)
|
||||||
|
}
|
||||||
|
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||||
|
for (item in delete) {
|
||||||
|
try {
|
||||||
|
runBlocking { deleteMediaOfEpisode(context, item).join() }
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val counter = delete.size
|
||||||
|
Log.i(TAG, String.format(Locale.US, "Auto-delete deleted %d episodes (%d requested)", counter, numToRemove))
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
@VisibleForTesting
|
||||||
|
fun calcMostRecentDateForDeletion(currentDate: Date): Date {
|
||||||
|
return minusHours(currentDate, numberOfHoursAfterPlayback)
|
||||||
|
}
|
||||||
public override fun getDefaultCleanupParameter(): Int {
|
public override fun getDefaultCleanupParameter(): Int {
|
||||||
return getNumEpisodesToCleanup(0)
|
return getNumEpisodesToCleanup(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||||
private fun minusHours(baseDate: Date, numberOfHours: Int): Date {
|
private fun minusHours(baseDate: Date, numberOfHours: Int): Date {
|
||||||
val cal = Calendar.getInstance()
|
val cal = Calendar.getInstance()
|
||||||
cal.time = baseDate
|
cal.time = baseDate
|
||||||
|
|
||||||
cal.add(Calendar.HOUR_OF_DAY, -1 * numberOfHours)
|
cal.add(Calendar.HOUR_OF_DAY, -1 * numberOfHours)
|
||||||
|
|
||||||
return cal.time
|
return cal.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -286,22 +257,17 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
* @return The number of episodes that were deleted.
|
* @return The number of episodes that were deleted.
|
||||||
*/
|
*/
|
||||||
protected abstract fun performCleanup(context: Context, numToRemove: Int): Int
|
protected abstract fun performCleanup(context: Context, numToRemove: Int): Int
|
||||||
|
|
||||||
fun performCleanup(context: Context): Int {
|
fun performCleanup(context: Context): Int {
|
||||||
return performCleanup(context, getDefaultCleanupParameter())
|
return performCleanup(context, getDefaultCleanupParameter())
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fun getDefaultCleanupParameter(): Int
|
|
||||||
/**
|
/**
|
||||||
* Returns a parameter for performCleanup. The implementation of this interface should decide how much
|
* Returns a parameter for performCleanup. The implementation of this interface should decide how much
|
||||||
* space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this
|
* space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this
|
||||||
* method should not have any effects.
|
* method should not have any effects.
|
||||||
*/
|
*/
|
||||||
|
protected abstract fun getDefaultCleanupParameter(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleans up just enough episodes to make room for the requested number
|
* Cleans up just enough episodes to make room for the requested number
|
||||||
*
|
|
||||||
* @param context Can be used for accessing the database
|
* @param context Can be used for accessing the database
|
||||||
* @param amountOfRoomNeeded the number of episodes we need space for
|
* @param amountOfRoomNeeded the number of episodes we need space for
|
||||||
* @return The number of epiosdes that were deleted
|
* @return The number of epiosdes that were deleted
|
||||||
|
@ -309,12 +275,10 @@ object EpisodeCleanupAlgorithmFactory {
|
||||||
fun makeRoomForEpisodes(context: Context, amountOfRoomNeeded: Int): Int {
|
fun makeRoomForEpisodes(context: Context, amountOfRoomNeeded: Int): Int {
|
||||||
return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded))
|
return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the number of episodes/items that *could* be cleaned up, if needed
|
* @return the number of episodes/items that *could* be cleaned up, if needed
|
||||||
*/
|
*/
|
||||||
abstract fun getReclaimableItems(): Int
|
abstract fun getReclaimableItems(): Int
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param amountOfRoomNeeded the number of episodes we want to download
|
* @param amountOfRoomNeeded the number of episodes we want to download
|
||||||
* @return the number of episodes to delete in order to make room
|
* @return the number of episodes to delete in order to make room
|
|
@ -0,0 +1,130 @@
|
||||||
|
package ac.mdiq.podcini.storage.algorithms
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||||
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
|
||||||
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
|
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||||
|
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.os.BatteryManager
|
||||||
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import java.util.concurrent.ExecutorService
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
|
||||||
|
object AutoDownloads {
|
||||||
|
private val TAG: String = AutoDownloads::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executor service used by the autodownloadUndownloadedEpisodes method.
|
||||||
|
*/
|
||||||
|
private val autodownloadExec: ExecutorService = Executors.newSingleThreadExecutor { r: Runnable? ->
|
||||||
|
val t = Thread(r)
|
||||||
|
t.priority = Thread.MIN_PRIORITY
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadAlgorithm = AutoDownloadAlgorithm()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks for non-downloaded episodes in the queue or list of unread episodes and request a download if
|
||||||
|
* 1. Network is available
|
||||||
|
* 2. The device is charging or the user allows auto download on battery
|
||||||
|
* 3. There is free space in the episode cache
|
||||||
|
* This method is executed on an internal single thread executor.
|
||||||
|
* @param context Used for accessing the DB.
|
||||||
|
* @return A Future that can be used for waiting for the methods completion.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
fun autodownloadEpisodeMedia(context: Context): Future<*> {
|
||||||
|
Logd(TAG, "autodownloadEpisodeMedia")
|
||||||
|
return autodownloadExec.submit(downloadAlgorithm.autoDownloadEpisodeMedia(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the automatic download algorithm used by Podcini. This class assumes that
|
||||||
|
* the client uses the [EpisodeCleanupAlgorithm].
|
||||||
|
*/
|
||||||
|
open class AutoDownloadAlgorithm {
|
||||||
|
/**
|
||||||
|
* Looks for undownloaded episodes in the queue or list of new items and request a download if
|
||||||
|
* 1. Network is available
|
||||||
|
* 2. The device is charging or the user allows auto download on battery
|
||||||
|
* 3. There is free space in the episode cache
|
||||||
|
* This method is executed on an internal single thread executor.
|
||||||
|
* @param context Used for accessing the DB.
|
||||||
|
* @return A Runnable that will be submitted to an ExecutorService.
|
||||||
|
*/
|
||||||
|
@UnstableApi
|
||||||
|
open fun autoDownloadEpisodeMedia(context: Context): Runnable? {
|
||||||
|
return Runnable {
|
||||||
|
// true if we should auto download based on network status
|
||||||
|
// val networkShouldAutoDl = (isAutoDownloadAllowed)
|
||||||
|
val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
|
||||||
|
// true if we should auto download based on power status
|
||||||
|
val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery)
|
||||||
|
Logd(TAG, "prepare autoDownloadUndownloadedItems $networkShouldAutoDl $powerShouldAutoDl")
|
||||||
|
// we should only auto download if both network AND power are happy
|
||||||
|
if (networkShouldAutoDl && powerShouldAutoDl) {
|
||||||
|
Logd(TAG, "Performing auto-dl of undownloaded episodes")
|
||||||
|
val candidates: MutableList<Episode>
|
||||||
|
val queue = curQueue.episodes
|
||||||
|
val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.NEW), SortOrder.DATE_NEW_OLD)
|
||||||
|
Logd(TAG, "newItems: ${newItems.size}")
|
||||||
|
candidates = ArrayList(queue.size + newItems.size)
|
||||||
|
candidates.addAll(queue)
|
||||||
|
for (newItem in newItems) {
|
||||||
|
val feedPrefs = newItem.feed!!.preferences
|
||||||
|
if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.filter.shouldAutoDownload(newItem)) candidates.add(newItem)
|
||||||
|
}
|
||||||
|
// filter items that are not auto downloadable
|
||||||
|
val it = candidates.iterator()
|
||||||
|
while (it.hasNext()) {
|
||||||
|
val item = it.next()
|
||||||
|
if (!item.isAutoDownloadEnabled || item.isDownloaded || item.media == null || isCurMedia(item.media) || item.feed?.isLocalFeed == true)
|
||||||
|
it.remove()
|
||||||
|
}
|
||||||
|
val autoDownloadableEpisodes = candidates.size
|
||||||
|
val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED))
|
||||||
|
val deletedEpisodes = AutoCleanups.build().makeRoomForEpisodes(context, autoDownloadableEpisodes)
|
||||||
|
val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
|
||||||
|
val episodeCacheSize = episodeCacheSize
|
||||||
|
val episodeSpaceLeft =
|
||||||
|
if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) autoDownloadableEpisodes
|
||||||
|
else episodeCacheSize - (downloadedEpisodes - deletedEpisodes)
|
||||||
|
val itemsToDownload: List<Episode> = candidates.subList(0, episodeSpaceLeft)
|
||||||
|
if (itemsToDownload.isNotEmpty()) {
|
||||||
|
Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download")
|
||||||
|
for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return true if the device is charging
|
||||||
|
*/
|
||||||
|
private fun deviceCharging(context: Context): Boolean {
|
||||||
|
// from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
|
||||||
|
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||||
|
val batteryStatus = context.registerReceiver(null, iFilter)
|
||||||
|
|
||||||
|
val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||||
|
return (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL)
|
||||||
|
}
|
||||||
|
companion object {
|
||||||
|
private val TAG: String = AutoDownloadAlgorithm::class.simpleName ?: "Anonymous"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,18 +5,11 @@ import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
|
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.ACTION_SHUTDOWN_PLAYBACK_SERVICE
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
|
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory
|
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueues
|
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueues
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
|
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
|
@ -29,7 +22,6 @@ import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||||
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.PowerUtils.deviceCharging
|
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor
|
import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor
|
||||||
|
@ -43,28 +35,12 @@ import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.DateFormat
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutorService
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import java.util.concurrent.Future
|
|
||||||
import kotlin.math.abs
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
object Episodes {
|
object Episodes {
|
||||||
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
|
private val TAG: String = Episodes::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
/**
|
|
||||||
* Executor service used by the autodownloadUndownloadedEpisodes method.
|
|
||||||
*/
|
|
||||||
private val autodownloadExec: ExecutorService = Executors.newSingleThreadExecutor { r: Runnable? ->
|
|
||||||
val t = Thread(r)
|
|
||||||
t.priority = Thread.MIN_PRIORITY
|
|
||||||
t
|
|
||||||
}
|
|
||||||
|
|
||||||
var downloadAlgorithm = AutomaticDownloadAlgorithm()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param offset The first episode that should be loaded.
|
* @param offset The first episode that should be loaded.
|
||||||
* @param limit The maximum number of episodes that should be loaded.
|
* @param limit The maximum number of episodes that should be loaded.
|
||||||
|
@ -113,101 +89,6 @@ object Episodes {
|
||||||
return if (media != null) realm.copyFromRealm(media) else null
|
return if (media != null) realm.copyFromRealm(media) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Looks for non-downloaded episodes in the queue or list of unread episodes and request a download if
|
|
||||||
* 1. Network is available
|
|
||||||
* 2. The device is charging or the user allows auto download on battery
|
|
||||||
* 3. There is free space in the episode cache
|
|
||||||
* This method is executed on an internal single thread executor.
|
|
||||||
* @param context Used for accessing the DB.
|
|
||||||
* @return A Future that can be used for waiting for the methods completion.
|
|
||||||
*/
|
|
||||||
@UnstableApi
|
|
||||||
fun autodownloadEpisodeMedia(context: Context): Future<*> {
|
|
||||||
Logd(TAG, "autodownloadEpisodeMedia")
|
|
||||||
return autodownloadExec.submit(downloadAlgorithm.autoDownloadEpisodeMedia(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller
|
|
||||||
* 'playbackCompletionDate'-value will be deleted first.
|
|
||||||
* This method should NOT be executed on the GUI thread.
|
|
||||||
* @param context Used for accessing the DB.
|
|
||||||
*/
|
|
||||||
fun performAutoCleanup(context: Context) {
|
|
||||||
EpisodeCleanupAlgorithmFactory.build().performCleanup(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implements the automatic download algorithm used by Podcini. This class assumes that
|
|
||||||
* the client uses the [EpisodeCleanupAlgorithm].
|
|
||||||
*/
|
|
||||||
open class AutomaticDownloadAlgorithm {
|
|
||||||
/**
|
|
||||||
* Looks for undownloaded episodes in the queue or list of new items and request a download if
|
|
||||||
* 1. Network is available
|
|
||||||
* 2. The device is charging or the user allows auto download on battery
|
|
||||||
* 3. There is free space in the episode cache
|
|
||||||
* This method is executed on an internal single thread executor.
|
|
||||||
* @param context Used for accessing the DB.
|
|
||||||
* @return A Runnable that will be submitted to an ExecutorService.
|
|
||||||
*/
|
|
||||||
@UnstableApi open fun autoDownloadEpisodeMedia(context: Context): Runnable? {
|
|
||||||
return Runnable {
|
|
||||||
// true if we should auto download based on network status
|
|
||||||
// val networkShouldAutoDl = (isAutoDownloadAllowed)
|
|
||||||
val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
|
|
||||||
|
|
||||||
// true if we should auto download based on power status
|
|
||||||
val powerShouldAutoDl = (deviceCharging(context) || isEnableAutodownloadOnBattery)
|
|
||||||
Logd(TAG, "prepare autoDownloadUndownloadedItems $networkShouldAutoDl $powerShouldAutoDl")
|
|
||||||
|
|
||||||
// we should only auto download if both network AND power are happy
|
|
||||||
if (networkShouldAutoDl && powerShouldAutoDl) {
|
|
||||||
Logd(TAG, "Performing auto-dl of undownloaded episodes")
|
|
||||||
|
|
||||||
val candidates: MutableList<Episode>
|
|
||||||
val queue = curQueue.episodes
|
|
||||||
|
|
||||||
val newItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.NEW), SortOrder.DATE_NEW_OLD)
|
|
||||||
Logd(TAG, "newItems: ${newItems.size}")
|
|
||||||
candidates = ArrayList(queue.size + newItems.size)
|
|
||||||
candidates.addAll(queue)
|
|
||||||
for (newItem in newItems) {
|
|
||||||
val feedPrefs = newItem.feed!!.preferences
|
|
||||||
if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.filter.shouldAutoDownload(newItem)) candidates.add(newItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
// filter items that are not auto downloadable
|
|
||||||
val it = candidates.iterator()
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val item = it.next()
|
|
||||||
if (!item.isAutoDownloadEnabled || item.isDownloaded || item.media == null || isCurMedia(item.media) || item.feed?.isLocalFeed == true)
|
|
||||||
it.remove()
|
|
||||||
}
|
|
||||||
|
|
||||||
val autoDownloadableEpisodes = candidates.size
|
|
||||||
val downloadedEpisodes = getEpisodesCount(EpisodeFilter(EpisodeFilter.DOWNLOADED))
|
|
||||||
val deletedEpisodes = EpisodeCleanupAlgorithmFactory.build().makeRoomForEpisodes(context, autoDownloadableEpisodes)
|
|
||||||
val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
|
|
||||||
val episodeCacheSize = episodeCacheSize
|
|
||||||
val episodeSpaceLeft = if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) autoDownloadableEpisodes
|
|
||||||
else episodeCacheSize - (downloadedEpisodes - deletedEpisodes)
|
|
||||||
|
|
||||||
val itemsToDownload: List<Episode> = candidates.subList(0, episodeSpaceLeft)
|
|
||||||
if (itemsToDownload.isNotEmpty()) {
|
|
||||||
Logd(TAG, "Enqueueing " + itemsToDownload.size + " items for download")
|
|
||||||
for (episode in itemsToDownload) DownloadServiceInterface.get()?.download(context, episode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else Logd(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = AutomaticDownloadAlgorithm::class.simpleName ?: "Anonymous"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @JvmStatic is needed because some Runnable blocks call this
|
// @JvmStatic is needed because some Runnable blocks call this
|
||||||
@OptIn(UnstableApi::class) @JvmStatic
|
@OptIn(UnstableApi::class) @JvmStatic
|
||||||
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job {
|
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job {
|
||||||
|
@ -228,7 +109,7 @@ object Episodes {
|
||||||
private fun deleteMediaSync(context: Context, episode: Episode): Boolean {
|
private fun deleteMediaSync(context: Context, episode: Episode): Boolean {
|
||||||
Logd(TAG, "deleteMediaSync called")
|
Logd(TAG, "deleteMediaSync called")
|
||||||
val media = episode.media ?: return false
|
val media = episode.media ?: return false
|
||||||
Log.i(TAG, String.format(Locale.US, "Requested to delete EpisodeMedia [id=%d, title=%s, downloaded=%s", media.id, media.getEpisodeTitle(), media.downloaded))
|
Logd(TAG, String.format(Locale.US, "Requested to delete EpisodeMedia [id=%d, title=%s, downloaded=%s", media.id, media.getEpisodeTitle(), media.downloaded))
|
||||||
var localDelete = false
|
var localDelete = false
|
||||||
val url = media.fileUrl
|
val url = media.fileUrl
|
||||||
when {
|
when {
|
||||||
|
@ -312,17 +193,7 @@ object Episodes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (removedFromQueue.isNotEmpty()) {
|
if (removedFromQueue.isNotEmpty()) removeFromAllQueues(*removedFromQueue.toTypedArray())
|
||||||
curQueue.episodes.clear()
|
|
||||||
curQueue.episodes.addAll(queueItems)
|
|
||||||
// upsertBlk(curQueue) {}
|
|
||||||
}
|
|
||||||
// TODO: need to update download logs?
|
|
||||||
// val adapter = getInstance()
|
|
||||||
// adapter.open()
|
|
||||||
// if (removedFromQueue.isNotEmpty()) adapter.setQueue(queueItems)
|
|
||||||
// adapter.removeFeedItems(episodes)
|
|
||||||
// adapter.close()
|
|
||||||
|
|
||||||
for (episode in removedFromQueue) EventFlow.postEvent(FlowEvent.QueueEvent.irreversibleRemoved(episode))
|
for (episode in removedFromQueue) EventFlow.postEvent(FlowEvent.QueueEvent.irreversibleRemoved(episode))
|
||||||
|
|
||||||
|
@ -372,7 +243,6 @@ object Episodes {
|
||||||
* Adds a Episode object to the playback history. A Episode object is in the playback history if
|
* Adds a Episode object to the playback history. A Episode object is in the playback history if
|
||||||
* its playback completion date is set to a non-null value. This method will set the playback completion date to the
|
* its playback completion date is set to a non-null value. This method will set the playback completion date to the
|
||||||
* current date regardless of the current value.
|
* current date regardless of the current value.
|
||||||
*
|
|
||||||
* @param episode Episode that should be added to the playback history.
|
* @param episode Episode that should be added to the playback history.
|
||||||
* @param date PlaybackCompletionDate for `media`
|
* @param date PlaybackCompletionDate for `media`
|
||||||
*/
|
*/
|
||||||
|
@ -414,67 +284,4 @@ object Episodes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishers sometimes mess up their feed by adding episodes twice or by changing the ID of existing episodes.
|
|
||||||
* This class tries to guess if publishers actually meant another episode,
|
|
||||||
* even if their feed explicitly says that the episodes are different.
|
|
||||||
*/
|
|
||||||
object EpisodeDuplicateGuesser {
|
|
||||||
fun seemDuplicates(item1: Episode, item2: Episode): Boolean {
|
|
||||||
if (sameAndNotEmpty(item1.identifier, item2.identifier)) return true
|
|
||||||
|
|
||||||
val media1 = item1.media
|
|
||||||
val media2 = item2.media
|
|
||||||
if (media1 == null || media2 == null) return false
|
|
||||||
|
|
||||||
if (sameAndNotEmpty(media1.getStreamUrl(), media2.getStreamUrl())) return true
|
|
||||||
|
|
||||||
return (titlesLookSimilar(item1, item2) && datesLookSimilar(item1, item2) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun sameAndNotEmpty(string1: String?, string2: String?): Boolean {
|
|
||||||
if (string1.isNullOrEmpty() || string2.isNullOrEmpty()) return false
|
|
||||||
return string1 == string2
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun datesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
|
||||||
if (item1.getPubDate() == null || item2.getPubDate() == null) return false
|
|
||||||
|
|
||||||
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US) // MM/DD/YY
|
|
||||||
val dateOriginal = dateFormat.format(item2.getPubDate()!!)
|
|
||||||
val dateNew = dateFormat.format(item1.getPubDate()!!)
|
|
||||||
return dateOriginal == dateNew // Same date; time is ignored.
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun durationsLookSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
|
||||||
return abs((media1.getDuration() - media2.getDuration()).toDouble()) < 10 * 60L * 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mimeTypeLooksSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
|
||||||
var mimeType1 = media1.mimeType
|
|
||||||
var mimeType2 = media2.mimeType
|
|
||||||
if (mimeType1 == null || mimeType2 == null) return true
|
|
||||||
|
|
||||||
if (mimeType1.contains("/") && mimeType2.contains("/")) {
|
|
||||||
mimeType1 = mimeType1.substring(0, mimeType1.indexOf("/"))
|
|
||||||
mimeType2 = mimeType2.substring(0, mimeType2.indexOf("/"))
|
|
||||||
}
|
|
||||||
return (mimeType1 == mimeType2)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun titlesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
|
||||||
return sameAndNotEmpty(canonicalizeTitle(item1.title), canonicalizeTitle(item2.title))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun canonicalizeTitle(title: String?): String {
|
|
||||||
if (title == null) return ""
|
|
||||||
return title
|
|
||||||
.trim { it <= ' ' }
|
|
||||||
.replace('“', '"')
|
|
||||||
.replace('”', '"')
|
|
||||||
.replace('„', '"')
|
|
||||||
.replace('—', '-')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.EpisodeDuplicateGuesser
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
import ac.mdiq.podcini.storage.database.Episodes.deleteEpisodes
|
||||||
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
|
import ac.mdiq.podcini.storage.database.LogsAndStats.addDownloadStatus
|
||||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueuesQuiet
|
||||||
|
@ -12,30 +11,28 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
import ac.mdiq.podcini.storage.model.*
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences
|
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
|
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
|
||||||
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import ac.mdiq.podcini.util.sorting.EpisodePubdateComparator
|
|
||||||
import android.app.backup.BackupManager
|
import android.app.backup.BackupManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import kotlinx.coroutines.Job
|
import io.realm.kotlin.ext.asFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import io.realm.kotlin.notifications.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
object Feeds {
|
object Feeds {
|
||||||
private val TAG: String = Feeds::class.simpleName ?: "Anonymous"
|
private val TAG: String = Feeds::class.simpleName ?: "Anonymous"
|
||||||
// internal val feeds: MutableList<Feed> = mutableListOf()
|
|
||||||
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
|
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
|
||||||
private val tags: MutableList<String> = mutableListOf()
|
private val tags: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
|
@ -47,11 +44,22 @@ object Feeds {
|
||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateFeedMap() {
|
fun updateFeedMap(feeds: List<Feed> = listOf(), wipe: Boolean = false) {
|
||||||
Logd(TAG, "updateFeedMap called")
|
Logd(TAG, "updateFeedMap called feeds: ${feeds.size} wipe: $wipe")
|
||||||
val feeds_ = realm.query(Feed::class).find()
|
when {
|
||||||
feedMap.clear()
|
feeds.isEmpty() -> {
|
||||||
feedMap.putAll(feeds_.associateBy { it.id })
|
val feeds_ = realm.query(Feed::class).find()
|
||||||
|
feedMap.clear()
|
||||||
|
feedMap.putAll(feeds_.associateBy { it.id })
|
||||||
|
}
|
||||||
|
wipe -> {
|
||||||
|
feedMap.clear()
|
||||||
|
feedMap.putAll(feeds.associateBy { it.id })
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
for (f in feeds) feedMap[f.id] = f
|
||||||
|
}
|
||||||
|
}
|
||||||
buildTags()
|
buildTags()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,21 +67,87 @@ object Feeds {
|
||||||
val tagsSet = mutableSetOf<String>()
|
val tagsSet = mutableSetOf<String>()
|
||||||
val feedsCopy = feedMap.values
|
val feedsCopy = feedMap.values
|
||||||
for (feed in feedsCopy) {
|
for (feed in feedsCopy) {
|
||||||
if (feed.preferences != null) {
|
if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
|
||||||
for (tag in feed.preferences!!.tags) {
|
|
||||||
if (tag != TAG_ROOT) tagsSet.add(tag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
tags.clear()
|
tags.clear()
|
||||||
tags.addAll(tagsSet)
|
tags.addAll(tagsSet)
|
||||||
tags.sort()
|
tags.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun monitorFeeds() {
|
||||||
|
val feeds = realm.query(Feed::class).find()
|
||||||
|
for (f in feeds) monitorFeed(f)
|
||||||
|
|
||||||
|
val feedQuery = realm.query(Feed::class)
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val feedsFlow = feedQuery.asFlow()
|
||||||
|
feedsFlow.collect { changes: ResultsChange<Feed> ->
|
||||||
|
when (changes) {
|
||||||
|
is UpdatedResults -> {
|
||||||
|
when {
|
||||||
|
changes.insertions.isNotEmpty() -> {
|
||||||
|
for (i in changes.insertions) {
|
||||||
|
Logd(TAG, "monitorFeeds inserted feed: ${changes.list[i].title}")
|
||||||
|
updateFeedMap(listOf(changes.list[i]))
|
||||||
|
monitorFeed(changes.list[i])
|
||||||
|
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ADDED, changes.list[i].id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// changes.changes.isNotEmpty() -> {
|
||||||
|
// for (i in changes.changes) {
|
||||||
|
// Logd(TAG, "monitorFeeds feed changed: ${changes.list[i].title}")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
changes.deletions.isNotEmpty() -> {
|
||||||
|
Logd(TAG, "monitorFeeds feed deleted: ${changes.deletions.size}")
|
||||||
|
updateFeedMap(changes.list, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// types other than UpdatedResults are not changes -- ignore them
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun monitorFeed(feed: Feed) {
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val feedPrefsFlow = feed.asFlow(listOf("preferences.*"))
|
||||||
|
feedPrefsFlow.collect { changes: SingleQueryChange<Feed> ->
|
||||||
|
when (changes) {
|
||||||
|
is UpdatedObject -> {
|
||||||
|
Logd(TAG, "monitorFeed UpdatedObject0 ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||||
|
updateFeedMap(listOf(changes.obj))
|
||||||
|
if (changes.isFieldChanged("preferences")) EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(changes.obj))
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
|
val feedFlow = feed.asFlow()
|
||||||
|
feedFlow.collect { changes: SingleQueryChange<Feed> ->
|
||||||
|
when (changes) {
|
||||||
|
is UpdatedObject -> {
|
||||||
|
Logd(TAG, "monitorFeed UpdatedObject ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||||
|
updateFeedMap(listOf(changes.obj))
|
||||||
|
if (changes.isFieldChanged("preferences")) EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(changes.obj))
|
||||||
|
}
|
||||||
|
is DeletedObject -> {
|
||||||
|
Logd(TAG, "monitorFeed DeletedObject ${feed.title}")
|
||||||
|
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.REMOVED, feed.id))
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getFeedListDownloadUrls(): List<String> {
|
fun getFeedListDownloadUrls(): List<String> {
|
||||||
Logd(TAG, "getFeedListDownloadUrls called")
|
Logd(TAG, "getFeedListDownloadUrls called")
|
||||||
val result: MutableList<String> = mutableListOf()
|
val result: MutableList<String> = mutableListOf()
|
||||||
// val feeds = realm.query(Feed::class).find()
|
|
||||||
for (f in feedMap.values) {
|
for (f in feedMap.values) {
|
||||||
val url = f.downloadUrl
|
val url = f.downloadUrl
|
||||||
if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) result.add(url)
|
if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) result.add(url)
|
||||||
|
@ -81,11 +155,7 @@ object Feeds {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: some callers don't need to copy
|
|
||||||
fun getFeed(feedId: Long, copy: Boolean = false): Feed? {
|
fun getFeed(feedId: Long, copy: Boolean = false): Feed? {
|
||||||
// Logd(TAG, "getFeed() called with: $feedId")
|
|
||||||
// val f = realm.query(Feed::class).query("id == $0", feedId).first().find()
|
|
||||||
// return if (f != null && f.isManaged()) realm.copyFromRealm(f) else null
|
|
||||||
val f = feedMap[feedId]
|
val f = feedMap[feedId]
|
||||||
return if (f != null) {
|
return if (f != null) {
|
||||||
if (copy) realm.copyFromRealm(f)
|
if (copy) realm.copyFromRealm(f)
|
||||||
|
@ -118,14 +188,13 @@ object Feeds {
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
|
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
|
||||||
Logd(TAG, "updateFeed called")
|
Logd(TAG, "updateFeed called")
|
||||||
// TODO: check further on enclosing in realm write block
|
|
||||||
var resultFeed: Feed?
|
var resultFeed: Feed?
|
||||||
val unlistedItems: MutableList<Episode> = ArrayList()
|
val unlistedItems: MutableList<Episode> = ArrayList()
|
||||||
|
|
||||||
// Look up feed in the feedslist
|
// Look up feed in the feedslist
|
||||||
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true)
|
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true)
|
||||||
if (savedFeed == null) {
|
if (savedFeed == null) {
|
||||||
Logd(TAG, "Found no existing Feed with title " + newFeed.title + ". Adding as new one.")
|
Logd(TAG, "Found no existing Feed with title ${newFeed.title}. Adding as new one.")
|
||||||
Logd(TAG, "newFeed.episodes: ${newFeed.episodes.size}")
|
Logd(TAG, "newFeed.episodes: ${newFeed.episodes.size}")
|
||||||
resultFeed = newFeed
|
resultFeed = newFeed
|
||||||
} else {
|
} else {
|
||||||
|
@ -217,7 +286,6 @@ object Feeds {
|
||||||
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
||||||
episode.setNew()
|
episode.setNew()
|
||||||
}
|
}
|
||||||
// idLong += 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,16 +314,16 @@ object Feeds {
|
||||||
// Update with default values that are set in database
|
// Update with default values that are set in database
|
||||||
resultFeed = searchFeedByIdentifyingValueOrID(newFeed)
|
resultFeed = searchFeedByIdentifyingValueOrID(newFeed)
|
||||||
} else persistFeedsSync(savedFeed)
|
} else persistFeedsSync(savedFeed)
|
||||||
updateFeedMap()
|
// updateFeedMap()
|
||||||
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
|
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
} catch (e: ExecutionException) {
|
} catch (e: ExecutionException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
// TODO: feedMonitor likely takes care of this
|
||||||
if (savedFeed != null) EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(savedFeed))
|
// if (savedFeed != null) EventFlow.postEvent(FlowEvent.FeedListEvent(savedFeed))
|
||||||
else EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(emptyList<Long>()))
|
// else EventFlow.postEvent(FlowEvent.FeedListEvent(emptyList<Long>()))
|
||||||
|
|
||||||
return resultFeed
|
return resultFeed
|
||||||
}
|
}
|
||||||
|
@ -302,7 +370,7 @@ object Feeds {
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
feed.lastUpdateFailed = lastUpdateFailed
|
feed.lastUpdateFailed = lastUpdateFailed
|
||||||
upsert(feed) {}
|
upsert(feed) {}
|
||||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed.id))
|
EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.ERROR, feed.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,32 +407,38 @@ object Feeds {
|
||||||
}
|
}
|
||||||
copyToRealm(feed)
|
copyToRealm(feed)
|
||||||
}
|
}
|
||||||
|
// updateFeedMap(feeds.toList())
|
||||||
}
|
}
|
||||||
for (feed in feeds) {
|
for (feed in feeds) {
|
||||||
if (!feed.isLocalFeed && feed.downloadUrl != null)
|
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedAddedIfSyncActive(context, feed.downloadUrl!!)
|
||||||
SynchronizationQueueSink.enqueueFeedAddedIfSyncActive(context, feed.downloadUrl!!)
|
|
||||||
}
|
}
|
||||||
val backupManager = BackupManager(context)
|
val backupManager = BackupManager(context)
|
||||||
backupManager.dataChanged()
|
backupManager.dataChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun persistFeedsSync(vararg feeds: Feed) {
|
private fun persistFeedsSync(vararg feeds: Feed) {
|
||||||
Logd(TAG, "persistCompleteFeeds called")
|
Logd(TAG, "persistFeedsSync called")
|
||||||
for (feed in feeds) {
|
for (feed in feeds) {
|
||||||
upsertBlk(feed) {}
|
upsertBlk(feed) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun persistFeedPreferences(feed: Feed) : Job {
|
fun persistFeedPreferences(feed: Feed) : Job {
|
||||||
Logd(TAG, "persistCompleteFeeds called")
|
Logd(TAG, "persistFeedPreferences called")
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||||
if (feed_ != null) {
|
if (feed_ != null) {
|
||||||
realm.write {
|
realm.write {
|
||||||
findLatest(feed_)?.let { it.preferences = feed.preferences }
|
findLatest(feed_)?.let {
|
||||||
|
it.preferences = feed.preferences
|
||||||
|
// updateFeedMap(listOf(it))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else upsert(feed) {}
|
} else {
|
||||||
if (feed.preferences != null) EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(feed.preferences!!))
|
upsert(feed) {}
|
||||||
|
// updateFeedMap(listOf(feed))
|
||||||
|
}
|
||||||
|
// if (feed.preferences != null) EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(feed.preferences!!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -389,11 +463,14 @@ object Feeds {
|
||||||
val episodes = feed_.episodes.toList()
|
val episodes = feed_.episodes.toList()
|
||||||
if (episodes.isNotEmpty()) episodes.forEach { e -> delete(e) }
|
if (episodes.isNotEmpty()) episodes.forEach { e -> delete(e) }
|
||||||
val feedToDelete = findLatest(feed_)
|
val feedToDelete = findLatest(feed_)
|
||||||
if (feedToDelete != null) delete(feedToDelete)
|
if (feedToDelete != null) {
|
||||||
|
delete(feedToDelete)
|
||||||
|
feedMap.remove(feedId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedRemovedIfSyncActive(context, feed.downloadUrl!!)
|
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedRemovedIfSyncActive(context, feed.downloadUrl!!)
|
||||||
if (postEvent) EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feed))
|
// if (postEvent) EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.REMOVED, feed.id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -426,4 +503,77 @@ object Feeds {
|
||||||
if (!UserPreferences.isAutoDelete) return false
|
if (!UserPreferences.isAutoDelete) return false
|
||||||
return !feed.isLocalFeed || UserPreferences.isAutoDeleteLocal
|
return !feed.isLocalFeed || UserPreferences.isAutoDeleteLocal
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares the pubDate of two FeedItems for sorting in reverse order
|
||||||
|
*/
|
||||||
|
class EpisodePubdateComparator : Comparator<Episode> {
|
||||||
|
override fun compare(lhs: Episode, rhs: Episode): Int {
|
||||||
|
return rhs.pubDate.compareTo(lhs.pubDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Publishers sometimes mess up their feed by adding episodes twice or by changing the ID of existing episodes.
|
||||||
|
* This class tries to guess if publishers actually meant another episode,
|
||||||
|
* even if their feed explicitly says that the episodes are different.
|
||||||
|
*/
|
||||||
|
object EpisodeDuplicateGuesser {
|
||||||
|
fun seemDuplicates(item1: Episode, item2: Episode): Boolean {
|
||||||
|
if (sameAndNotEmpty(item1.identifier, item2.identifier)) return true
|
||||||
|
|
||||||
|
val media1 = item1.media
|
||||||
|
val media2 = item2.media
|
||||||
|
if (media1 == null || media2 == null) return false
|
||||||
|
|
||||||
|
if (sameAndNotEmpty(media1.getStreamUrl(), media2.getStreamUrl())) return true
|
||||||
|
|
||||||
|
return (titlesLookSimilar(item1, item2) && datesLookSimilar(item1, item2) && durationsLookSimilar(media1, media2) && mimeTypeLooksSimilar(media1, media2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sameAndNotEmpty(string1: String?, string2: String?): Boolean {
|
||||||
|
if (string1.isNullOrEmpty() || string2.isNullOrEmpty()) return false
|
||||||
|
return string1 == string2
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun datesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
||||||
|
if (item1.getPubDate() == null || item2.getPubDate() == null) return false
|
||||||
|
|
||||||
|
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US) // MM/DD/YY
|
||||||
|
val dateOriginal = dateFormat.format(item2.getPubDate()!!)
|
||||||
|
val dateNew = dateFormat.format(item1.getPubDate()!!)
|
||||||
|
return dateOriginal == dateNew // Same date; time is ignored.
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun durationsLookSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
||||||
|
return abs((media1.getDuration() - media2.getDuration()).toDouble()) < 10 * 60L * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mimeTypeLooksSimilar(media1: EpisodeMedia, media2: EpisodeMedia): Boolean {
|
||||||
|
var mimeType1 = media1.mimeType
|
||||||
|
var mimeType2 = media2.mimeType
|
||||||
|
if (mimeType1 == null || mimeType2 == null) return true
|
||||||
|
|
||||||
|
if (mimeType1.contains("/") && mimeType2.contains("/")) {
|
||||||
|
mimeType1 = mimeType1.substring(0, mimeType1.indexOf("/"))
|
||||||
|
mimeType2 = mimeType2.substring(0, mimeType2.indexOf("/"))
|
||||||
|
}
|
||||||
|
return (mimeType1 == mimeType2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun titlesLookSimilar(item1: Episode, item2: Episode): Boolean {
|
||||||
|
return sameAndNotEmpty(canonicalizeTitle(item1.title), canonicalizeTitle(item2.title))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun canonicalizeTitle(title: String?): String {
|
||||||
|
if (title == null) return ""
|
||||||
|
return title
|
||||||
|
.trim { it <= ' ' }
|
||||||
|
.replace('“', '"')
|
||||||
|
.replace('”', '"')
|
||||||
|
.replace('„', '"')
|
||||||
|
.replace('—', '-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -42,9 +42,7 @@ object LogsAndStats {
|
||||||
Logd(TAG, "getStatistics called")
|
Logd(TAG, "getStatistics called")
|
||||||
|
|
||||||
val medias = realm.query(EpisodeMedia::class).find()
|
val medias = realm.query(EpisodeMedia::class).find()
|
||||||
val groupdMedias = medias.groupBy {
|
val groupdMedias = medias.groupBy { it.episode?.feedId ?: 0L }
|
||||||
it.episode?.feedId ?: 0L
|
|
||||||
}
|
|
||||||
val result = StatisticsResult()
|
val result = StatisticsResult()
|
||||||
result.oldestDate = Long.MAX_VALUE
|
result.oldestDate = Long.MAX_VALUE
|
||||||
for (fid in groupdMedias.keys) {
|
for (fid in groupdMedias.keys) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted
|
import ac.mdiq.podcini.preferences.UserPreferences.isQueueKeepSorted
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder
|
import ac.mdiq.podcini.preferences.UserPreferences.queueKeepSortedOrder
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.autodownloadEpisodeMedia
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.markPlayed
|
import ac.mdiq.podcini.storage.database.Episodes.markPlayed
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
|
@ -186,7 +186,7 @@ object Queues {
|
||||||
queue.episodes.addAll(qItems)
|
queue.episodes.addAll(qItems)
|
||||||
}
|
}
|
||||||
for (event in events) EventFlow.postEvent(event)
|
for (event in events) EventFlow.postEvent(event)
|
||||||
} else Log.w(TAG, "Queue was not modified by call to removeQueueItem")
|
} else Logd(TAG, "Queue was not modified by call to removeQueueItem")
|
||||||
|
|
||||||
// TODO: what's this for?
|
// TODO: what's this for?
|
||||||
if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context)
|
if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context)
|
||||||
|
|
|
@ -16,9 +16,12 @@ import kotlinx.coroutines.*
|
||||||
import kotlin.coroutines.ContinuationInterceptor
|
import kotlin.coroutines.ContinuationInterceptor
|
||||||
|
|
||||||
object RealmDB {
|
object RealmDB {
|
||||||
val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
|
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
|
private const val SCHEMA_VERSION_NUMBER = 4L
|
||||||
|
|
||||||
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val ioScope = CoroutineScope(Dispatchers.IO)
|
|
||||||
val realm: Realm
|
val realm: Realm
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
@ -33,7 +36,7 @@ object RealmDB {
|
||||||
DownloadResult::class,
|
DownloadResult::class,
|
||||||
Chapter::class))
|
Chapter::class))
|
||||||
.name("Podcini.realm")
|
.name("Podcini.realm")
|
||||||
.schemaVersion(3)
|
.schemaVersion(SCHEMA_VERSION_NUMBER)
|
||||||
.build()
|
.build()
|
||||||
realm = Realm.open(config)
|
realm = Realm.open(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ac.mdiq.podcini.storage.model
|
package ac.mdiq.podcini.storage.model
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import io.realm.kotlin.ext.realmListOf
|
import io.realm.kotlin.ext.realmListOf
|
||||||
import io.realm.kotlin.ext.realmSetOf
|
import io.realm.kotlin.ext.realmSetOf
|
||||||
import io.realm.kotlin.types.RealmList
|
import io.realm.kotlin.types.RealmList
|
||||||
|
@ -13,7 +12,6 @@ import io.realm.kotlin.types.annotations.Index
|
||||||
import io.realm.kotlin.types.annotations.PrimaryKey
|
import io.realm.kotlin.types.annotations.PrimaryKey
|
||||||
import org.apache.commons.lang3.builder.ToStringBuilder
|
import org.apache.commons.lang3.builder.ToStringBuilder
|
||||||
import org.apache.commons.lang3.builder.ToStringStyle
|
import org.apache.commons.lang3.builder.ToStringStyle
|
||||||
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +52,6 @@ class Episode : RealmObject {
|
||||||
@Ignore
|
@Ignore
|
||||||
var feed: Feed? = null
|
var feed: Feed? = null
|
||||||
get() {
|
get() {
|
||||||
// Logd(TAG, "feed.get() ${field == null} ${title}")
|
|
||||||
if (field == null && feedId != null) field = getFeed(feedId!!)
|
if (field == null && feedId != null) field = getFeed(feedId!!)
|
||||||
return field
|
return field
|
||||||
}
|
}
|
||||||
|
@ -138,26 +135,6 @@ class Episode : RealmObject {
|
||||||
// this.hasChapters = false
|
// this.hasChapters = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor is used by DBReader.
|
|
||||||
*/
|
|
||||||
// constructor(id: Long, title: String?, link: String?, pubDate: Date?, paymentLink: String?, feedId: Long,
|
|
||||||
// hasChapters: Boolean, imageUrl: String?, state: Int,
|
|
||||||
// itemIdentifier: String?, autoDownloadEnabled: Boolean, podcastIndexChapterUrl: String?) {
|
|
||||||
// this.id = id
|
|
||||||
// this.title = title
|
|
||||||
// this.link = link
|
|
||||||
// this.pubDate = pubDate?.time ?: 0
|
|
||||||
// this.paymentLink = paymentLink
|
|
||||||
// this.feedId = feedId
|
|
||||||
//// this.hasChapters = hasChapters
|
|
||||||
// this.imageUrl = imageUrl
|
|
||||||
// this.playState = state
|
|
||||||
// this.identifier = itemIdentifier
|
|
||||||
// this.isAutoDownloadEnabled = autoDownloadEnabled
|
|
||||||
// this.podcastIndexChapterUrl = podcastIndexChapterUrl
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This constructor should be used for creating test objects.
|
* This constructor should be used for creating test objects.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package ac.mdiq.podcini.storage.model
|
package ac.mdiq.podcini.storage.model
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.net.feed.parser.media.id3.ChapterReader
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
|
||||||
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
||||||
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
import ac.mdiq.podcini.util.showStackTrace
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
@ -116,14 +116,14 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||||
this.downloaded = downloaded
|
this.downloaded = downloaded
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(id: Long, item: Episode?, duration: Int, position: Int,
|
// constructor(id: Long, item: Episode?, duration: Int, position: Int,
|
||||||
size: Long, mime_type: String?, file_url: String?, download_url: String?,
|
// size: Long, mime_type: String?, file_url: String?, download_url: String?,
|
||||||
downloaded: Boolean, playbackCompletionDate: Date?, played_duration: Int,
|
// downloaded: Boolean, playbackCompletionDate: Date?, played_duration: Int,
|
||||||
hasEmbeddedPicture: Boolean?, lastPlayedTime: Long)
|
// hasEmbeddedPicture: Boolean?, lastPlayedTime: Long)
|
||||||
: this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, playbackCompletionDate, played_duration, lastPlayedTime) {
|
// : this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, playbackCompletionDate, played_duration, lastPlayedTime) {
|
||||||
|
//
|
||||||
this.hasEmbeddedPicture = hasEmbeddedPicture
|
// this.hasEmbeddedPicture = hasEmbeddedPicture
|
||||||
}
|
// }
|
||||||
|
|
||||||
fun getHumanReadableIdentifier(): String? {
|
fun getHumanReadableIdentifier(): String? {
|
||||||
return if (episode?.title != null) episode!!.title else downloadUrl
|
return if (episode?.title != null) episode!!.title else downloadUrl
|
||||||
|
@ -165,16 +165,16 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||||
return duration
|
return duration
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setDuration(duration: Int) {
|
override fun setDuration(newDuration: Int) {
|
||||||
this.duration = duration
|
this.duration = newDuration
|
||||||
}
|
}
|
||||||
override fun getPosition(): Int {
|
override fun getPosition(): Int {
|
||||||
return position
|
return position
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPosition(position: Int) {
|
override fun setPosition(newPosition: Int) {
|
||||||
this.position = position
|
this.position = newPosition
|
||||||
if (position > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false)
|
if (newPosition > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastPlayedTime(): Long {
|
override fun getLastPlayedTime(): Long {
|
||||||
|
@ -247,8 +247,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
override fun getEpisodeTitle(): String {
|
override fun getEpisodeTitle(): String {
|
||||||
if (episode == null) return "No title"
|
return episode?.title ?: episode?.identifyingValue ?: "No title"
|
||||||
return if (episode!!.title != null) episode!!.title!! else episode!!.identifyingValue?:"No title"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getChapters(): List<Chapter> {
|
override fun getChapters(): List<Chapter> {
|
||||||
|
@ -264,8 +263,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFeedTitle(): String {
|
override fun getFeedTitle(): String {
|
||||||
if (episode == null) return ""
|
return episode?.feed?.title?:""
|
||||||
return episode!!.feed?.title?:""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIdentifier(): Any {
|
override fun getIdentifier(): Any {
|
||||||
|
@ -368,6 +366,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val TAG: String = EpisodeMedia::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
const val FEEDFILETYPE_FEEDMEDIA: Int = 2
|
const val FEEDFILETYPE_FEEDMEDIA: Int = 2
|
||||||
const val PLAYABLE_TYPE_FEEDMEDIA: Int = 1
|
const val PLAYABLE_TYPE_FEEDMEDIA: Int = 1
|
||||||
const val FILENAME_PREFIX_EMBEDDED_COVER: String = "metadata-retriever:"
|
const val FILENAME_PREFIX_EMBEDDED_COVER: String = "metadata-retriever:"
|
||||||
|
|
|
@ -29,7 +29,7 @@ class Feed : RealmObject {
|
||||||
|
|
||||||
var fileUrl: String? = null
|
var fileUrl: String? = null
|
||||||
var downloadUrl: String? = null
|
var downloadUrl: String? = null
|
||||||
var downloaded: Boolean = false
|
// var downloaded: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* title as defined by the feed.
|
* title as defined by the feed.
|
||||||
|
@ -156,65 +156,33 @@ class Feed : RealmObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This constructor is used for restoring a feed from the database.
|
* This constructor is used for test purposes.
|
||||||
*/
|
*/
|
||||||
constructor(id: Long, lastUpdate: String?, title: String?, customTitle: String?, link: String?,
|
constructor(id: Long, lastUpdate: String?, title: String?, link: String?, description: String?, paymentLink: String?,
|
||||||
description: String?, paymentLinks: String?, author: String?, language: String?,
|
author: String?, language: String?, type: String?, feedIdentifier: String?, imageUrl: String?, fileUrl: String?,
|
||||||
type: String?, feedIdentifier: String?, imageUrl: String?, fileUrl: String?,
|
downloadUrl: String?) {
|
||||||
downloadUrl: String?, downloaded: Boolean, paged: Boolean, nextPageLink: String?,
|
|
||||||
filter: String?, sortOrder: SortOrder?, lastUpdateFailed: Boolean) {
|
|
||||||
this.id = id
|
this.id = id
|
||||||
this.fileUrl = fileUrl
|
this.fileUrl = fileUrl
|
||||||
this.downloadUrl = downloadUrl
|
this.downloadUrl = downloadUrl
|
||||||
this.downloaded = downloaded
|
|
||||||
this.eigenTitle = title
|
this.eigenTitle = title
|
||||||
this.customTitle = customTitle
|
this.customTitle = customTitle
|
||||||
this.lastUpdate = lastUpdate
|
this.lastUpdate = lastUpdate
|
||||||
this.link = link
|
this.link = link
|
||||||
this.description = description
|
this.description = description
|
||||||
this.paymentLinks = extractPaymentLinks(paymentLinks)
|
this.paymentLinks = extractPaymentLinks(paymentLink)
|
||||||
this.author = author
|
this.author = author
|
||||||
this.language = language
|
this.language = language
|
||||||
this.type = type
|
this.type = type
|
||||||
this.identifier = feedIdentifier
|
this.identifier = feedIdentifier
|
||||||
this.imageUrl = imageUrl
|
this.imageUrl = imageUrl
|
||||||
this.isPaged = paged
|
this.isPaged = false
|
||||||
this.nextPageLink = nextPageLink
|
this.nextPageLink = nextPageLink
|
||||||
// if (filter != null) this.episodeFilter = EpisodeFilter(filter)
|
this.preferences?.filterString = ""
|
||||||
// else this.episodeFilter = EpisodeFilter()
|
|
||||||
this.preferences?.filterString = filter ?: ""
|
|
||||||
this.sortOrder = sortOrder
|
this.sortOrder = sortOrder
|
||||||
this.preferences?.sortOrderCode = sortOrder?.code ?: 0
|
this.preferences?.sortOrderCode = sortOrder?.code ?: 0
|
||||||
this.lastUpdateFailed = lastUpdateFailed
|
this.lastUpdateFailed = lastUpdateFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This constructor is used for test purposes.
|
|
||||||
*/
|
|
||||||
constructor(id: Long, lastUpdate: String?, title: String?, link: String?, description: String?, paymentLink: String?,
|
|
||||||
author: String?, language: String?, type: String?, feedIdentifier: String?, imageUrl: String?, fileUrl: String?,
|
|
||||||
downloadUrl: String?, downloaded: Boolean)
|
|
||||||
: this(id,
|
|
||||||
lastUpdate,
|
|
||||||
title,
|
|
||||||
null,
|
|
||||||
link,
|
|
||||||
description,
|
|
||||||
paymentLink,
|
|
||||||
author,
|
|
||||||
language,
|
|
||||||
type,
|
|
||||||
feedIdentifier,
|
|
||||||
imageUrl,
|
|
||||||
fileUrl,
|
|
||||||
downloadUrl,
|
|
||||||
downloaded,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
false)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
|
* This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized.
|
||||||
*/
|
*/
|
||||||
|
@ -228,7 +196,6 @@ class Feed : RealmObject {
|
||||||
this.lastUpdate = lastUpdate
|
this.lastUpdate = lastUpdate
|
||||||
fileUrl = null
|
fileUrl = null
|
||||||
this.downloadUrl = url
|
this.downloadUrl = url
|
||||||
downloaded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -247,7 +214,7 @@ class Feed : RealmObject {
|
||||||
preferences = FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)
|
preferences = FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHumanReadableIdentifier(): String? {
|
fun getTextIdentifier(): String? {
|
||||||
return when {
|
return when {
|
||||||
!customTitle.isNullOrEmpty() -> customTitle
|
!customTitle.isNullOrEmpty() -> customTitle
|
||||||
!eigenTitle.isNullOrEmpty() -> eigenTitle
|
!eigenTitle.isNullOrEmpty() -> eigenTitle
|
||||||
|
|
|
@ -22,9 +22,6 @@ class FeedPreferences(@Index var feedID: Long,
|
||||||
@Ignore @JvmField var volumeAdaptionSetting: VolumeAdaptionSetting?,
|
@Ignore @JvmField var volumeAdaptionSetting: VolumeAdaptionSetting?,
|
||||||
var username: String?,
|
var username: String?,
|
||||||
var password: String?,
|
var password: String?,
|
||||||
/**
|
|
||||||
* @return the filter for this feed
|
|
||||||
*/
|
|
||||||
@Ignore @JvmField var filter: FeedEpisodesFilter,
|
@Ignore @JvmField var filter: FeedEpisodesFilter,
|
||||||
var playSpeed: Float,
|
var playSpeed: Float,
|
||||||
var introSkip: Int,
|
var introSkip: Int,
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
open class CommonSymbols {
|
|
||||||
companion object {
|
|
||||||
const val HEAD: String = "head"
|
|
||||||
const val BODY: String = "body"
|
|
||||||
const val TITLE: String = "title"
|
|
||||||
|
|
||||||
const val XML_FEATURE_INDENT_OUTPUT: String = "http://xmlpull.org/v1/doc/features.html#indent-output"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.ParcelFileDescriptor
|
|
||||||
import android.text.format.Formatter
|
|
||||||
import android.util.Log
|
|
||||||
import org.apache.commons.io.FileUtils
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import java.io.*
|
|
||||||
import java.nio.channels.FileChannel
|
|
||||||
|
|
||||||
object DatabaseTransporter {
|
|
||||||
private val TAG: String = DatabaseTransporter::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun exportToDocument(uri: Uri?, context: Context) {
|
|
||||||
var pfd: ParcelFileDescriptor? = null
|
|
||||||
var fileOutputStream: FileOutputStream? = null
|
|
||||||
try {
|
|
||||||
pfd = context.contentResolver.openFileDescriptor(uri!!, "wt")
|
|
||||||
fileOutputStream = FileOutputStream(pfd!!.fileDescriptor)
|
|
||||||
exportToStream(fileOutputStream, context)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
IOUtils.closeQuietly(fileOutputStream)
|
|
||||||
if (pfd != null) {
|
|
||||||
try {
|
|
||||||
pfd.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Logd(TAG, "Unable to close ParcelFileDescriptor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun exportToStream(outFileStream: FileOutputStream, context: Context) {
|
|
||||||
var src: FileChannel? = null
|
|
||||||
var dst: FileChannel? = null
|
|
||||||
try {
|
|
||||||
val realmPath = realm.configuration.path
|
|
||||||
Logd(TAG, "exportToStream realmPath: $realmPath")
|
|
||||||
val currentDB = File(realmPath)
|
|
||||||
|
|
||||||
if (currentDB.exists()) {
|
|
||||||
src = FileInputStream(currentDB).channel
|
|
||||||
dst = outFileStream.channel
|
|
||||||
val srcSize = src.size()
|
|
||||||
dst.transferFrom(src, 0, srcSize)
|
|
||||||
|
|
||||||
val newDstSize = dst.size()
|
|
||||||
if (newDstSize != srcSize)
|
|
||||||
throw IOException(String.format("Unable to write entire database. Expected to write %s, but wrote %s.", Formatter.formatShortFileSize(context, srcSize), Formatter.formatShortFileSize(context, newDstSize)))
|
|
||||||
} else {
|
|
||||||
throw IOException("Can not access current database")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
IOUtils.closeQuietly(src)
|
|
||||||
IOUtils.closeQuietly(dst)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun importBackup(inputUri: Uri?, context: Context) {
|
|
||||||
val TEMP_DB_NAME = "temp.realm"
|
|
||||||
var inputStream: InputStream? = null
|
|
||||||
try {
|
|
||||||
val tempDB = context.getDatabasePath(TEMP_DB_NAME)
|
|
||||||
inputStream = context.contentResolver.openInputStream(inputUri!!)
|
|
||||||
FileUtils.copyInputStreamToFile(inputStream, tempDB)
|
|
||||||
|
|
||||||
val realmPath = realm.configuration.path
|
|
||||||
val currentDB = File(realmPath)
|
|
||||||
val success = currentDB.delete()
|
|
||||||
if (!success) throw IOException("Unable to delete old database")
|
|
||||||
|
|
||||||
FileUtils.moveFile(tempDB, currentDB)
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
IOUtils.closeQuietly(inputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.sync.SyncService.Companion.isValidGuid
|
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.persistEpisodes
|
|
||||||
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.utils.EpisodeUtil.hasAlmostEnded
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import org.json.JSONArray
|
|
||||||
import java.io.Reader
|
|
||||||
|
|
||||||
/** Reads OPML documents. */
|
|
||||||
object EpisodeProgressReader {
|
|
||||||
private const val TAG = "EpisodeProgressReader"
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
fun readDocument(reader: Reader) {
|
|
||||||
val jsonString = reader.readText()
|
|
||||||
val jsonArray = JSONArray(jsonString)
|
|
||||||
val remoteActions = mutableListOf<EpisodeAction>()
|
|
||||||
for (i in 0 until jsonArray.length()) {
|
|
||||||
val jsonAction = jsonArray.getJSONObject(i)
|
|
||||||
Logd(TAG, "Loaded EpisodeActions message: $i $jsonAction")
|
|
||||||
val action = readFromJsonObject(jsonAction) ?: continue
|
|
||||||
remoteActions.add(action)
|
|
||||||
}
|
|
||||||
if (remoteActions.isEmpty()) return
|
|
||||||
|
|
||||||
val updatedItems: MutableList<Episode> = ArrayList()
|
|
||||||
for (action in remoteActions) {
|
|
||||||
Logd(TAG, "processing action: $action")
|
|
||||||
val result = processEpisodeAction(action) ?: continue
|
|
||||||
updatedItems.add(result.second)
|
|
||||||
}
|
|
||||||
// loadAdditionalFeedItemListData(updatedItems)
|
|
||||||
// need to do it the sync way
|
|
||||||
for (episode in updatedItems) {
|
|
||||||
upsertBlk(episode) {}
|
|
||||||
}
|
|
||||||
Logd(TAG, "Parsing finished.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun processEpisodeAction(action: EpisodeAction): Pair<Long, Episode>? {
|
|
||||||
val guid = if (isValidGuid(action.guid)) action.guid else null
|
|
||||||
val feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
|
||||||
if (feedItem == null) {
|
|
||||||
Log.i(TAG, "Unknown feed item: $action")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (feedItem.media == null) {
|
|
||||||
Log.i(TAG, "Feed item has no media: $action")
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
var idRemove = 0L
|
|
||||||
feedItem.media!!.setPosition(action.position * 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")
|
|
||||||
|
|
||||||
return Pair(idRemove, feedItem)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
|
||||||
import ac.mdiq.podcini.net.sync.model.SyncServiceException
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
|
||||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.content.Context
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.json.JSONArray
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Writer
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/** Writes saved favorites to file. */
|
|
||||||
class EpisodesProgressWriter : ExportWriter {
|
|
||||||
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
|
||||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
|
||||||
Logd(TAG, "Starting to write document")
|
|
||||||
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
|
|
||||||
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PAUSED), SortOrder.DATE_NEW_OLD)
|
|
||||||
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.PLAYED), SortOrder.DATE_NEW_OLD)
|
|
||||||
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
|
||||||
val comItems = mutableSetOf<Episode>()
|
|
||||||
comItems.addAll(pausedItems)
|
|
||||||
comItems.addAll(readItems)
|
|
||||||
comItems.addAll(favoriteItems)
|
|
||||||
Logd(TAG, "Save state for all " + comItems.size + " played episodes")
|
|
||||||
for (item in comItems) {
|
|
||||||
val media = item.media ?: continue
|
|
||||||
val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
|
|
||||||
.timestamp(Date(media.getLastPlayedTime()))
|
|
||||||
.started(media.getPosition() / 1000)
|
|
||||||
.position(media.getPosition() / 1000)
|
|
||||||
.total(media.getDuration() / 1000)
|
|
||||||
.isFavorite(item.isFavorite)
|
|
||||||
.playState(item.playState)
|
|
||||||
.build()
|
|
||||||
queuedEpisodeActions.add(played)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (queuedEpisodeActions.isNotEmpty()) {
|
|
||||||
try {
|
|
||||||
Logd(TAG, "Saving ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
|
|
||||||
val list = JSONArray()
|
|
||||||
for (episodeAction in queuedEpisodeActions) {
|
|
||||||
val obj = episodeAction.writeToJsonObject()
|
|
||||||
if (obj != null) {
|
|
||||||
Logd(TAG, "saving EpisodeAction: $obj")
|
|
||||||
list.put(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer?.write(list.toString())
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
throw SyncServiceException(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logd(TAG, "Finished writing document")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileExtension(): String {
|
|
||||||
return "json"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "EpisodesProgressWriter"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
/*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Writer
|
|
||||||
|
|
||||||
interface ExportWriter {
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
|
||||||
fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context)
|
|
||||||
|
|
||||||
fun fileExtension(): String?
|
|
||||||
}
|
|
|
@ -1,110 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
|
||||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.content.Context
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Writer
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/** Writes saved favorites to file. */
|
|
||||||
class FavoritesWriter : ExportWriter {
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
|
||||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
|
||||||
Logd(TAG, "Starting to write document")
|
|
||||||
|
|
||||||
val templateStream = context!!.assets.open("html-export-template.html")
|
|
||||||
var template = IOUtils.toString(templateStream, UTF_8)
|
|
||||||
template = template.replace("\\{TITLE\\}".toRegex(), "Favorites")
|
|
||||||
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
||||||
|
|
||||||
val favTemplateStream = context.assets.open(FAVORITE_TEMPLATE)
|
|
||||||
val favTemplate = IOUtils.toString(favTemplateStream, UTF_8)
|
|
||||||
|
|
||||||
val feedTemplateStream = context.assets.open(FEED_TEMPLATE)
|
|
||||||
val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8)
|
|
||||||
|
|
||||||
val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
|
||||||
val favoriteByFeed = getFeedMap(allFavorites)
|
|
||||||
|
|
||||||
writer!!.append(templateParts[0])
|
|
||||||
|
|
||||||
for (feedId in favoriteByFeed.keys) {
|
|
||||||
val favorites: List<Episode> = favoriteByFeed[feedId]!!
|
|
||||||
writer.append("<li><div>\n")
|
|
||||||
writeFeed(writer, favorites[0].feed, feedTemplate)
|
|
||||||
|
|
||||||
writer.append("<ul>\n")
|
|
||||||
for (item in favorites) {
|
|
||||||
writeFavoriteItem(writer, item, favTemplate)
|
|
||||||
}
|
|
||||||
writer.append("</ul></div></li>\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.append(templateParts[1])
|
|
||||||
|
|
||||||
Logd(TAG, "Finished writing document")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Group favorite episodes by feed, sorting them by publishing date in descending order.
|
|
||||||
*
|
|
||||||
* @param favoritesList `List` of all favorite episodes.
|
|
||||||
* @return A `Map` favorite episodes, keyed by feed ID.
|
|
||||||
*/
|
|
||||||
private fun getFeedMap(favoritesList: List<Episode>): Map<Long, MutableList<Episode>> {
|
|
||||||
val feedMap: MutableMap<Long, MutableList<Episode>> = TreeMap()
|
|
||||||
|
|
||||||
for (item in favoritesList) {
|
|
||||||
var feedEpisodes = feedMap[item.feedId]
|
|
||||||
|
|
||||||
if (feedEpisodes == null) {
|
|
||||||
feedEpisodes = ArrayList()
|
|
||||||
if (item.feedId != null) feedMap[item.feedId!!] = feedEpisodes
|
|
||||||
}
|
|
||||||
|
|
||||||
feedEpisodes.add(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return feedMap
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeFeed(writer: Writer?, feed: Feed?, feedTemplate: String) {
|
|
||||||
val feedInfo = feedTemplate
|
|
||||||
.replace("{FEED_IMG}", feed!!.imageUrl!!)
|
|
||||||
.replace("{FEED_TITLE}", feed.title!!)
|
|
||||||
.replace("{FEED_LINK}", feed.link!!)
|
|
||||||
.replace("{FEED_WEBSITE}", feed.downloadUrl!!)
|
|
||||||
|
|
||||||
writer!!.append(feedInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
private fun writeFavoriteItem(writer: Writer?, item: Episode, favoriteTemplate: String) {
|
|
||||||
var favItem = favoriteTemplate.replace("{FAV_TITLE}", item.title!!.trim { it <= ' ' })
|
|
||||||
favItem = if (item.link != null) favItem.replace("{FAV_WEBSITE}", item.link!!)
|
|
||||||
else favItem.replace("{FAV_WEBSITE}", "")
|
|
||||||
|
|
||||||
favItem = if (item.media != null && item.media!!.downloadUrl != null) favItem.replace("{FAV_MEDIA}", item.media!!.downloadUrl!!)
|
|
||||||
else favItem.replace("{FAV_MEDIA}", "")
|
|
||||||
|
|
||||||
writer!!.append(favItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileExtension(): String {
|
|
||||||
return "html"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = FavoritesWriter::class.simpleName ?: "Anonymous"
|
|
||||||
private const val FAVORITE_TEMPLATE = "html-export-favorites-item-template.html"
|
|
||||||
private const val FEED_TEMPLATE = "html-export-feed-template.html"
|
|
||||||
private const val UTF_8 = "UTF-8"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import org.apache.commons.io.IOUtils
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Writer
|
|
||||||
|
|
||||||
/** Writes HTML documents. */
|
|
||||||
class HtmlWriter : ExportWriter {
|
|
||||||
/**
|
|
||||||
* Takes a list of feeds and a writer and writes those into an HTML
|
|
||||||
* document.
|
|
||||||
*/
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
|
||||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
|
||||||
Logd(TAG, "Starting to write document")
|
|
||||||
|
|
||||||
val templateStream = context!!.assets.open("html-export-template.html")
|
|
||||||
var template = IOUtils.toString(templateStream, "UTF-8")
|
|
||||||
template = template.replace("\\{TITLE\\}".toRegex(), "Subscriptions")
|
|
||||||
val templateParts = template.split("\\{FEEDS\\}".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
|
||||||
|
|
||||||
writer!!.append(templateParts[0])
|
|
||||||
for (feed in feeds!!) {
|
|
||||||
writer.append("<li><div><img src=\"")
|
|
||||||
writer.append(feed!!.imageUrl)
|
|
||||||
writer.append("\" /><p>")
|
|
||||||
writer.append(feed.title)
|
|
||||||
writer.append(" <span><a href=\"")
|
|
||||||
writer.append(feed.link)
|
|
||||||
writer.append("\">Website</a> • <a href=\"")
|
|
||||||
writer.append(feed.downloadUrl)
|
|
||||||
writer.append("\">Feed</a></span></p></div></li>\n")
|
|
||||||
}
|
|
||||||
writer.append(templateParts[1])
|
|
||||||
Logd(TAG, "Finished writing document")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileExtension(): String {
|
|
||||||
return "html"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = HtmlWriter::class.simpleName ?: "Anonymous"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
/** Represents a single feed in an OPML file. */
|
|
||||||
class OpmlElement {
|
|
||||||
@JvmField
|
|
||||||
var text: String? = null
|
|
||||||
var xmlUrl: String? = null
|
|
||||||
var htmlUrl: String? = null
|
|
||||||
var type: String? = null
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.util.Log
|
|
||||||
import org.xmlpull.v1.XmlPullParser
|
|
||||||
import org.xmlpull.v1.XmlPullParserException
|
|
||||||
import org.xmlpull.v1.XmlPullParserFactory
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Reader
|
|
||||||
|
|
||||||
/** Reads OPML documents. */
|
|
||||||
class OpmlReader {
|
|
||||||
// ATTRIBUTES
|
|
||||||
private var isInOpml = false
|
|
||||||
private var elementList: ArrayList<OpmlElement>? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads an Opml document and returns a list of all OPML elements it can
|
|
||||||
* find
|
|
||||||
*
|
|
||||||
* @throws IOException
|
|
||||||
* @throws XmlPullParserException
|
|
||||||
*/
|
|
||||||
@Throws(XmlPullParserException::class, IOException::class)
|
|
||||||
fun readDocument(reader: Reader?): ArrayList<OpmlElement> {
|
|
||||||
elementList = ArrayList()
|
|
||||||
val factory = XmlPullParserFactory.newInstance()
|
|
||||||
factory.isNamespaceAware = true
|
|
||||||
val xpp = factory.newPullParser()
|
|
||||||
xpp.setInput(reader)
|
|
||||||
var eventType = xpp.eventType
|
|
||||||
|
|
||||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
||||||
when (eventType) {
|
|
||||||
XmlPullParser.START_DOCUMENT -> Logd(TAG, "Reached beginning of document")
|
|
||||||
XmlPullParser.START_TAG -> when {
|
|
||||||
xpp.name == OpmlSymbols.OPML -> {
|
|
||||||
isInOpml = true
|
|
||||||
Logd(TAG, "Reached beginning of OPML tree.")
|
|
||||||
}
|
|
||||||
isInOpml && xpp.name == OpmlSymbols.OUTLINE -> {
|
|
||||||
// TODO: check more about this, java.io.IOException: Underlying input stream returned zero bytes
|
|
||||||
Logd(TAG, "Found new Opml element")
|
|
||||||
val element = OpmlElement()
|
|
||||||
|
|
||||||
val title = xpp.getAttributeValue(null, CommonSymbols.TITLE)
|
|
||||||
if (title != null) {
|
|
||||||
Log.i(TAG, "Using title: $title")
|
|
||||||
element.text = title
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Title not found, using text")
|
|
||||||
element.text = xpp.getAttributeValue(null, OpmlSymbols.TEXT)
|
|
||||||
}
|
|
||||||
element.xmlUrl = xpp.getAttributeValue(null, OpmlSymbols.XMLURL)
|
|
||||||
element.htmlUrl = xpp.getAttributeValue(null, OpmlSymbols.HTMLURL)
|
|
||||||
element.type = xpp.getAttributeValue(null, OpmlSymbols.TYPE)
|
|
||||||
if (element.xmlUrl != null) {
|
|
||||||
if (element.text == null) {
|
|
||||||
Log.i(TAG, "Opml element has no text attribute.")
|
|
||||||
element.text = element.xmlUrl
|
|
||||||
}
|
|
||||||
elementList!!.add(element)
|
|
||||||
} else {
|
|
||||||
Logd(TAG, "Skipping element because of missing xml url")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// TODO: on first install app: java.io.IOException: Underlying input stream returned zero bytes
|
|
||||||
eventType = xpp.next()
|
|
||||||
} catch(e: Exception) {
|
|
||||||
Log.e(TAG, "xpp.next() invalid: $e")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Logd(TAG, "Parsing finished.")
|
|
||||||
|
|
||||||
return elementList!!
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = OpmlReader::class.simpleName ?: "Anonymous"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
/** Contains symbols for reading and writing OPML documents. */
|
|
||||||
internal object OpmlSymbols : CommonSymbols() {
|
|
||||||
const val OPML: String = "opml"
|
|
||||||
const val OUTLINE: String = "outline"
|
|
||||||
const val TEXT: String = "text"
|
|
||||||
const val XMLURL: String = "xmlUrl"
|
|
||||||
const val HTMLURL: String = "htmlUrl"
|
|
||||||
const val TYPE: String = "type"
|
|
||||||
const val VERSION: String = "version"
|
|
||||||
const val DATE_CREATED: String = "dateCreated"
|
|
||||||
}
|
|
|
@ -1,64 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Xml
|
|
||||||
import ac.mdiq.podcini.util.DateFormatter.formatRfc822Date
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.Writer
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/** Writes OPML documents. */
|
|
||||||
class OpmlWriter : ExportWriter {
|
|
||||||
/**
|
|
||||||
* Takes a list of feeds and a writer and writes those into an OPML
|
|
||||||
* document.
|
|
||||||
*/
|
|
||||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
|
||||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
|
||||||
Logd(TAG, "Starting to write document")
|
|
||||||
val xs = Xml.newSerializer()
|
|
||||||
xs.setFeature(CommonSymbols.XML_FEATURE_INDENT_OUTPUT, true)
|
|
||||||
xs.setOutput(writer)
|
|
||||||
|
|
||||||
xs.startDocument(ENCODING, false)
|
|
||||||
xs.startTag(null, OpmlSymbols.OPML)
|
|
||||||
xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION)
|
|
||||||
|
|
||||||
xs.startTag(null, CommonSymbols.HEAD)
|
|
||||||
xs.startTag(null, CommonSymbols.TITLE)
|
|
||||||
xs.text(OPML_TITLE)
|
|
||||||
xs.endTag(null, CommonSymbols.TITLE)
|
|
||||||
xs.startTag(null, OpmlSymbols.DATE_CREATED)
|
|
||||||
xs.text(formatRfc822Date(Date()))
|
|
||||||
xs.endTag(null, OpmlSymbols.DATE_CREATED)
|
|
||||||
xs.endTag(null, CommonSymbols.HEAD)
|
|
||||||
|
|
||||||
xs.startTag(null, CommonSymbols.BODY)
|
|
||||||
for (feed in feeds!!) {
|
|
||||||
xs.startTag(null, OpmlSymbols.OUTLINE)
|
|
||||||
xs.attribute(null, OpmlSymbols.TEXT, feed!!.title)
|
|
||||||
xs.attribute(null, CommonSymbols.TITLE, feed.title)
|
|
||||||
if (feed.type != null) xs.attribute(null, OpmlSymbols.TYPE, feed.type)
|
|
||||||
xs.attribute(null, OpmlSymbols.XMLURL, feed.downloadUrl)
|
|
||||||
if (feed.link != null) xs.attribute(null, OpmlSymbols.HTMLURL, feed.link)
|
|
||||||
xs.endTag(null, OpmlSymbols.OUTLINE)
|
|
||||||
}
|
|
||||||
xs.endTag(null, CommonSymbols.BODY)
|
|
||||||
xs.endTag(null, OpmlSymbols.OPML)
|
|
||||||
xs.endDocument()
|
|
||||||
Logd(TAG, "Finished writing document")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun fileExtension(): String {
|
|
||||||
return "opml"
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val TAG: String = OpmlWriter::class.simpleName ?: "Anonymous"
|
|
||||||
private const val ENCODING = "UTF-8"
|
|
||||||
private const val OPML_VERSION = "2.0"
|
|
||||||
private const val OPML_TITLE = "Podcini Subscriptions"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,118 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.transport
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.BuildConfig
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import java.io.*
|
|
||||||
|
|
||||||
object PreferencesTransporter {
|
|
||||||
private val TAG: String = PreferencesTransporter::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun exportToDocument(uri: Uri, context: Context) {
|
|
||||||
try {
|
|
||||||
val chosenDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Destination directory is not valid")
|
|
||||||
val exportSubDir = chosenDir.createDirectory("Podcini-Prefs") ?: throw IOException("Error creating subdirectory Podcini-Prefs")
|
|
||||||
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
|
|
||||||
file.name.startsWith("shared_prefs")
|
|
||||||
}?.firstOrNull()
|
|
||||||
if (sharedPreferencesDir != null) {
|
|
||||||
sharedPreferencesDir.listFiles()!!.forEach { file ->
|
|
||||||
val destFile = exportSubDir.createFile("text/xml", file.name)
|
|
||||||
if (destFile != null) copyFile(file, destFile, context)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("Error", "shared_prefs directory not found")
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
throw e
|
|
||||||
} finally { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFile(sourceFile: File, destFile: DocumentFile, context: Context) {
|
|
||||||
try {
|
|
||||||
val inputStream = FileInputStream(sourceFile)
|
|
||||||
val outputStream = context.contentResolver.openOutputStream(destFile.uri)
|
|
||||||
if (outputStream != null) copyStream(inputStream, outputStream)
|
|
||||||
inputStream.close()
|
|
||||||
outputStream?.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e("Error", "Error copying file: $e")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyFile(sourceFile: DocumentFile, destFile: File, context: Context) {
|
|
||||||
try {
|
|
||||||
val inputStream = context.contentResolver.openInputStream(sourceFile.uri)
|
|
||||||
val outputStream = FileOutputStream(destFile)
|
|
||||||
if (inputStream != null) copyStream(inputStream, outputStream)
|
|
||||||
inputStream?.close()
|
|
||||||
outputStream.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e("Error", "Error copying file: $e")
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var bytesRead: Int
|
|
||||||
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
|
|
||||||
outputStream.write(buffer, 0, bytesRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun importBackup(uri: Uri, context: Context) {
|
|
||||||
try {
|
|
||||||
val exportedDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Backup directory is not valid")
|
|
||||||
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
|
|
||||||
file.name.startsWith("shared_prefs")
|
|
||||||
}?.firstOrNull()
|
|
||||||
if (sharedPreferencesDir != null) {
|
|
||||||
sharedPreferencesDir.listFiles()?.forEach { file ->
|
|
||||||
// val prefName = file.name.substring(0, file.name.lastIndexOf('.'))
|
|
||||||
file.delete()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e("Error", "shared_prefs directory not found")
|
|
||||||
}
|
|
||||||
val files = exportedDir.listFiles()
|
|
||||||
var hasPodciniRPrefs = false
|
|
||||||
for (file in files) {
|
|
||||||
if (file?.isFile == true && file.name?.endsWith(".xml") == true && file.name!!.contains("podcini.R")) {
|
|
||||||
hasPodciniRPrefs = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (file in files) {
|
|
||||||
if (file?.isFile == true && file.name?.endsWith(".xml") == true) {
|
|
||||||
var destName = file.name!!
|
|
||||||
// for importing from Podcini version 5 and below
|
|
||||||
if (!hasPodciniRPrefs) {
|
|
||||||
when {
|
|
||||||
destName.contains("podcini") -> destName = destName.replace("podcini", "podcini.R")
|
|
||||||
destName.contains("EpisodeItemListRecyclerView") -> destName = destName.replace("EpisodeItemListRecyclerView", "EpisodesRecyclerView")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when {
|
|
||||||
// for debug version importing release version
|
|
||||||
BuildConfig.DEBUG && !destName.contains(".debug") -> destName = destName.replace("podcini.R", "podcini.R.debug")
|
|
||||||
// for release version importing debug version
|
|
||||||
!BuildConfig.DEBUG && destName.contains(".debug") -> destName = destName.replace(".debug", "")
|
|
||||||
}
|
|
||||||
val destFile = File(sharedPreferencesDir, destName)
|
|
||||||
copyFile(file, destFile, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
throw e
|
|
||||||
} finally { }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -67,10 +67,7 @@ object ChapterUtils {
|
||||||
try {
|
try {
|
||||||
openStream(playable, context).use { inVal ->
|
openStream(playable, context).use { inVal ->
|
||||||
val chapters = readId3ChaptersFrom(inVal)
|
val chapters = readId3ChaptersFrom(inVal)
|
||||||
if (chapters.isNotEmpty()) {
|
if (chapters.isNotEmpty()) return chapters
|
||||||
Log.i(TAG, "Chapters loaded")
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Unable to load ID3 chapters: " + e.message)
|
Log.e(TAG, "Unable to load ID3 chapters: " + e.message)
|
||||||
|
@ -81,10 +78,7 @@ object ChapterUtils {
|
||||||
try {
|
try {
|
||||||
openStream(playable, context).use { inVal ->
|
openStream(playable, context).use { inVal ->
|
||||||
val chapters = readOggChaptersFromInputStream(inVal)
|
val chapters = readOggChaptersFromInputStream(inVal)
|
||||||
if (chapters.isNotEmpty()) {
|
if (chapters.isNotEmpty()) return chapters
|
||||||
Log.i(TAG, "Chapters loaded")
|
|
||||||
return chapters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "Unable to load vorbis chapters: " + e.message)
|
Log.e(TAG, "Unable to load vorbis chapters: " + e.message)
|
||||||
|
@ -151,7 +145,7 @@ object ChapterUtils {
|
||||||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||||
enumerateEmptyChapterTitles(chapters)
|
enumerateEmptyChapterTitles(chapters)
|
||||||
if (!chaptersValid(chapters)) {
|
if (!chaptersValid(chapters)) {
|
||||||
Log.i(TAG, "Chapter data was invalid")
|
Logd(TAG, "Chapter data was invalid")
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
return chapters
|
return chapters
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
@ -40,7 +40,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||||
playbackService?.mediaPlayer?.resume()
|
playbackService?.mPlayer?.resume()
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
} else {
|
} else {
|
||||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
@ -30,7 +30,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||||
playbackService?.mediaPlayer?.resume()
|
playbackService?.mPlayer?.resume()
|
||||||
playbackService?.taskManager?.restartSleepTimer()
|
playbackService?.taskManager?.restartSleepTimer()
|
||||||
} else {
|
} else {
|
||||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||||
|
|
|
@ -2,7 +2,6 @@ package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||||
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
|
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
||||||
|
@ -11,6 +10,7 @@ import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.model.RemoteMedia
|
import ac.mdiq.podcini.storage.model.RemoteMedia
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.util.event.EventFlow
|
import ac.mdiq.podcini.util.event.EventFlow
|
||||||
import ac.mdiq.podcini.util.event.FlowEvent
|
import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -19,6 +19,7 @@ import ac.mdiq.podcini.preferences.UserPreferences.defaultPage
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
||||||
import ac.mdiq.podcini.receiver.PlayerWidget
|
import ac.mdiq.podcini.receiver.PlayerWidget
|
||||||
|
import ac.mdiq.podcini.storage.database.Feeds.monitorFeeds
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedMap
|
import ac.mdiq.podcini.storage.database.Feeds.updateFeedMap
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||||
|
@ -95,7 +96,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
private lateinit var mainView: View
|
private lateinit var mainView: View
|
||||||
private lateinit var navDrawerFragment: NavDrawerFragment
|
private lateinit var navDrawerFragment: NavDrawerFragment
|
||||||
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
||||||
private lateinit var audioPlayerFragmentView: View
|
private lateinit var audioPlayerView: View
|
||||||
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||||
private lateinit var navDrawer: View
|
private lateinit var navDrawer: View
|
||||||
private lateinit var dummyView : View
|
private lateinit var dummyView : View
|
||||||
|
@ -130,6 +131,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
SwipeActions.getSharedPrefs(this@MainActivity)
|
SwipeActions.getSharedPrefs(this@MainActivity)
|
||||||
QueueFragment.getSharedPrefs(this@MainActivity)
|
QueueFragment.getSharedPrefs(this@MainActivity)
|
||||||
updateFeedMap()
|
updateFeedMap()
|
||||||
|
monitorFeeds()
|
||||||
// InTheatre.apply { }
|
// InTheatre.apply { }
|
||||||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||||
|
@ -196,11 +198,11 @@ class MainActivity : CastEnabledActivity() {
|
||||||
transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG)
|
transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG)
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
navDrawer = findViewById(R.id.navDrawerFragment)
|
navDrawer = findViewById(R.id.navDrawerFragment)
|
||||||
audioPlayerFragmentView = findViewById(R.id.audioplayerFragment)
|
audioPlayerView = findViewById(R.id.audioplayerFragment)
|
||||||
|
|
||||||
runOnIOScope { checkFirstLaunch() }
|
runOnIOScope { checkFirstLaunch() }
|
||||||
|
|
||||||
this.bottomSheet = BottomSheetBehavior.from(audioPlayerFragmentView) as LockableBottomSheetBehavior<*>
|
this.bottomSheet = BottomSheetBehavior.from(audioPlayerView) as LockableBottomSheetBehavior<*>
|
||||||
this.bottomSheet.isHideable = false
|
this.bottomSheet.isHideable = false
|
||||||
this.bottomSheet.isDraggable = false
|
this.bottomSheet.isDraggable = false
|
||||||
this.bottomSheet.setBottomSheetCallback(bottomSheetCallback)
|
this.bottomSheet.setBottomSheetCallback(bottomSheetCallback)
|
||||||
|
@ -382,7 +384,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||||
|
|
||||||
private fun updateInsets() {
|
private fun updateInsets() {
|
||||||
setPlayerVisible(audioPlayerFragmentView.visibility == View.VISIBLE)
|
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
|
||||||
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||||
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom
|
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom
|
||||||
}
|
}
|
||||||
|
@ -403,7 +405,11 @@ class MainActivity : CastEnabledActivity() {
|
||||||
val playerParams = playerView?.layoutParams as? MarginLayoutParams
|
val playerParams = playerView?.layoutParams as? MarginLayoutParams
|
||||||
playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
|
playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
|
||||||
playerView.layoutParams = playerParams
|
playerView.layoutParams = playerParams
|
||||||
audioPlayerFragmentView.visibility = if (visible) View.VISIBLE else View.GONE
|
audioPlayerView.visibility = if (visible) View.VISIBLE else View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isPlayerVisible(): Boolean {
|
||||||
|
return audioPlayerView.visibility == View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFragment(tag: String?, args: Bundle?) {
|
fun loadFragment(tag: String?, args: Bundle?) {
|
||||||
|
@ -669,7 +675,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
val s: Snackbar
|
val s: Snackbar
|
||||||
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
s = Snackbar.make(mainView, text!!, duration)
|
s = Snackbar.make(mainView, text!!, duration)
|
||||||
if (audioPlayerFragmentView.visibility == View.VISIBLE) s.setAnchorView(audioPlayerFragmentView)
|
if (audioPlayerView.visibility == View.VISIBLE) s.setAnchorView(audioPlayerView)
|
||||||
} else s = Snackbar.make(binding.root, text!!, duration)
|
} else s = Snackbar.make(binding.root, text!!, duration)
|
||||||
|
|
||||||
s.show()
|
s.show()
|
||||||
|
|
|
@ -5,8 +5,8 @@ import ac.mdiq.podcini.databinding.OpmlSelectionBinding
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
||||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
|
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||||
import ac.mdiq.podcini.storage.transport.OpmlElement
|
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlElement
|
||||||
import ac.mdiq.podcini.storage.transport.OpmlReader
|
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
|
|
@ -5,12 +5,12 @@ import ac.mdiq.podcini.databinding.AudioControlsBinding
|
||||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.PlaybackController
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||||
|
@ -83,7 +83,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) {
|
if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) {
|
||||||
Log.i(TAG, "videoMode not selected, use window mode")
|
Logd(TAG, "videoMode not selected, use window mode")
|
||||||
videoMode = VideoMode.WINDOW_VIEW
|
videoMode = VideoMode.WINDOW_VIEW
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -434,7 +434,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
||||||
butAudioTracks.setOnClickListener {
|
butAudioTracks.setOnClickListener {
|
||||||
// setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
|
// setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
|
||||||
playbackService?.mediaPlayer?.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
|
playbackService?.mPlayer?.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
|
||||||
Handler(Looper.getMainLooper()).postDelayed({ this.setupAudioTracks() }, 500)
|
Handler(Looper.getMainLooper()).postDelayed({ this.setupAudioTracks() }, 500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -455,13 +455,13 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
|
|
||||||
private val audioTracks: List<String>
|
private val audioTracks: List<String>
|
||||||
get() {
|
get() {
|
||||||
val tracks = playbackService?.mediaPlayer?.getAudioTracks()
|
val tracks = playbackService?.mPlayer?.getAudioTracks()
|
||||||
if (tracks.isNullOrEmpty()) return emptyList()
|
if (tracks.isNullOrEmpty()) return emptyList()
|
||||||
return tracks.filterNotNull().map { it }
|
return tracks.filterNotNull().map { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private val selectedAudioTrack: Int
|
private val selectedAudioTrack: Int
|
||||||
get() = playbackService?.mediaPlayer?.getSelectedAudioTrack() ?: -1
|
get() = playbackService?.mPlayer?.getSelectedAudioTrack() ?: -1
|
||||||
|
|
||||||
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
||||||
return when {
|
return when {
|
||||||
|
|
|
@ -18,6 +18,7 @@ import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
|
||||||
import ac.mdiq.podcini.receiver.PlayerWidget
|
import ac.mdiq.podcini.receiver.PlayerWidget
|
||||||
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs
|
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs
|
||||||
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
|
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
|
||||||
class WidgetConfigActivity : AppCompatActivity() {
|
class WidgetConfigActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
@ -63,9 +64,7 @@ class WidgetConfigActivity : AppCompatActivity() {
|
||||||
val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||||
widgetPreview.setBackgroundColor(color)
|
widgetPreview.setBackgroundColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -96,6 +95,8 @@ class WidgetConfigActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setInitialState() {
|
private fun setInitialState() {
|
||||||
|
PlayerWidget.getSharedPrefs(this)
|
||||||
|
|
||||||
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||||
ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, true)
|
ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, true)
|
||||||
ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, true)
|
ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, true)
|
||||||
|
@ -123,6 +124,7 @@ class WidgetConfigActivity : AppCompatActivity() {
|
||||||
private fun confirmCreateWidget() {
|
private fun confirmCreateWidget() {
|
||||||
val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||||
|
|
||||||
|
Logd("WidgetConfigActivity", "confirmCreateWidget appWidgetId $appWidgetId")
|
||||||
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||||
val editor = prefs!!.edit()
|
val editor = prefs!!.edit()
|
||||||
editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor)
|
editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.unmanagedCopy
|
import ac.mdiq.podcini.storage.database.RealmDB.unmanagedCopy
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
||||||
|
@ -25,12 +26,27 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
||||||
private val TAG: String = this::class.simpleName ?: "Anonymous"
|
private val TAG: String = this::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||||
|
protected val activity: Activity?
|
||||||
|
get() = mainActivityRef.get()
|
||||||
|
|
||||||
private var episodes: List<Episode> = ArrayList()
|
private var episodes: List<Episode> = ArrayList()
|
||||||
|
private var feed: Feed? = null
|
||||||
var longPressedItem: Episode? = null
|
var longPressedItem: Episode? = null
|
||||||
private var longPressedPosition: Int = 0 // used to init actionMode
|
private var longPressedPosition: Int = 0 // used to init actionMode
|
||||||
private var dummyViews = 0
|
private var dummyViews = 0
|
||||||
|
|
||||||
|
val selectedItems: List<Any>
|
||||||
|
get() {
|
||||||
|
val items: MutableList<Episode> = ArrayList()
|
||||||
|
for (i in 0 until itemCount) {
|
||||||
|
if (i < episodes.size && isSelected(i)) {
|
||||||
|
val item = getItem(i)
|
||||||
|
if (item != null) items.add(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setHasStableIds(true)
|
setHasStableIds(true)
|
||||||
}
|
}
|
||||||
|
@ -40,8 +56,9 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateItems(items: List<Episode>) {
|
fun updateItems(items: List<Episode>, feed_: Feed? = null) {
|
||||||
episodes = items
|
episodes = items
|
||||||
|
feed = feed_
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
updateTitle()
|
updateTitle()
|
||||||
}
|
}
|
||||||
|
@ -72,6 +89,7 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
||||||
beforeBindViewHolder(holder, pos)
|
beforeBindViewHolder(holder, pos)
|
||||||
|
|
||||||
val item: Episode = unmanagedCopy(episodes[pos])
|
val item: Episode = unmanagedCopy(episodes[pos])
|
||||||
|
if (feed != null) item.feed = feed
|
||||||
holder.bind(item)
|
holder.bind(item)
|
||||||
|
|
||||||
// holder.infoCard.setOnCreateContextMenuListener(this)
|
// holder.infoCard.setOnCreateContextMenuListener(this)
|
||||||
|
@ -154,9 +172,6 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val activity: Activity?
|
|
||||||
get() = mainActivityRef.get()
|
|
||||||
|
|
||||||
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||||
val inflater: MenuInflater = activity!!.menuInflater
|
val inflater: MenuInflater = activity!!.menuInflater
|
||||||
if (inActionMode()) {
|
if (inActionMode()) {
|
||||||
|
@ -188,16 +203,4 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedItems: List<Any>
|
|
||||||
get() {
|
|
||||||
val items: MutableList<Episode> = ArrayList()
|
|
||||||
for (i in 0 until itemCount) {
|
|
||||||
if (i < episodes.size && isSelected(i)) {
|
|
||||||
val item = getItem(i)
|
|
||||||
if (item != null) items.add(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,17 +58,6 @@ class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSear
|
||||||
viewHolder.updateView.visibility = View.VISIBLE
|
viewHolder.updateView.visibility = View.VISIBLE
|
||||||
} else viewHolder.updateView.visibility = View.INVISIBLE
|
} else viewHolder.updateView.visibility = View.INVISIBLE
|
||||||
|
|
||||||
//Update the empty imageView with the image from the feed
|
|
||||||
// if (!podcast.imageUrl.isNullOrBlank()) Glide.with(context)
|
|
||||||
// .load(podcast.imageUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .placeholder(R.color.light_gray)
|
|
||||||
// .diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
// .transform(FitCenter(),
|
|
||||||
// RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
|
|
||||||
// .dontAnimate())
|
|
||||||
// .into(viewHolder.coverView)
|
|
||||||
|
|
||||||
viewHolder.coverView.load(podcast.imageUrl) {
|
viewHolder.coverView.load(podcast.imageUrl) {
|
||||||
placeholder(R.color.light_gray)
|
placeholder(R.color.light_gray)
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
|
|
|
@ -22,13 +22,6 @@ class SimpleIconListAdapter<T : SimpleIconListAdapter.ListItem>(private val cont
|
||||||
val binding = SimpleIconListItemBinding.bind(view!!)
|
val binding = SimpleIconListItemBinding.bind(view!!)
|
||||||
binding.title.text = item.title
|
binding.title.text = item.title
|
||||||
binding.subtitle.text = item.subtitle
|
binding.subtitle.text = item.subtitle
|
||||||
// if (item.imageUrl.isNotBlank()) Glide.with(context)
|
|
||||||
// .load(item.imageUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
// .fitCenter()
|
|
||||||
// .dontAnimate())
|
|
||||||
// .into(binding.icon)
|
|
||||||
binding.icon.load(item.imageUrl)
|
binding.icon.load(item.imageUrl)
|
||||||
return view
|
return view
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ object RemoveFeedDialog {
|
||||||
for (feed in feeds) {
|
for (feed in feeds) {
|
||||||
deleteFeed(context, feed.id, false)
|
deleteFeed(context, feed.id, false)
|
||||||
}
|
}
|
||||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feeds))
|
// EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.REMOVED, feeds))
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Logd(TAG, "Feed(s) deleted")
|
Logd(TAG, "Feed(s) deleted")
|
||||||
|
|
|
@ -4,6 +4,8 @@ import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
|
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||||
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
@ -138,7 +140,7 @@ import java.util.*
|
||||||
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||||
isSkipSilence = isChecked
|
isSkipSilence = isChecked
|
||||||
// setSkipSilence(isChecked)
|
// setSkipSilence(isChecked)
|
||||||
playbackService?.mediaPlayer?.setPlaybackParams(playbackService!!.currentPlaybackSpeed, isChecked)
|
playbackService?.mPlayer?.setPlaybackParams(playbackService!!.curSpeed, isChecked)
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -217,13 +219,13 @@ import java.util.*
|
||||||
if (currentMediaType == MediaType.VIDEO) {
|
if (currentMediaType == MediaType.VIDEO) {
|
||||||
curState.curTempSpeed = speed
|
curState.curTempSpeed = speed
|
||||||
videoPlaybackSpeed = speed
|
videoPlaybackSpeed = speed
|
||||||
playbackService!!.mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||||
} else {
|
} else {
|
||||||
if (codeArray != null && codeArray.size == 3) {
|
if (codeArray != null && codeArray.size == 3) {
|
||||||
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
|
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
|
||||||
if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed)
|
if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed)
|
||||||
if (codeArray[1]) {
|
if (codeArray[1]) {
|
||||||
val episode = (playbackService!!.playable as? EpisodeMedia)?.episode ?: playbackService!!.currentitem
|
val episode = (curMedia as? EpisodeMedia)?.episode ?: curEpisode
|
||||||
if (episode != null) {
|
if (episode != null) {
|
||||||
var feed = episode.feed
|
var feed = episode.feed
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
|
@ -240,11 +242,11 @@ import java.util.*
|
||||||
}
|
}
|
||||||
if (codeArray[0]) {
|
if (codeArray[0]) {
|
||||||
curState.curTempSpeed = speed
|
curState.curTempSpeed = speed
|
||||||
playbackService!!.mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
curState.curTempSpeed = speed
|
curState.curTempSpeed = speed
|
||||||
playbackService!!.mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,20 +62,14 @@ import kotlin.math.min
|
||||||
|
|
||||||
override fun loadData(): List<Episode> {
|
override fun loadData(): List<Episode> {
|
||||||
allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false)
|
allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false)
|
||||||
Logd(TAG, "loadData() allEpisodes.size ${allEpisodes.size}")
|
return allEpisodes.subList(0, min(allEpisodes.size-1, page * EPISODES_PER_PAGE))
|
||||||
return allEpisodes.subList(0, page * EPISODES_PER_PAGE)
|
|
||||||
// return getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), allEpisodesSortOrder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadMoreData(page: Int): List<Episode> {
|
override fun loadMoreData(page: Int): List<Episode> {
|
||||||
val offset = (page - 1) * EPISODES_PER_PAGE
|
val offset = (page - 1) * EPISODES_PER_PAGE
|
||||||
Logd(TAG, "loadMoreData() page: $page $offset ${allEpisodes.size}")
|
|
||||||
if (offset >= allEpisodes.size) return listOf()
|
if (offset >= allEpisodes.size) return listOf()
|
||||||
val toIndex = offset + EPISODES_PER_PAGE
|
val toIndex = offset + EPISODES_PER_PAGE
|
||||||
Logd(TAG, "loadMoreData() $offset $toIndex ${min(allEpisodes.size, toIndex)}")
|
|
||||||
return allEpisodes.subList(offset, min(allEpisodes.size, toIndex))
|
return allEpisodes.subList(offset, min(allEpisodes.size, toIndex))
|
||||||
// return allEpisodes.subList((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE)
|
|
||||||
// return getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter(), allEpisodesSortOrder)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadTotalItemCount(): Int {
|
override fun loadTotalItemCount(): Int {
|
||||||
|
@ -86,10 +80,6 @@ import kotlin.math.min
|
||||||
return EpisodeFilter(prefFilterAllEpisodes)
|
return EpisodeFilter(prefFilterAllEpisodes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFragmentTag(): String {
|
|
||||||
return TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPrefName(): String {
|
override fun getPrefName(): String {
|
||||||
return PREF_NAME
|
return PREF_NAME
|
||||||
}
|
}
|
||||||
|
@ -162,7 +152,7 @@ import kotlin.math.min
|
||||||
override fun onSelectionChanged() {
|
override fun onSelectionChanged() {
|
||||||
super.onSelectionChanged()
|
super.onSelectionChanged()
|
||||||
allEpisodesSortOrder = sortOrder
|
allEpisodesSortOrder = sortOrder
|
||||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(0))
|
EventFlow.postEvent(FlowEvent.FeedsSortedEvent())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,33 +2,31 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding
|
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController
|
import ac.mdiq.podcini.playback.PlaybackController
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
|
||||||
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||||
import ac.mdiq.podcini.storage.model.Chapter
|
import ac.mdiq.podcini.storage.model.Chapter
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
|
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||||
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.storage.utils.MediaType
|
import ac.mdiq.podcini.storage.utils.MediaType
|
||||||
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
import ac.mdiq.podcini.ui.actions.menuhandler.EpisodeMenuHandler
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
|
@ -41,7 +39,6 @@ import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||||
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||||
import ac.mdiq.podcini.ui.view.PlayButton
|
import ac.mdiq.podcini.ui.view.PlayButton
|
||||||
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
|
|
||||||
import ac.mdiq.podcini.util.Converter
|
import ac.mdiq.podcini.util.Converter
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||||
|
@ -52,7 +49,6 @@ import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.ImageButton
|
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.SeekBar
|
import android.widget.SeekBar
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
@ -93,23 +89,20 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
private var playerDetailsFragment: PlayerDetailsFragment? = null
|
private var playerDetailsFragment: PlayerDetailsFragment? = null
|
||||||
|
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private var playerFragment1: InternalPlayerFragment? = null
|
|
||||||
private var playerFragment2: InternalPlayerFragment? = null
|
|
||||||
private var playerFragment: InternalPlayerFragment? = null
|
|
||||||
|
|
||||||
private var playerView1: View? = null
|
private var playerUI1: PlayerUIFragment? = null
|
||||||
private var playerView2: View? = null
|
private var playerUI2: PlayerUIFragment? = null
|
||||||
|
private var playerUI: PlayerUIFragment? = null
|
||||||
|
private var playerUIView1: View? = null
|
||||||
|
private var playerUIView2: View? = null
|
||||||
|
|
||||||
private lateinit var cardViewSeek: CardView
|
private lateinit var cardViewSeek: CardView
|
||||||
private lateinit var txtvSeek: TextView
|
|
||||||
|
|
||||||
private var controller: PlaybackController? = null
|
private var controller: PlaybackController? = null
|
||||||
private var seekedToChapterStart = false
|
private var seekedToChapterStart = false
|
||||||
// private var currentChapterIndex = -1
|
// private var currentChapterIndex = -1
|
||||||
private var duration = 0
|
|
||||||
|
|
||||||
private var currentMedia: Playable? = null
|
private var currentMedia: Playable? = null
|
||||||
private var currentitem: Episode? = null
|
|
||||||
|
|
||||||
private var isShowPlay: Boolean = false
|
private var isShowPlay: Boolean = false
|
||||||
var isCollapsed = true
|
var isCollapsed = true
|
||||||
|
@ -136,25 +129,21 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
controller = createController()
|
controller = createController()
|
||||||
controller!!.init()
|
controller!!.init()
|
||||||
|
|
||||||
playerFragment1 = InternalPlayerFragment.newInstance(controller!!)
|
playerUI1 = PlayerUIFragment.newInstance(controller!!)
|
||||||
childFragmentManager.beginTransaction()
|
childFragmentManager.beginTransaction()
|
||||||
.replace(R.id.playerFragment1, playerFragment1!!, "InternalPlayerFragment1")
|
.replace(R.id.playerFragment1, playerUI1!!, "InternalPlayerFragment1")
|
||||||
.commit()
|
.commit()
|
||||||
playerView1 = binding.root.findViewById(R.id.playerFragment1)
|
playerUIView1 = binding.root.findViewById(R.id.playerFragment1)
|
||||||
playerView1?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
playerUIView1?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||||
|
|
||||||
playerFragment2 = InternalPlayerFragment.newInstance(controller!!)
|
playerUI2 = PlayerUIFragment.newInstance(controller!!)
|
||||||
childFragmentManager.beginTransaction()
|
childFragmentManager.beginTransaction()
|
||||||
.replace(R.id.playerFragment2, playerFragment2!!, "InternalPlayerFragment2")
|
.replace(R.id.playerFragment2, playerUI2!!, "InternalPlayerFragment2")
|
||||||
.commit()
|
.commit()
|
||||||
playerView2 = binding.root.findViewById(R.id.playerFragment2)
|
playerUIView2 = binding.root.findViewById(R.id.playerFragment2)
|
||||||
playerView2?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
playerUIView2?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||||
|
|
||||||
onCollaped()
|
onCollaped()
|
||||||
|
|
||||||
cardViewSeek = binding.cardViewSeek
|
cardViewSeek = binding.cardViewSeek
|
||||||
txtvSeek = binding.txtvSeek
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,18 +173,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
transaction.replace(R.id.itemDescription, playerDetailsFragment!!).commit()
|
transaction.replace(R.id.itemDescription, playerDetailsFragment!!).commit()
|
||||||
}
|
}
|
||||||
isCollapsed = false
|
isCollapsed = false
|
||||||
playerFragment = playerFragment2
|
playerUI = playerUI2
|
||||||
playerFragment?.updateUi(currentMedia)
|
playerUI?.updateUi(currentMedia)
|
||||||
playerFragment?.butPlay?.setIsShowPlay(isShowPlay)
|
playerUI?.butPlay?.setIsShowPlay(isShowPlay)
|
||||||
playerDetailsFragment?.load()
|
playerDetailsFragment?.updateInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onCollaped() {
|
fun onCollaped() {
|
||||||
Logd(TAG, "onCollaped()")
|
Logd(TAG, "onCollaped()")
|
||||||
isCollapsed = true
|
isCollapsed = true
|
||||||
playerFragment = playerFragment1
|
playerUI = playerUI1
|
||||||
playerFragment?.updateUi(currentMedia)
|
playerUI?.updateUi(currentMedia)
|
||||||
playerFragment?.butPlay?.setIsShowPlay(isShowPlay)
|
playerUI?.butPlay?.setIsShowPlay(isShowPlay)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setChapterDividers(media: Playable?) {
|
private fun setChapterDividers(media: Playable?) {
|
||||||
|
@ -218,11 +207,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
// }
|
// }
|
||||||
|
|
||||||
fun loadMediaInfo(includingChapters: Boolean) {
|
fun loadMediaInfo(includingChapters: Boolean) {
|
||||||
|
val actMain = (activity as MainActivity)
|
||||||
if (curMedia == null) {
|
if (curMedia == null) {
|
||||||
(activity as MainActivity).setPlayerVisible(false)
|
if (actMain.isPlayerVisible()) actMain.setPlayerVisible(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
(activity as MainActivity).setPlayerVisible(true)
|
if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true)
|
||||||
|
|
||||||
|
if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) playerDetailsFragment?.updateInfo()
|
||||||
|
|
||||||
if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) {
|
if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) {
|
||||||
Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters")
|
Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters")
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -232,9 +225,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
currentMedia = media
|
currentMedia = media
|
||||||
|
if (currentMedia is EpisodeMedia) {
|
||||||
|
val item = (currentMedia as EpisodeMedia).episode
|
||||||
|
if (item != null) playerDetailsFragment?.setItem(item)
|
||||||
|
}
|
||||||
updateUi()
|
updateUi()
|
||||||
playerFragment?.updateUi(currentMedia)
|
playerUI?.updateUi(currentMedia)
|
||||||
if (!includingChapters) loadMediaInfo(true)
|
// TODO: disable for now
|
||||||
|
// if (!includingChapters) loadMediaInfo(true)
|
||||||
}.invokeOnCompletion { throwable ->
|
}.invokeOnCompletion { throwable ->
|
||||||
if (throwable!= null) {
|
if (throwable!= null) {
|
||||||
Log.e(TAG, Log.getStackTraceString(throwable))
|
Log.e(TAG, Log.getStackTraceString(throwable))
|
||||||
|
@ -247,16 +245,16 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
return object : PlaybackController(requireActivity()) {
|
return object : PlaybackController(requireActivity()) {
|
||||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
||||||
isShowPlay = showPlay
|
isShowPlay = showPlay
|
||||||
playerFragment?.butPlay?.setIsShowPlay(showPlay)
|
playerUI?.butPlay?.setIsShowPlay(showPlay)
|
||||||
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
|
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
|
||||||
}
|
}
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
this@AudioPlayerFragment.loadMediaInfo(false)
|
this@AudioPlayerFragment.loadMediaInfo(false)
|
||||||
if (!isCollapsed) playerDetailsFragment?.load()
|
if (!isCollapsed) playerDetailsFragment?.updateInfo()
|
||||||
}
|
}
|
||||||
override fun onPlaybackEnd() {
|
override fun onPlaybackEnd() {
|
||||||
isShowPlay = true
|
isShowPlay = true
|
||||||
playerFragment?.butPlay?.setIsShowPlay(true)
|
playerUI?.butPlay?.setIsShowPlay(true)
|
||||||
// playerFragment2?.butPlay?.setIsShowPlay(true)
|
// playerFragment2?.butPlay?.setIsShowPlay(true)
|
||||||
(activity as MainActivity).setPlayerVisible(false)
|
(activity as MainActivity).setPlayerVisible(false)
|
||||||
}
|
}
|
||||||
|
@ -274,13 +272,20 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
retainInstance = true
|
retainInstance = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
Logd(TAG, "onResume() isCollapsed: $isCollapsed")
|
||||||
|
super.onResume()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
Logd(TAG, "onStart() isCollapsed: $isCollapsed")
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
loadMediaInfo(false)
|
loadMediaInfo(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
Logd(TAG, "onStop()")
|
||||||
super.onStop()
|
super.onStop()
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates
|
||||||
|
@ -307,19 +312,23 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
private fun onEvenStartPlay(event: FlowEvent.PlayEvent) {
|
private fun onEvenStartPlay(event: FlowEvent.PlayEvent) {
|
||||||
Logd(TAG, "onEvenStartPlay ${event.episode.title}")
|
Logd(TAG, "onEvenStartPlay ${event.episode.title}")
|
||||||
currentitem = event.episode
|
val currentitem = event.episode
|
||||||
if (currentMedia?.getIdentifier() == null || currentitem!!.media?.getIdentifier() != currentMedia?.getIdentifier())
|
if (currentMedia?.getIdentifier() == null || currentitem.media?.getIdentifier() != currentMedia?.getIdentifier()) {
|
||||||
playerDetailsFragment?.setItem(currentitem!!)
|
currentMedia = currentitem.media
|
||||||
|
playerDetailsFragment?.setItem(currentitem)
|
||||||
|
}
|
||||||
(activity as MainActivity).setPlayerVisible(true)
|
(activity as MainActivity).setPlayerVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventSink: Job? = null
|
private var eventSink: Job? = null
|
||||||
private fun cancelFlowEvents() {
|
private fun cancelFlowEvents() {
|
||||||
|
Logd(TAG, "cancelFlowEvents")
|
||||||
eventSink?.cancel()
|
eventSink?.cancel()
|
||||||
eventSink = null
|
eventSink = null
|
||||||
}
|
}
|
||||||
private fun procFlowEvents() {
|
private fun procFlowEvents() {
|
||||||
if (eventSink != null) return
|
if (eventSink != null) return
|
||||||
|
Logd(TAG, "procFlowEvents")
|
||||||
eventSink = lifecycleScope.launch {
|
eventSink = lifecycleScope.launch {
|
||||||
EventFlow.events.collectLatest { event ->
|
EventFlow.events.collectLatest { event ->
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
|
@ -327,25 +336,27 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
is FlowEvent.PlaybackServiceEvent -> {
|
is FlowEvent.PlaybackServiceEvent -> {
|
||||||
if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)
|
if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)
|
||||||
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
playerFragment?.onPlaybackServiceChanged(event)
|
playerUI?.onPlaybackServiceChanged(event)
|
||||||
}
|
}
|
||||||
is FlowEvent.PlayEvent -> onEvenStartPlay(event)
|
is FlowEvent.PlayEvent -> onEvenStartPlay(event)
|
||||||
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
|
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
|
||||||
is FlowEvent.FavoritesEvent -> loadMediaInfo(false)
|
is FlowEvent.FavoritesEvent -> loadMediaInfo(false)
|
||||||
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
||||||
|
is FlowEvent.PlaybackPositionEvent -> onPositionUpdate(event)
|
||||||
is FlowEvent.PlaybackPositionEvent -> playerFragment?.onPositionUpdate(event)
|
is FlowEvent.SpeedChangedEvent -> playerUI?.updatePlaybackSpeedButton(event)
|
||||||
is FlowEvent.SpeedChangedEvent -> playerFragment?.updatePlaybackSpeedButton(event)
|
|
||||||
|
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
|
// if (!isCollapsed) loadMediaInfo(false)
|
||||||
|
playerUI?.onPositionUpdate(event)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
|
||||||
when {
|
when {
|
||||||
fromUser -> {
|
fromUser -> {
|
||||||
val prog: Float = progress / (seekBar.max.toFloat())
|
val prog: Float = progress / (seekBar.max.toFloat())
|
||||||
|
@ -362,10 +373,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
// updateUi(controller!!.getMedia)
|
// updateUi(controller!!.getMedia)
|
||||||
// sbPosition.highlightCurrentChapter()
|
// sbPosition.highlightCurrentChapter()
|
||||||
// }
|
// }
|
||||||
txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${Converter.getDurationStringLong(position)}")
|
binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${Converter.getDurationStringLong(position)}")
|
||||||
} else txtvSeek.text = Converter.getDurationStringLong(position)
|
} else binding.txtvSeek.text = Converter.getDurationStringLong(position)
|
||||||
}
|
}
|
||||||
duration != playbackService?.duration -> updateUi()
|
duration != playbackService?.curDuration -> updateUi()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -403,8 +414,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
val isEpisodeMedia = media is EpisodeMedia
|
val isEpisodeMedia = media is EpisodeMedia
|
||||||
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
|
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
|
||||||
var item = currentitem
|
val item = if (isEpisodeMedia) (media as EpisodeMedia).episode else null
|
||||||
if (item == null && isEpisodeMedia) item = (media as EpisodeMedia).episode
|
|
||||||
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
|
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
|
||||||
|
|
||||||
val mediaType = curMedia?.getMediaType()
|
val mediaType = curMedia?.getMediaType()
|
||||||
|
@ -419,15 +429,16 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
|
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
|
||||||
val media: Playable = curMedia ?: return false
|
val media: Playable = curMedia ?: return false
|
||||||
|
val feedItem = if (media is EpisodeMedia) media.episode else null
|
||||||
var feedItem = currentitem
|
|
||||||
if (feedItem == null && media is EpisodeMedia) feedItem = media.episode
|
|
||||||
// feedItem: FeedItem? = if (media is EpisodeMedia) media.item else null
|
|
||||||
if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
|
if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
|
||||||
|
|
||||||
val itemId = menuItem.itemId
|
val itemId = menuItem.itemId
|
||||||
when (itemId) {
|
when (itemId) {
|
||||||
R.id.show_home_reader_view -> playerDetailsFragment?.buildHomeReaderText()
|
R.id.show_home_reader_view -> {
|
||||||
|
if (playerDetailsFragment?.showHomeText == true) menuItem.setIcon(R.drawable.ic_home)
|
||||||
|
else menuItem.setIcon(R.drawable.outline_home_24)
|
||||||
|
playerDetailsFragment?.buildHomeReaderText()
|
||||||
|
}
|
||||||
R.id.show_video -> {
|
R.id.show_video -> {
|
||||||
controller!!.playPause()
|
controller!!.playPause()
|
||||||
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
||||||
|
@ -463,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
fun fadePlayerToToolbar(slideOffset: Float) {
|
fun fadePlayerToToolbar(slideOffset: Float) {
|
||||||
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
|
val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat()
|
||||||
val player = playerView1
|
val player = playerUIView1
|
||||||
player?.alpha = 1 - playerFadeProgress
|
player?.alpha = 1 - playerFadeProgress
|
||||||
player?.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE
|
player?.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE
|
||||||
val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat()
|
val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat()
|
||||||
|
@ -471,65 +482,37 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE
|
toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
class InternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
class PlayerUIFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
||||||
val TAG = this::class.simpleName ?: "Anonymous"
|
val TAG = this::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
private var _binding: InternalPlayerFragmentBinding? = null
|
private var _binding: PlayerUiFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var imgvCover: ImageView
|
private lateinit var imgvCover: ImageView
|
||||||
var butPlay: PlayButton? = null
|
var butPlay: PlayButton? = null
|
||||||
|
|
||||||
private var isControlButtonsSet = false
|
private var isControlButtonsSet = false
|
||||||
|
|
||||||
private lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView
|
|
||||||
private lateinit var txtvPlaybackSpeed: TextView
|
|
||||||
|
|
||||||
private lateinit var episodeTitle: TextView
|
|
||||||
private lateinit var butRev: ImageButton
|
|
||||||
private lateinit var txtvRev: TextView
|
|
||||||
private lateinit var butFF: ImageButton
|
|
||||||
private lateinit var txtvFF: TextView
|
|
||||||
private lateinit var butSkip: ImageButton
|
|
||||||
private lateinit var txtvSkip: TextView
|
|
||||||
|
|
||||||
private lateinit var txtvPosition: TextView
|
|
||||||
private lateinit var txtvLength: TextView
|
private lateinit var txtvLength: TextView
|
||||||
private lateinit var sbPosition: ChapterSeekBar
|
private lateinit var sbPosition: ChapterSeekBar
|
||||||
|
|
||||||
private var prevMedia: Playable? = null
|
private var prevMedia: Playable? = null
|
||||||
|
|
||||||
private var showTimeLeft = false
|
private var showTimeLeft = false
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = InternalPlayerFragmentBinding.inflate(inflater)
|
_binding = PlayerUiFragmentBinding.inflate(inflater)
|
||||||
Logd(TAG, "fragment onCreateView")
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
|
||||||
episodeTitle = binding.titleView
|
|
||||||
butPlaybackSpeed = binding.butPlaybackSpeed
|
|
||||||
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
|
|
||||||
imgvCover = binding.imgvCover
|
imgvCover = binding.imgvCover
|
||||||
butPlay = binding.butPlay
|
butPlay = binding.butPlay
|
||||||
butRev = binding.butRev
|
|
||||||
txtvRev = binding.txtvRev
|
|
||||||
butFF = binding.butFF
|
|
||||||
txtvFF = binding.txtvFF
|
|
||||||
butSkip = binding.butSkip
|
|
||||||
txtvSkip = binding.txtvSkip
|
|
||||||
sbPosition = binding.sbPosition
|
sbPosition = binding.sbPosition
|
||||||
txtvPosition = binding.txtvPosition
|
|
||||||
txtvLength = binding.txtvLength
|
txtvLength = binding.txtvLength
|
||||||
|
|
||||||
setupLengthTextView()
|
setupLengthTextView()
|
||||||
setupControlButtons()
|
setupControlButtons()
|
||||||
butPlaybackSpeed.setOnClickListener {
|
binding.butPlaybackSpeed.setOnClickListener {
|
||||||
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
|
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
|
||||||
}
|
}
|
||||||
sbPosition.setOnSeekBarChangeListener(this)
|
sbPosition.setOnSeekBarChangeListener(this)
|
||||||
|
binding.playerUiFragment.setOnClickListener {
|
||||||
binding.internalPlayerFragment.setOnClickListener {
|
Logd(TAG, "playerUiFragment was clicked")
|
||||||
Logd(TAG, "internalPlayerFragment was clicked")
|
|
||||||
val media = curMedia
|
val media = curMedia
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
val mediaType = media.getMediaType()
|
val mediaType = media.getMediaType()
|
||||||
|
@ -540,32 +523,28 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
} else {
|
} else {
|
||||||
controller?.playPause()
|
controller?.playPause()
|
||||||
// controller!!.ensureService()
|
// controller!!.ensureService()
|
||||||
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), mediaType)
|
val intent = getPlayerActivityIntent(requireContext(), mediaType)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
butPlay?.setOnClickListener {
|
butPlay?.setOnClickListener {
|
||||||
if (controller == null) return@setOnClickListener
|
if (controller == null) return@setOnClickListener
|
||||||
|
// val media = curMedia
|
||||||
val media = curMedia
|
if (curMedia != null) {
|
||||||
if (media != null) {
|
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
||||||
if (media.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
|
||||||
controller!!.playPause()
|
controller!!.playPause()
|
||||||
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media.getMediaType()))
|
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
|
||||||
} else controller!!.playPause()
|
} else controller!!.playPause()
|
||||||
|
|
||||||
if (!isControlButtonsSet) {
|
if (!isControlButtonsSet) {
|
||||||
sbPosition.visibility = View.VISIBLE
|
sbPosition.visibility = View.VISIBLE
|
||||||
isControlButtonsSet = true
|
isControlButtonsSet = true
|
||||||
|
@ -573,17 +552,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun setupControlButtons() {
|
@OptIn(UnstableApi::class) private fun setupControlButtons() {
|
||||||
butRev.setOnClickListener {
|
binding.butRev.setOnClickListener {
|
||||||
if (controller != null && isPlaybackServiceReady()) {
|
if (controller != null && playbackService?.isServiceReady() == true) {
|
||||||
val curr: Int = position
|
playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000)
|
||||||
seekTo(curr - UserPreferences.rewindSecs * 1000)
|
|
||||||
sbPosition.visibility = View.VISIBLE
|
sbPosition.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
butRev.setOnLongClickListener {
|
binding.butRev.setOnLongClickListener {
|
||||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
|
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, binding.txtvRev)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
butPlay?.setOnLongClickListener {
|
butPlay?.setOnLongClickListener {
|
||||||
|
@ -593,61 +570,53 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
butFF.setOnClickListener {
|
binding.butFF.setOnClickListener {
|
||||||
if (controller != null && isPlaybackServiceReady()) {
|
if (controller != null && playbackService?.isServiceReady() == true) {
|
||||||
val curr: Int = position
|
playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000)
|
||||||
seekTo(curr + UserPreferences.fastForwardSecs * 1000)
|
|
||||||
sbPosition.visibility = View.VISIBLE
|
sbPosition.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
butFF.setOnLongClickListener {
|
binding.butFF.setOnLongClickListener {
|
||||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
|
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, binding.txtvFF)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
butSkip.setOnClickListener {
|
binding.butSkip.setOnClickListener {
|
||||||
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
|
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
|
||||||
val speedForward = UserPreferences.speedforwardSpeed
|
val speedForward = UserPreferences.speedforwardSpeed
|
||||||
if (speedForward > 0.1f) speedForward(speedForward)
|
if (speedForward > 0.1f) speedForward(speedForward)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
butSkip.setOnLongClickListener {
|
binding.butSkip.setOnLongClickListener {
|
||||||
activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
|
activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun speedForward(speed: Float) {
|
private fun speedForward(speed: Float) {
|
||||||
// playbackService?.speedForward(speed)
|
// playbackService?.speedForward(speed)
|
||||||
if (playbackService?.mediaPlayer == null || playbackService?.isFallbackSpeed == true) return
|
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
|
||||||
|
|
||||||
if (playbackService?.isSpeedForward == false) {
|
if (playbackService?.isSpeedForward == false) {
|
||||||
playbackService?.normalSpeed = playbackService?.mediaPlayer!!.getPlaybackSpeed()
|
playbackService?.normalSpeed = playbackService?.mPlayer!!.getPlaybackSpeed()
|
||||||
playbackService?.mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
playbackService?.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||||
} else playbackService?.mediaPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
} else playbackService?.mPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||||
|
|
||||||
playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward
|
playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun setupLengthTextView() {
|
@OptIn(UnstableApi::class) private fun setupLengthTextView() {
|
||||||
showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
||||||
txtvLength.setOnClickListener(View.OnClickListener {
|
txtvLength.setOnClickListener(View.OnClickListener {
|
||||||
if (controller == null) return@OnClickListener
|
if (controller == null) return@OnClickListener
|
||||||
showTimeLeft = !showTimeLeft
|
showTimeLeft = !showTimeLeft
|
||||||
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
||||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(position, duration))
|
onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, duration))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
|
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
|
||||||
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
|
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
|
||||||
txtvPlaybackSpeed.text = speedStr
|
binding.txtvPlaybackSpeed.text = speedStr
|
||||||
butPlaybackSpeed.setSpeed(event.newSpeed)
|
binding.butPlaybackSpeed.setSpeed(event.newSpeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
if (controller == null || position == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) return
|
if (curMedia?.getIdentifier() != event.media?.getIdentifier() || controller == null || curPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) return
|
||||||
|
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||||
val currentPosition: Int = converter.convert(event.position)
|
val currentPosition: Int = converter.convert(event.position)
|
||||||
val duration: Int = converter.convert(event.duration)
|
val duration: Int = converter.convert(event.duration)
|
||||||
|
@ -656,9 +625,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
binding.txtvPosition.text = Converter.getDurationStringLong(currentPosition)
|
||||||
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
|
binding.txtvPosition.setContentDescription(getString(R.string.position,
|
||||||
txtvPosition.setContentDescription(getString(R.string.position,
|
|
||||||
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
|
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
|
||||||
val showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
val showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
||||||
if (showTimeLeft) {
|
if (showTimeLeft) {
|
||||||
|
@ -670,7 +638,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
|
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
|
||||||
txtvLength.text = Converter.getDurationStringLong(duration)
|
txtvLength.text = Converter.getDurationStringLong(duration)
|
||||||
}
|
}
|
||||||
if (sbPosition.visibility == View.INVISIBLE && isPlaybackServiceReady()) sbPosition.visibility = View.VISIBLE
|
if (sbPosition.visibility == View.INVISIBLE && playbackService?.isServiceReady() == true) sbPosition.visibility = View.VISIBLE
|
||||||
|
|
||||||
if (!sbPosition.isPressed) {
|
if (!sbPosition.isPressed) {
|
||||||
val progress: Float = (event.position.toFloat()) / event.duration
|
val progress: Float = (event.position.toFloat()) / event.duration
|
||||||
|
@ -678,7 +646,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
sbPosition.progress = (progress * sbPosition.max).toInt()
|
sbPosition.progress = (progress * sbPosition.max).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onPlaybackServiceChanged(event: FlowEvent.PlaybackServiceEvent) {
|
fun onPlaybackServiceChanged(event: FlowEvent.PlaybackServiceEvent) {
|
||||||
when (event.action) {
|
when (event.action) {
|
||||||
FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false)
|
FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false)
|
||||||
|
@ -686,46 +653,37 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
// PlaybackServiceEvent.Action.SERVICE_RESTARTED -> (activity as MainActivity).setPlayerVisible(true)
|
// PlaybackServiceEvent.Action.SERVICE_RESTARTED -> (activity as MainActivity).setPlayerVisible(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onStart() {
|
@OptIn(UnstableApi::class) override fun onStart() {
|
||||||
Logd(TAG, "onStart() called")
|
Logd(TAG, "onStart() called")
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
binding.txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
|
||||||
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
|
binding.txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
|
||||||
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
|
if (UserPreferences.speedforwardSpeed > 0.1f) binding.txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
|
||||||
if (UserPreferences.speedforwardSpeed > 0.1f) txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
|
else binding.txtvSkip.visibility = View.GONE
|
||||||
else txtvSkip.visibility = View.GONE
|
|
||||||
val media = curMedia ?: return
|
val media = curMedia ?: return
|
||||||
updatePlaybackSpeedButton(FlowEvent.SpeedChangedEvent(getCurrentPlaybackSpeed(media)))
|
updatePlaybackSpeedButton(FlowEvent.SpeedChangedEvent(getCurrentPlaybackSpeed(media)))
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
Logd(TAG, "onPause() called")
|
Logd(TAG, "onPause() called")
|
||||||
super.onPause()
|
super.onPause()
|
||||||
controller?.pause()
|
controller?.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
|
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
if (isPlaybackServiceReady()) {
|
if (playbackService?.isServiceReady() == true) {
|
||||||
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
||||||
seekTo((prog * duration).toInt())
|
seekTo((prog * duration).toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun updateUi(media: Playable?) {
|
fun updateUi(media: Playable?) {
|
||||||
Logd(TAG, "updateUi called $media")
|
Logd(TAG, "updateUi called $media")
|
||||||
if (media == null) return
|
if (media == null) return
|
||||||
|
binding.titleView.text = media.getEpisodeTitle()
|
||||||
episodeTitle.text = media.getEpisodeTitle()
|
|
||||||
// (activity as MainActivity).setPlayerVisible(true)
|
// (activity as MainActivity).setPlayerVisible(true)
|
||||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(media.getPosition(), media.getDuration()))
|
onPositionUpdate(FlowEvent.PlaybackPositionEvent(media, media.getPosition(), media.getDuration()))
|
||||||
|
|
||||||
if (prevMedia?.getIdentifier() != media.getIdentifier()) {
|
if (prevMedia?.getIdentifier() != media.getIdentifier()) {
|
||||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
|
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
|
||||||
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
|
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
|
||||||
|
@ -761,18 +719,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var controller: PlaybackController? = null
|
var controller: PlaybackController? = null
|
||||||
fun newInstance(controller_: PlaybackController) : InternalPlayerFragment {
|
fun newInstance(controller_: PlaybackController) : PlayerUIFragment {
|
||||||
controller = controller_
|
controller = controller_
|
||||||
return InternalPlayerFragment()
|
return PlayerUIFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
|
val TAG = AudioPlayerFragment::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
fun isPlaybackServiceReady() : Boolean {
|
|
||||||
return playbackService?.isServiceReady() == true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
|
|
||||||
@UnstableApi abstract class BaseEpisodesFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener {
|
@UnstableApi abstract class BaseEpisodesFragment : Fragment(), SelectableAdapter.OnSelectModeListener, Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
val TAG = this::class.simpleName ?: "Anonymous"
|
val TAG = this::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
@ -96,25 +95,19 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
setupLoadMoreScrollListener()
|
setupLoadMoreScrollListener()
|
||||||
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||||
|
|
||||||
swipeActions = SwipeActions(this, getFragmentTag()).attachTo(recyclerView)
|
swipeActions = SwipeActions(this, TAG).attachTo(recyclerView)
|
||||||
lifecycle.addObserver(swipeActions)
|
lifecycle.addObserver(swipeActions)
|
||||||
swipeActions.setFilter(getFilter())
|
swipeActions.setFilter(getFilter())
|
||||||
refreshSwipeTelltale()
|
refreshSwipeTelltale()
|
||||||
binding.leftActionIcon.setOnClickListener {
|
binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||||
swipeActions.showDialog()
|
binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||||
}
|
|
||||||
binding.rightActionIcon.setOnClickListener {
|
|
||||||
swipeActions.showDialog()
|
|
||||||
}
|
|
||||||
|
|
||||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||||
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||||
|
|
||||||
swipeRefreshLayout = binding.swipeRefresh
|
swipeRefreshLayout = binding.swipeRefresh
|
||||||
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
||||||
swipeRefreshLayout.setOnRefreshListener {
|
swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) }
|
||||||
FeedUpdateManager.runOnceOrAsk(requireContext())
|
|
||||||
}
|
|
||||||
|
|
||||||
createListAdaptor()
|
createListAdaptor()
|
||||||
|
|
||||||
|
@ -232,7 +225,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
// Apparently, none of the visibility check method works reliably on its own, so we just use all.
|
// Apparently, none of the visibility check method works reliably on its own, so we just use all.
|
||||||
!userVisibleHint || !isVisible || !isMenuVisible -> return false
|
!userVisibleHint || !isVisible || !isMenuVisible -> return false
|
||||||
listAdapter.longPressedItem == null -> {
|
listAdapter.longPressedItem == null -> {
|
||||||
Log.i(TAG, "Selected item or listAdapter was null, ignoring selection")
|
Logd(TAG, "Selected item or listAdapter was null, ignoring selection")
|
||||||
return super.onContextItemSelected(item)
|
return super.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
listAdapter.onContextItemSelected(item) -> return true
|
listAdapter.onContextItemSelected(item) -> return true
|
||||||
|
@ -328,7 +321,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
speedDialView.visibility = View.GONE
|
speedDialView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.EpisodeEvent) {
|
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
||||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||||
for (item in event.episodes) {
|
for (item in event.episodes) {
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||||
|
@ -342,15 +335,14 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||||
if (currentPlaying != null && currentPlaying!!.isCurMedia)
|
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||||
currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
|
||||||
else {
|
else {
|
||||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||||
for (i in 0 until listAdapter.itemCount) {
|
for (i in 0 until listAdapter.itemCount) {
|
||||||
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
||||||
if (holder != null && holder.isCurMedia) {
|
if (holder != null && event.media?.getIdentifier() == holder.episode?.media?.getIdentifier()) {
|
||||||
currentPlaying = holder
|
currentPlaying = holder
|
||||||
holder.notifyPlaybackPositionUpdated(event)
|
holder.notifyPlaybackPositionUpdated(event)
|
||||||
break
|
break
|
||||||
|
@ -361,7 +353,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
private fun onKeyUp(event: KeyEvent) {
|
private fun onKeyUp(event: KeyEvent) {
|
||||||
if (!isAdded || !isVisible || !isMenuVisible) return
|
if (!isAdded || !isVisible || !isMenuVisible) return
|
||||||
|
|
||||||
when (event.keyCode) {
|
when (event.keyCode) {
|
||||||
KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0)
|
KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0)
|
||||||
KeyEvent.KEYCODE_B -> recyclerView.smoothScrollToPosition(listAdapter.itemCount)
|
KeyEvent.KEYCODE_B -> recyclerView.smoothScrollToPosition(listAdapter.itemCount)
|
||||||
|
@ -369,7 +360,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.EpisodeDownloadEvent) {
|
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||||
for (downloadUrl in event.urls) {
|
for (downloadUrl in event.urls) {
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl)
|
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl)
|
||||||
if (pos >= 0) listAdapter.notifyItemChangedCompat(pos)
|
if (pos >= 0) listAdapter.notifyItemChangedCompat(pos)
|
||||||
|
@ -393,9 +384,9 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||||
is FlowEvent.FeedListUpdateEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> loadItems()
|
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||||
is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event)
|
is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event)
|
||||||
is FlowEvent.EpisodeEvent -> onEventMainThread(event)
|
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -404,8 +395,8 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
EventFlow.stickyEvents.collectLatest { event ->
|
EventFlow.stickyEvents.collectLatest { event ->
|
||||||
Logd(TAG, "Received sticky event: ${event.TAG}")
|
Logd(TAG, "Received sticky event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event)
|
is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
|
||||||
is FlowEvent.FeedUpdateRunningEvent -> onEventMainThread(event)
|
is FlowEvent.FeedUpdateRunningEvent -> onFeedUpdateRunningEvent(event)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -456,15 +447,15 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
protected abstract fun loadTotalItemCount(): Int
|
protected abstract fun loadTotalItemCount(): Int
|
||||||
|
|
||||||
protected abstract fun getFilter(): EpisodeFilter
|
open fun getFilter(): EpisodeFilter {
|
||||||
|
return EpisodeFilter.unfiltered()
|
||||||
protected abstract fun getFragmentTag(): String
|
}
|
||||||
|
|
||||||
protected abstract fun getPrefName(): String
|
protected abstract fun getPrefName(): String
|
||||||
|
|
||||||
protected open fun updateToolbar() {}
|
protected open fun updateToolbar() {}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.FeedUpdateRunningEvent) {
|
private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdateRunningEvent) {
|
||||||
swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning
|
swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -475,6 +466,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val KEY_UP_ARROW = "up_arrow"
|
private const val KEY_UP_ARROW = "up_arrow"
|
||||||
const val EPISODES_PER_PAGE: Int = 150
|
const val EPISODES_PER_PAGE: Int = 50
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.getCurrentChapterIndex
|
import ac.mdiq.podcini.storage.utils.ChapterUtils.getCurrentChapterIndex
|
||||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
|
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChapters
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.storage.model.Chapter
|
import ac.mdiq.podcini.storage.model.Chapter
|
||||||
import ac.mdiq.podcini.storage.utils.EmbeddedChapterImage
|
import ac.mdiq.podcini.storage.utils.EmbeddedChapterImage
|
||||||
|
@ -159,6 +159,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
|
if (event.media?.getIdentifier() != media?.getIdentifier()) return
|
||||||
updateChapterSelection(getCurrentChapter(media), false)
|
updateChapterSelection(getCurrentChapter(media), false)
|
||||||
adapter.notifyTimeChanged(event.position.toLong())
|
adapter.notifyTimeChanged(event.position.toLong())
|
||||||
}
|
}
|
||||||
|
@ -166,7 +167,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
private fun getCurrentChapter(media: Playable?): Int {
|
private fun getCurrentChapter(media: Playable?): Int {
|
||||||
if (controller == null) return -1
|
if (controller == null) return -1
|
||||||
|
|
||||||
return getCurrentChapterIndex(media, position)
|
return getCurrentChapterIndex(media, curPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadMediaInfo(forceRefresh: Boolean) {
|
private fun loadMediaInfo(forceRefresh: Boolean) {
|
||||||
|
@ -274,12 +275,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
||||||
} else {
|
} else {
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
val imgUrl = EmbeddedChapterImage.getModelFor(media!!,position)
|
val imgUrl = EmbeddedChapterImage.getModelFor(media!!,position)
|
||||||
// if (imgUrl != null) Glide.with(context)
|
|
||||||
// .load(imgUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .dontAnimate()
|
|
||||||
// .transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
|
|
||||||
// .into(holder.image)
|
|
||||||
holder.image.load(imgUrl)
|
holder.image.load(imgUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,8 +33,6 @@ import ac.mdiq.podcini.util.event.FlowEvent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -63,13 +61,11 @@ import java.util.*
|
||||||
private var runningDownloads: Set<String> = HashSet()
|
private var runningDownloads: Set<String> = HashSet()
|
||||||
private var items: MutableList<Episode> = mutableListOf()
|
private var items: MutableList<Episode> = mutableListOf()
|
||||||
|
|
||||||
private lateinit var infoBar: TextView
|
|
||||||
private lateinit var adapter: DownloadsListAdapter
|
private lateinit var adapter: DownloadsListAdapter
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var recyclerView: EpisodesRecyclerView
|
private lateinit var recyclerView: EpisodesRecyclerView
|
||||||
private lateinit var swipeActions: SwipeActions
|
private lateinit var swipeActions: SwipeActions
|
||||||
private lateinit var speedDialView: SpeedDialView
|
private lateinit var speedDialView: SpeedDialView
|
||||||
private lateinit var progressBar: ProgressBar
|
|
||||||
private lateinit var emptyView: EmptyViewHandler
|
private lateinit var emptyView: EmptyViewHandler
|
||||||
|
|
||||||
private var displayUpArrow = false
|
private var displayUpArrow = false
|
||||||
|
@ -114,10 +110,7 @@ import java.util.*
|
||||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||||
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||||
|
|
||||||
progressBar = binding.progLoading
|
binding.progLoading.visibility = View.VISIBLE
|
||||||
progressBar.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
infoBar = binding.infoBar
|
|
||||||
|
|
||||||
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||||
speedDialView = multiSelectDial.fabSD
|
speedDialView = multiSelectDial.fabSD
|
||||||
|
@ -243,7 +236,7 @@ import java.util.*
|
||||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
val selectedItem: Episode? = adapter.longPressedItem
|
val selectedItem: Episode? = adapter.longPressedItem
|
||||||
if (selectedItem == null) {
|
if (selectedItem == null) {
|
||||||
Log.i(TAG, "Selected item at current position was null, ignoring selection")
|
Logd(TAG, "Selected item at current position was null, ignoring selection")
|
||||||
return super.onContextItemSelected(item)
|
return super.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
if (adapter.onContextItemSelected(item)) return true
|
if (adapter.onContextItemSelected(item)) return true
|
||||||
|
@ -287,12 +280,12 @@ import java.util.*
|
||||||
|
|
||||||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
// Logd(TAG, "onPlaybackPositionEvent called with ${event.TAG}")
|
// Logd(TAG, "onPlaybackPositionEvent called with ${event.TAG}")
|
||||||
if (currentPlaying != null && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||||
else {
|
else {
|
||||||
Logd(TAG, "onPlaybackPositionEvent ${event.TAG} search list")
|
Logd(TAG, "onPlaybackPositionEvent ${event.TAG} search list")
|
||||||
for (i in 0 until adapter.itemCount) {
|
for (i in 0 until adapter.itemCount) {
|
||||||
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
||||||
if (holder != null && holder.isCurMedia) {
|
if (holder != null && event.media?.getIdentifier() == holder.episode?.media?.getIdentifier()) {
|
||||||
currentPlaying = holder
|
currentPlaying = holder
|
||||||
holder.notifyPlaybackPositionUpdated(event)
|
holder.notifyPlaybackPositionUpdated(event)
|
||||||
break
|
break
|
||||||
|
@ -334,7 +327,7 @@ import java.util.*
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
items = result.toMutableList()
|
items = result.toMutableList()
|
||||||
// adapter.setDummyViews(0)
|
// adapter.setDummyViews(0)
|
||||||
progressBar.visibility = View.GONE
|
binding.progLoading.visibility = View.GONE
|
||||||
adapter.updateItems(result)
|
adapter.updateItems(result)
|
||||||
refreshInfoBar()
|
refreshInfoBar()
|
||||||
}
|
}
|
||||||
|
@ -362,12 +355,10 @@ import java.util.*
|
||||||
var info = String.format(Locale.getDefault(), "%d%s", items.size, getString(R.string.episodes_suffix))
|
var info = String.format(Locale.getDefault(), "%d%s", items.size, getString(R.string.episodes_suffix))
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
var sizeMB: Long = 0
|
var sizeMB: Long = 0
|
||||||
for (item in items) {
|
for (item in items) sizeMB += item.media?.size ?: 0
|
||||||
sizeMB += item.media?.size?:0
|
|
||||||
}
|
|
||||||
info += " • " + (sizeMB / 1000000) + " MB"
|
info += " • " + (sizeMB / 1000000) + " MB"
|
||||||
}
|
}
|
||||||
infoBar.text = info
|
binding.infoBar.text = info
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartSelectMode() {
|
override fun onStartSelectMode() {
|
||||||
|
|
|
@ -35,8 +35,6 @@ class EpisodeHomeFragment : Fragment() {
|
||||||
private var _binding: EpisodeHomeFragmentBinding? = null
|
private var _binding: EpisodeHomeFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
// private val ioScope = CoroutineScope(Dispatchers.IO) // IO dispatcher for initialization
|
|
||||||
|
|
||||||
private var startIndex = 0
|
private var startIndex = 0
|
||||||
private var ttsSpeed = 1.0f
|
private var ttsSpeed = 1.0f
|
||||||
|
|
||||||
|
@ -181,6 +179,9 @@ class EpisodeHomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
menu.findItem(R.id.share_notes)?.setVisible(readMode)
|
menu.findItem(R.id.share_notes)?.setVisible(readMode)
|
||||||
menu.findItem(R.id.switchJS)?.setVisible(!readMode)
|
menu.findItem(R.id.switchJS)?.setVisible(!readMode)
|
||||||
|
val btn = menu.findItem(R.id.switch_home)
|
||||||
|
if (readMode) btn?.setIcon(R.drawable.baseline_home_24)
|
||||||
|
else btn?.setIcon(R.drawable.outline_home_24)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
|
||||||
|
@ -191,12 +192,10 @@ class EpisodeHomeFragment : Fragment() {
|
||||||
@OptIn(UnstableApi::class) override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
@OptIn(UnstableApi::class) override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||||
when (menuItem.itemId) {
|
when (menuItem.itemId) {
|
||||||
R.id.switch_home -> {
|
R.id.switch_home -> {
|
||||||
Logd(TAG, "switch_home selected")
|
|
||||||
switchMode()
|
switchMode()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.switchJS -> {
|
R.id.switchJS -> {
|
||||||
Logd(TAG, "switchJS selected")
|
|
||||||
jsEnabled = !jsEnabled
|
jsEnabled = !jsEnabled
|
||||||
showWebContent()
|
showWebContent()
|
||||||
return true
|
return true
|
||||||
|
@ -287,11 +286,7 @@ class EpisodeHomeFragment : Fragment() {
|
||||||
fun newInstance(item: Episode): EpisodeHomeFragment {
|
fun newInstance(item: Episode): EpisodeHomeFragment {
|
||||||
val fragment = EpisodeHomeFragment()
|
val fragment = EpisodeHomeFragment()
|
||||||
Logd(TAG, "item.itemIdentifier ${item.identifier}")
|
Logd(TAG, "item.itemIdentifier ${item.identifier}")
|
||||||
if (item.identifier != currentItem?.identifier) {
|
if (item.identifier != currentItem?.identifier) currentItem = item
|
||||||
currentItem = item
|
|
||||||
} else {
|
|
||||||
// currentItem?.feed = item.feed
|
|
||||||
}
|
|
||||||
return fragment
|
return fragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
import ac.mdiq.podcini.storage.database.RealmDB.unmanagedCopy
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
@ -79,20 +80,11 @@ import kotlin.math.max
|
||||||
|
|
||||||
private lateinit var shownotesCleaner: ShownotesCleaner
|
private lateinit var shownotesCleaner: ShownotesCleaner
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var root: ViewGroup
|
|
||||||
private lateinit var webvDescription: ShownotesWebView
|
private lateinit var webvDescription: ShownotesWebView
|
||||||
private lateinit var txtvPodcast: TextView
|
|
||||||
private lateinit var txtvTitle: TextView
|
|
||||||
private lateinit var txtvDuration: TextView
|
|
||||||
private lateinit var txtvPublished: TextView
|
|
||||||
private lateinit var imgvCover: ImageView
|
private lateinit var imgvCover: ImageView
|
||||||
private lateinit var progbarDownload: CircularProgressBar
|
|
||||||
private lateinit var progbarLoading: ProgressBar
|
|
||||||
|
|
||||||
private lateinit var homeButtonAction: View
|
|
||||||
private lateinit var butAction1: ImageView
|
private lateinit var butAction1: ImageView
|
||||||
private lateinit var butAction2: ImageView
|
private lateinit var butAction2: ImageView
|
||||||
private lateinit var noMediaLabel: View
|
|
||||||
|
|
||||||
private var actionButton1: EpisodeActionButton? = null
|
private var actionButton1: EpisodeActionButton? = null
|
||||||
private var actionButton2: EpisodeActionButton? = null
|
private var actionButton2: EpisodeActionButton? = null
|
||||||
|
@ -101,7 +93,7 @@ import kotlin.math.max
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
|
||||||
_binding = EpisodeInfoFragmentBinding.inflate(inflater, container, false)
|
_binding = EpisodeInfoFragmentBinding.inflate(inflater, container, false)
|
||||||
root = binding.root
|
// root = binding.root
|
||||||
Logd(TAG, "fragment onCreateView")
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
|
||||||
toolbar = binding.toolbar
|
toolbar = binding.toolbar
|
||||||
|
@ -110,13 +102,9 @@ import kotlin.math.max
|
||||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
txtvPodcast = binding.txtvPodcast
|
binding.txtvPodcast.setOnClickListener { openPodcast() }
|
||||||
txtvPodcast.setOnClickListener { openPodcast() }
|
binding.txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||||
txtvTitle = binding.txtvTitle
|
binding.txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
||||||
txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
|
||||||
txtvDuration = binding.txtvDuration
|
|
||||||
txtvPublished = binding.txtvPublished
|
|
||||||
txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
|
||||||
webvDescription = binding.webvDescription
|
webvDescription = binding.webvDescription
|
||||||
webvDescription.setTimecodeSelectedListener { time: Int? ->
|
webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||||
val cMedia = curMedia
|
val cMedia = curMedia
|
||||||
|
@ -127,14 +115,10 @@ import kotlin.math.max
|
||||||
|
|
||||||
imgvCover = binding.imgvCover
|
imgvCover = binding.imgvCover
|
||||||
imgvCover.setOnClickListener { openPodcast() }
|
imgvCover.setOnClickListener { openPodcast() }
|
||||||
progbarDownload = binding.circularProgressBar
|
|
||||||
progbarLoading = binding.progbarLoading
|
|
||||||
homeButtonAction = binding.homeButton
|
|
||||||
butAction1 = binding.butAction1
|
butAction1 = binding.butAction1
|
||||||
butAction2 = binding.butAction2
|
butAction2 = binding.butAction2
|
||||||
noMediaLabel = binding.noMediaLabel
|
|
||||||
|
|
||||||
homeButtonAction.setOnClickListener {
|
binding.homeButton.setOnClickListener {
|
||||||
if (!item?.link.isNullOrEmpty()) {
|
if (!item?.link.isNullOrEmpty()) {
|
||||||
homeFragment = EpisodeHomeFragment.newInstance(item!!)
|
homeFragment = EpisodeHomeFragment.newInstance(item!!)
|
||||||
(activity as MainActivity).loadChildFragment(homeFragment!!)
|
(activity as MainActivity).loadChildFragment(homeFragment!!)
|
||||||
|
@ -242,7 +226,7 @@ import kotlin.math.max
|
||||||
@UnstableApi override fun onResume() {
|
@UnstableApi override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (itemLoaded) {
|
if (itemLoaded) {
|
||||||
progbarLoading.visibility = View.GONE
|
binding.progbarLoading.visibility = View.GONE
|
||||||
updateAppearance()
|
updateAppearance()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,13 +234,14 @@ import kotlin.math.max
|
||||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
_binding = null
|
|
||||||
|
binding.root.removeView(webvDescription)
|
||||||
root.removeView(webvDescription)
|
|
||||||
webvDescription.clearHistory()
|
webvDescription.clearHistory()
|
||||||
webvDescription.clearCache(true)
|
webvDescription.clearCache(true)
|
||||||
webvDescription.clearView()
|
webvDescription.clearView()
|
||||||
webvDescription.destroy()
|
webvDescription.destroy()
|
||||||
|
|
||||||
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun onFragmentLoaded() {
|
@UnstableApi private fun onFragmentLoaded() {
|
||||||
|
@ -276,14 +261,14 @@ import kotlin.math.max
|
||||||
// these are already available via button1 and button2
|
// these are already available via button1 and button2
|
||||||
else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
|
else EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
|
||||||
|
|
||||||
if (item!!.feed != null) txtvPodcast.text = item!!.feed!!.title
|
if (item!!.feed != null) binding.txtvPodcast.text = item!!.feed!!.title
|
||||||
txtvTitle.text = item!!.title
|
binding.txtvTitle.text = item!!.title
|
||||||
binding.itemLink.text = item!!.link
|
binding.itemLink.text = item!!.link
|
||||||
|
|
||||||
if (item?.pubDate != null) {
|
if (item?.pubDate != null) {
|
||||||
val pubDateStr = DateFormatter.formatAbbrev(context, Date(item!!.pubDate))
|
val pubDateStr = DateFormatter.formatAbbrev(context, Date(item!!.pubDate))
|
||||||
txtvPublished.text = pubDateStr
|
binding.txtvPublished.text = pubDateStr
|
||||||
txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(item!!.pubDate)))
|
binding.txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(item!!.pubDate)))
|
||||||
}
|
}
|
||||||
|
|
||||||
val media = item?.media
|
val media = item?.media
|
||||||
|
@ -326,14 +311,14 @@ import kotlin.math.max
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun updateButtons() {
|
@UnstableApi private fun updateButtons() {
|
||||||
progbarDownload.visibility = View.GONE
|
binding.circularProgressBar.visibility = View.GONE
|
||||||
val dls = DownloadServiceInterface.get()
|
val dls = DownloadServiceInterface.get()
|
||||||
if (item != null && item!!.media != null && item!!.media!!.downloadUrl != null) {
|
if (item != null && item!!.media != null && item!!.media!!.downloadUrl != null) {
|
||||||
val url = item!!.media!!.downloadUrl!!
|
val url = item!!.media!!.downloadUrl!!
|
||||||
if (dls != null && dls.isDownloadingEpisode(url)) {
|
if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||||
progbarDownload.visibility = View.VISIBLE
|
binding.circularProgressBar.visibility = View.VISIBLE
|
||||||
progbarDownload.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), item)
|
binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), item)
|
||||||
progbarDownload.setIndeterminate(dls.isEpisodeQueued(url))
|
binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,12 +329,12 @@ import kotlin.math.max
|
||||||
butAction1.visibility = View.INVISIBLE
|
butAction1.visibility = View.INVISIBLE
|
||||||
actionButton2 = VisitWebsiteActionButton(item!!)
|
actionButton2 = VisitWebsiteActionButton(item!!)
|
||||||
}
|
}
|
||||||
noMediaLabel.visibility = View.VISIBLE
|
binding.noMediaLabel.visibility = View.VISIBLE
|
||||||
} else {
|
} else {
|
||||||
noMediaLabel.visibility = View.GONE
|
binding.noMediaLabel.visibility = View.GONE
|
||||||
if (media.getDuration() > 0) {
|
if (media.getDuration() > 0) {
|
||||||
txtvDuration.text = Converter.getDurationStringLong(media.getDuration())
|
binding.txtvDuration.text = Converter.getDurationStringLong(media.getDuration())
|
||||||
txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
binding.txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||||
}
|
}
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
actionButton1 = when {
|
actionButton1 = when {
|
||||||
|
@ -439,7 +424,7 @@ import kotlin.math.max
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun load() {
|
@UnstableApi private fun load() {
|
||||||
if (!itemLoaded) progbarLoading.visibility = View.VISIBLE
|
if (!itemLoaded) binding.progbarLoading.visibility = View.VISIBLE
|
||||||
|
|
||||||
Logd(TAG, "load() called")
|
Logd(TAG, "load() called")
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
@ -453,7 +438,7 @@ import kotlin.math.max
|
||||||
feedItem
|
feedItem
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
progbarLoading.visibility = View.GONE
|
binding.progbarLoading.visibility = View.GONE
|
||||||
item = result
|
item = result
|
||||||
onFragmentLoaded()
|
onFragmentLoaded()
|
||||||
itemLoaded = true
|
itemLoaded = true
|
||||||
|
@ -465,7 +450,7 @@ import kotlin.math.max
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setItem(item_: Episode) {
|
fun setItem(item_: Episode) {
|
||||||
item = item_
|
item = unmanagedCopy(item_)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -10,7 +10,8 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||||
import ac.mdiq.podcini.storage.database.LogsAndStats.getFeedDownloadLog
|
import ac.mdiq.podcini.storage.database.LogsAndStats.getFeedDownloadLog
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
import ac.mdiq.podcini.storage.database.RealmDB.unmanagedCopy
|
||||||
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
|
@ -89,6 +90,7 @@ import java.util.concurrent.Semaphore
|
||||||
private var enableFilter: Boolean = true
|
private var enableFilter: Boolean = true
|
||||||
|
|
||||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
private var onInit: Boolean = true
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -194,7 +196,6 @@ import java.util.concurrent.Semaphore
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
loadItems()
|
loadItems()
|
||||||
// realmFeedMonitor()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
@ -269,11 +270,14 @@ import java.util.concurrent.Semaphore
|
||||||
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
|
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
|
||||||
R.id.refresh_complete_item -> {
|
R.id.refresh_complete_item -> {
|
||||||
Thread {
|
Thread {
|
||||||
feed!!.nextPageLink = feed!!.downloadUrl
|
|
||||||
feed!!.pageNr = 0
|
|
||||||
try {
|
try {
|
||||||
runBlocking { resetPagedFeedPage(feed).join() }
|
if (feed != null) {
|
||||||
FeedUpdateManager.runOnce(requireContext(), feed)
|
val feed_ = unmanagedCopy(feed!!)
|
||||||
|
feed_.nextPageLink = feed_.downloadUrl
|
||||||
|
feed_.pageNr = 0
|
||||||
|
upsertBlk(feed_) {}
|
||||||
|
FeedUpdateManager.runOnce(requireContext(), feed_)
|
||||||
|
}
|
||||||
} catch (e: ExecutionException) {
|
} catch (e: ExecutionException) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
} catch (e: InterruptedException) {
|
} catch (e: InterruptedException) {
|
||||||
|
@ -302,19 +306,10 @@ import java.util.concurrent.Semaphore
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resetPagedFeedPage(feed: Feed?) : Job {
|
|
||||||
return runOnIOScope {
|
|
||||||
if (feed != null) {
|
|
||||||
feed.nextPageLink = feed.downloadUrl
|
|
||||||
upsert(feed) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
val selectedItem: Episode? = adapter.longPressedItem
|
val selectedItem: Episode? = adapter.longPressedItem
|
||||||
if (selectedItem == null) {
|
if (selectedItem == null) {
|
||||||
Log.i(TAG, "Selected item at current position was null, ignoring selection")
|
Logd(TAG, "Selected item at current position was null, ignoring selection")
|
||||||
return super.onContextItemSelected(item)
|
return super.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
if (adapter.onContextItemSelected(item)) return true
|
if (adapter.onContextItemSelected(item)) return true
|
||||||
|
@ -330,33 +325,6 @@ import java.util.concurrent.Semaphore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEpisodesFilterSortEvent(event: FlowEvent.EpisodesFilterOrSortEvent) {
|
|
||||||
// Logd(TAG, "onEvent() called with: event = [$event]")
|
|
||||||
if (event.feed.id == feed?.id) {
|
|
||||||
when (event.action) {
|
|
||||||
FlowEvent.EpisodesFilterOrSortEvent.Action.SORT_ORDER_CHANGED -> {
|
|
||||||
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
|
|
||||||
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
|
||||||
adapter.updateItems(episodes)
|
|
||||||
}
|
|
||||||
FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED -> {
|
|
||||||
episodes.clear()
|
|
||||||
if (enableFilter) {
|
|
||||||
feed!!.preferences?.filterString = event.feed.preferences?.filterString ?: ""
|
|
||||||
val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
|
|
||||||
episodes.addAll(episodes_)
|
|
||||||
} else {
|
|
||||||
episodes.addAll(feed!!.episodes)
|
|
||||||
}
|
|
||||||
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
|
|
||||||
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
|
||||||
binding.header.counts.text = episodes.size.toString()
|
|
||||||
adapter.updateItems(episodes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
||||||
// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
|
// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
|
||||||
if (feed == null || episodes.isEmpty()) return
|
if (feed == null || episodes.isEmpty()) return
|
||||||
|
@ -367,19 +335,8 @@ import java.util.concurrent.Semaphore
|
||||||
while (i < size) {
|
while (i < size) {
|
||||||
val item = event.episodes[i]
|
val item = event.episodes[i]
|
||||||
if (item.feedId != feed!!.id) continue
|
if (item.feedId != feed!!.id) continue
|
||||||
// Unmanaged embedded objects don't support parent access
|
|
||||||
// Logd(TAG, "item.media.parent: ${item.media?.parent<Episode>()?.title}")
|
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
val pos: Int = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||||
if (pos >= 0) {
|
if (pos >= 0) {
|
||||||
// Logd(TAG, "replacing episode: ${item.title} ${item.media?.downloaded} ${item.media?.fileUrl}")
|
|
||||||
// val item_ = getEpisode(item.id)
|
|
||||||
// if (item_ != null) Logd(TAG, "episode in DB: ${item_.title} ${item_.media?.downloaded} ${item_.media?.fileUrl}")
|
|
||||||
// val feed_ = getFeed(item.feedId?:0)
|
|
||||||
// if (feed_ != null) {
|
|
||||||
// for (item_1 in feed_.episodes) {
|
|
||||||
// Logd(TAG, "episode in Feed: ${item_1.title} ${item_1.media?.downloaded} ${item_1.media?.fileUrl != null}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
episodes.removeAt(pos)
|
episodes.removeAt(pos)
|
||||||
episodes.add(pos, item)
|
episodes.add(pos, item)
|
||||||
adapter.notifyItemChangedCompat(pos)
|
adapter.notifyItemChangedCompat(pos)
|
||||||
|
@ -449,12 +406,12 @@ import java.util.concurrent.Semaphore
|
||||||
|
|
||||||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||||
if (currentPlaying != null && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||||
else {
|
else {
|
||||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||||
for (i in 0 until adapter.itemCount) {
|
for (i in 0 until adapter.itemCount) {
|
||||||
val holder: EpisodeViewHolder? = binding.recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
val holder: EpisodeViewHolder? = binding.recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
||||||
if (holder != null && holder.isCurMedia) {
|
if (holder != null && event.media?.getIdentifier() == holder.episode?.media?.getIdentifier()) {
|
||||||
currentPlaying = holder
|
currentPlaying = holder
|
||||||
holder.notifyPlaybackPositionUpdated(event)
|
holder.notifyPlaybackPositionUpdated(event)
|
||||||
break
|
break
|
||||||
|
@ -483,12 +440,11 @@ import java.util.concurrent.Semaphore
|
||||||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
||||||
is FlowEvent.PlayEvent -> onEvenStartPlay(event)
|
is FlowEvent.PlayEvent -> onEvenStartPlay(event)
|
||||||
is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event)
|
is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event)
|
||||||
is FlowEvent.FeedPrefsChangeEvent -> onFeedPrefsChanged(event)
|
is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadItems()
|
||||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||||
is FlowEvent.EpisodesFilterOrSortEvent -> onEpisodesFilterSortEvent(event)
|
|
||||||
is FlowEvent.PlayerSettingsEvent -> loadItems()
|
is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||||
is FlowEvent.EpisodePlayedEvent -> onEpisodePlayedEvent(event)
|
is FlowEvent.EpisodePlayedEvent -> onEpisodePlayedEvent(event)
|
||||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
is FlowEvent.FeedListEvent -> if (feed != null && event.contains(feed!!)) loadItems()
|
||||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
|
@ -512,16 +468,6 @@ import java.util.concurrent.Semaphore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
|
|
||||||
Log.d(TAG,"onFeedPrefsChanged called")
|
|
||||||
if (feed?.id == event.prefs.feedID) {
|
|
||||||
feed!!.preferences = event.prefs
|
|
||||||
for (item in episodes) {
|
|
||||||
item.feed?.preferences = event.prefs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartSelectMode() {
|
override fun onStartSelectMode() {
|
||||||
swipeActions.detach()
|
swipeActions.detach()
|
||||||
if (feed != null && feed!!.isLocalFeed) dialBinding.fabSD.removeActionItemById(R.id.download_batch)
|
if (feed != null && feed!!.isLocalFeed) dialBinding.fabSD.removeActionItemById(R.id.download_batch)
|
||||||
|
@ -542,13 +488,6 @@ import java.util.concurrent.Semaphore
|
||||||
swipeActions.attachTo(binding.recyclerView)
|
swipeActions.attachTo(binding.recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) {
|
|
||||||
if (feed != null && event.contains(feed!!)) {
|
|
||||||
Logd(TAG, "onFeedListChanged called")
|
|
||||||
loadItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdateRunningEvent) {
|
private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdateRunningEvent) {
|
||||||
nextPageLoader.setLoadingState(event.isFeedUpdateRunning)
|
nextPageLoader.setLoadingState(event.isFeedUpdateRunning)
|
||||||
if (!event.isFeedUpdateRunning) nextPageLoader.root.visibility = View.GONE
|
if (!event.isFeedUpdateRunning) nextPageLoader.root.visibility = View.GONE
|
||||||
|
@ -612,13 +551,22 @@ import java.util.concurrent.Semaphore
|
||||||
binding.header.butFilter.setOnLongClickListener {
|
binding.header.butFilter.setOnLongClickListener {
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
enableFilter = !enableFilter
|
enableFilter = !enableFilter
|
||||||
if (enableFilter) binding.header.butFilter.setColorFilter(Color.WHITE)
|
episodes.clear()
|
||||||
else binding.header.butFilter.setColorFilter(Color.RED)
|
if (enableFilter) {
|
||||||
onEpisodesFilterSortEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED, feed!!))
|
binding.header.butFilter.setColorFilter(Color.WHITE)
|
||||||
|
val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
|
||||||
|
episodes.addAll(episodes_)
|
||||||
|
} else {
|
||||||
|
binding.header.butFilter.setColorFilter(Color.RED)
|
||||||
|
episodes.addAll(feed!!.episodes)
|
||||||
|
}
|
||||||
|
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
|
||||||
|
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
||||||
|
binding.header.counts.text = episodes.size.toString()
|
||||||
|
adapter.updateItems(episodes, feed)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.header.txtvFailure.setOnClickListener { showErrorDetails() }
|
binding.header.txtvFailure.setOnClickListener { showErrorDetails() }
|
||||||
binding.header.counts.text = adapter.itemCount.toString()
|
binding.header.counts.text = adapter.itemCount.toString()
|
||||||
headerCreated = true
|
headerCreated = true
|
||||||
|
@ -659,31 +607,34 @@ import java.util.concurrent.Semaphore
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
feed = withContext(Dispatchers.IO) {
|
feed = withContext(Dispatchers.IO) {
|
||||||
val feed_ = getFeed(feedID, true)
|
val feed_ = getFeed(feedID)
|
||||||
if (feed_ != null) {
|
if (feed_ != null) {
|
||||||
episodes.clear()
|
episodes.clear()
|
||||||
if (!feed_.preferences?.filterString.isNullOrEmpty()) {
|
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {
|
||||||
val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) }
|
val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) }
|
||||||
episodes.addAll(episodes_)
|
episodes.addAll(episodes_)
|
||||||
} else episodes.addAll(feed_.episodes)
|
} else episodes.addAll(feed_.episodes)
|
||||||
val sortOrder = fromCode(feed_.preferences?.sortOrderCode?:0)
|
val sortOrder = fromCode(feed_.preferences?.sortOrderCode?:0)
|
||||||
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
if (sortOrder != null) getPermutor(sortOrder).reorder(episodes)
|
||||||
var hasNonMediaItems = false
|
if (onInit) {
|
||||||
for (item in episodes) {
|
var hasNonMediaItems = false
|
||||||
// TODO: perhaps shouldn't set for all items, do it in the adaptor?
|
for (item in episodes) {
|
||||||
item.feed = feed_
|
if (item.media == null) {
|
||||||
if (item.media == null) hasNonMediaItems = true
|
hasNonMediaItems = true
|
||||||
// Logd(TAG, "loadItems ${item.media?.downloaded} ${item.title}")
|
break
|
||||||
}
|
}
|
||||||
if (hasNonMediaItems) {
|
}
|
||||||
ioScope.launch {
|
if (hasNonMediaItems) {
|
||||||
withContext(Dispatchers.IO) {
|
ioScope.launch {
|
||||||
if (!ttsReady) {
|
withContext(Dispatchers.IO) {
|
||||||
initializeTTS(requireContext())
|
if (!ttsReady) {
|
||||||
semaphore.acquire()
|
initializeTTS(requireContext())
|
||||||
|
semaphore.acquire()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onInit = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feed_
|
feed_
|
||||||
|
@ -695,7 +646,7 @@ import java.util.concurrent.Semaphore
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
adapter.setDummyViews(0)
|
adapter.setDummyViews(0)
|
||||||
if (feed != null && episodes.isNotEmpty()) {
|
if (feed != null && episodes.isNotEmpty()) {
|
||||||
adapter.updateItems(episodes)
|
adapter.updateItems(episodes, feed)
|
||||||
binding.header.counts.text = episodes.size.toString()
|
binding.header.counts.text = episodes.size.toString()
|
||||||
}
|
}
|
||||||
updateToolbar()
|
updateToolbar()
|
||||||
|
@ -737,14 +688,14 @@ import java.util.concurrent.Semaphore
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
|
Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
|
||||||
runOnIOScope {
|
runOnIOScope {
|
||||||
feed.preferences?.filterString = newFilterValues.joinToString()
|
|
||||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||||
if (feed_ != null) {
|
if (feed_ != null) {
|
||||||
realm.write {
|
realm.write {
|
||||||
findLatest(feed_)?.let { it.preferences?.filterString = feed.preferences?.filterString ?: "" }
|
findLatest(feed_)?.let {
|
||||||
|
it.preferences?.filterString = newFilterValues.joinToString()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else upsert(feed) {}
|
}
|
||||||
EventFlow.postEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED, feed))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -756,7 +707,6 @@ import java.util.concurrent.Semaphore
|
||||||
sortOrder = if (feed?.sortOrder == null) SortOrder.DATE_NEW_OLD
|
sortOrder = if (feed?.sortOrder == null) SortOrder.DATE_NEW_OLD
|
||||||
else feed.sortOrder
|
else feed.sortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) {
|
override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) {
|
||||||
if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW || ascending == SortOrder.COMPLETED_DATE_OLD_NEW
|
if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.PLAYED_DATE_OLD_NEW || ascending == SortOrder.COMPLETED_DATE_OLD_NEW
|
||||||
|| ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.RANDOM
|
|| ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.RANDOM
|
||||||
|
@ -765,24 +715,19 @@ import java.util.concurrent.Semaphore
|
||||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onSelectionChanged() {
|
@UnstableApi override fun onSelectionChanged() {
|
||||||
super.onSelectionChanged()
|
super.onSelectionChanged()
|
||||||
if (feed != null) {
|
if (feed != null) {
|
||||||
// val sortOrder = fromCode(feed.sortOrderCode)
|
|
||||||
// if (sortOrder != null) getPermutor(sortOrder).reorder(feed.episodes)
|
|
||||||
// EventFlow.postEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.SORT_ORDER_CHANGED, feed.id))
|
|
||||||
// persistEpisodeSortOrder(feed, sortOrder)
|
|
||||||
Logd(TAG, "persist Episode SortOrder")
|
Logd(TAG, "persist Episode SortOrder")
|
||||||
runOnIOScope {
|
runOnIOScope {
|
||||||
feed.sortOrder = sortOrder
|
|
||||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||||
if (feed_ != null) {
|
if (feed_ != null) {
|
||||||
realm.write {
|
realm.write {
|
||||||
findLatest(feed_)?.let { it.sortOrder = feed.sortOrder }
|
findLatest(feed_)?.let {
|
||||||
|
it.sortOrder = sortOrder
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else upsert(feed) {}
|
}
|
||||||
EventFlow.postEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.SORT_ORDER_CHANGED, feed))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||||
import ac.mdiq.podcini.databinding.FeedinfoBinding
|
import ac.mdiq.podcini.databinding.FeedinfoBinding
|
||||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
||||||
|
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||||
import ac.mdiq.podcini.net.utils.HtmlToPlainText
|
import ac.mdiq.podcini.net.utils.HtmlToPlainText
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL
|
import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL
|
||||||
|
@ -32,7 +32,6 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
@ -66,15 +65,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
private lateinit var feed: Feed
|
private lateinit var feed: Feed
|
||||||
private lateinit var imgvCover: ImageView
|
private lateinit var imgvCover: ImageView
|
||||||
private lateinit var txtvTitle: TextView
|
|
||||||
private lateinit var txtvDescription: TextView
|
|
||||||
private lateinit var txtvFundingUrl: TextView
|
|
||||||
private lateinit var lblSupport: TextView
|
|
||||||
private lateinit var txtvUrl: TextView
|
|
||||||
private lateinit var txtvAuthorHeader: TextView
|
|
||||||
private lateinit var imgvBackground: ImageView
|
private lateinit var imgvBackground: ImageView
|
||||||
private lateinit var infoContainer: View
|
|
||||||
private lateinit var header: View
|
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
|
|
||||||
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) {
|
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) {
|
||||||
|
@ -115,18 +106,10 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
appBar.addOnOffsetChangedListener(iconTintManager)
|
appBar.addOnOffsetChangedListener(iconTintManager)
|
||||||
|
|
||||||
imgvCover = binding.header.imgvCover
|
imgvCover = binding.header.imgvCover
|
||||||
txtvTitle = binding.header.txtvTitle
|
|
||||||
txtvAuthorHeader = binding.header.txtvAuthor
|
|
||||||
imgvBackground = binding.imgvBackground
|
imgvBackground = binding.imgvBackground
|
||||||
infoContainer = binding.infoContainer
|
|
||||||
header = binding.header.root
|
|
||||||
// https://github.com/bumptech/glide/issues/529
|
// https://github.com/bumptech/glide/issues/529
|
||||||
// imgvBackground.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
// imgvBackground.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
||||||
|
|
||||||
txtvDescription = binding.txtvDescription
|
|
||||||
txtvUrl = binding.txtvUrl
|
|
||||||
lblSupport = binding.lblSupport
|
|
||||||
txtvFundingUrl = binding.txtvFundingUrl
|
|
||||||
binding.header.episodes.text = feed.episodes.size.toString() + " episodes"
|
binding.header.episodes.text = feed.episodes.size.toString() + " episodes"
|
||||||
binding.header.episodes.setOnClickListener {
|
binding.header.episodes.setOnClickListener {
|
||||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||||
|
@ -134,13 +117,12 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.btnvRelatedFeeds.setOnClickListener {
|
binding.btnvRelatedFeeds.setOnClickListener {
|
||||||
val fragment = OnlineSearchFragment.newInstance(CombinedSearcher::class.java, "${txtvAuthorHeader.text} podcasts")
|
val fragment = OnlineSearchFragment.newInstance(CombinedSearcher::class.java, "${binding.header.txtvAuthor.text} podcasts")
|
||||||
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
(activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||||
}
|
}
|
||||||
|
|
||||||
txtvUrl.setOnClickListener(copyUrlToClipboard)
|
binding.txtvUrl.setOnClickListener(copyUrlToClipboard)
|
||||||
|
|
||||||
// val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
|
||||||
val feedId = feed.id
|
val feedId = feed.id
|
||||||
parentFragmentManager.beginTransaction().replace(R.id.statisticsFragmentContainer,
|
parentFragmentManager.beginTransaction().replace(R.id.statisticsFragmentContainer,
|
||||||
FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
|
FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
|
||||||
|
@ -158,8 +140,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
super.onConfigurationChanged(newConfig)
|
super.onConfigurationChanged(newConfig)
|
||||||
val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt()
|
val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt()
|
||||||
header.setPadding(horizontalSpacing, header.paddingTop, horizontalSpacing, header.paddingBottom)
|
binding.header.root.setPadding(horizontalSpacing, binding.header.root.paddingTop, horizontalSpacing, binding.header.root.paddingBottom)
|
||||||
infoContainer.setPadding(horizontalSpacing, infoContainer.paddingTop, horizontalSpacing, infoContainer.paddingBottom)
|
binding.infoContainer.setPadding(horizontalSpacing, binding.infoContainer.paddingTop, horizontalSpacing, binding.infoContainer.paddingBottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showFeed() {
|
private fun showFeed() {
|
||||||
|
@ -173,22 +155,22 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
txtvTitle.text = feed.title
|
binding.header.txtvTitle.text = feed.title
|
||||||
txtvTitle.setMaxLines(6)
|
binding.header.txtvTitle.setMaxLines(6)
|
||||||
|
|
||||||
val description: String = HtmlToPlainText.getPlainText(feed.description?:"")
|
val description: String = HtmlToPlainText.getPlainText(feed.description?:"")
|
||||||
txtvDescription.text = description
|
binding.txtvDescription.text = description
|
||||||
|
|
||||||
if (!feed.author.isNullOrEmpty()) txtvAuthorHeader.text = feed.author
|
if (!feed.author.isNullOrEmpty()) binding.header.txtvAuthor.text = feed.author
|
||||||
|
|
||||||
txtvUrl.text = feed.downloadUrl
|
binding.txtvUrl.text = feed.downloadUrl
|
||||||
txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
binding.txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||||
|
|
||||||
if (feed.paymentLinks.isEmpty()) {
|
if (feed.paymentLinks.isEmpty()) {
|
||||||
lblSupport.visibility = View.GONE
|
binding.lblSupport.visibility = View.GONE
|
||||||
txtvFundingUrl.visibility = View.GONE
|
binding.txtvFundingUrl.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
lblSupport.visibility = View.VISIBLE
|
binding.lblSupport.visibility = View.VISIBLE
|
||||||
val fundingList: ArrayList<FeedFunding> = feed.paymentLinks
|
val fundingList: ArrayList<FeedFunding> = feed.paymentLinks
|
||||||
|
|
||||||
// Filter for duplicates, but keep items in the order that they have in the feed.
|
// Filter for duplicates, but keep items in the order that they have in the feed.
|
||||||
|
@ -212,7 +194,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
str.append("\n")
|
str.append("\n")
|
||||||
}
|
}
|
||||||
str = StringBuilder(StringUtils.trim(str.toString()))
|
str = StringBuilder(StringUtils.trim(str.toString()))
|
||||||
txtvFundingUrl.text = str.toString()
|
binding.txtvFundingUrl.text = str.toString()
|
||||||
}
|
}
|
||||||
refreshToolbarState()
|
refreshToolbarState()
|
||||||
}
|
}
|
||||||
|
@ -260,8 +242,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
object : EditUrlSettingsDialog(activity as Activity, feed) {
|
object : EditUrlSettingsDialog(activity as Activity, feed) {
|
||||||
override fun setUrl(url: String?) {
|
override fun setUrl(url: String?) {
|
||||||
feed.downloadUrl = url
|
feed.downloadUrl = url
|
||||||
txtvUrl.text = feed.downloadUrl
|
binding.txtvUrl.text = feed.downloadUrl
|
||||||
txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
binding.txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||||
}
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
|
|
|
@ -275,8 +275,8 @@ class FeedSettingsFragment : Fragment() {
|
||||||
// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feedPrefs!!.feedID))
|
// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feedPrefs!!.feedID))
|
||||||
}
|
}
|
||||||
updateVolumeAdaptationValue()
|
updateVolumeAdaptationValue()
|
||||||
if (feed != null && feedPrefs!!.volumeAdaptionSetting != null)
|
// if (feed != null && feedPrefs!!.volumeAdaptionSetting != null)
|
||||||
EventFlow.postEvent(FlowEvent.VolumeAdaptionChangedEvent(feedPrefs!!.volumeAdaptionSetting!!, feed!!.id))
|
// EventFlow.postEvent(FlowEvent.VolumeAdaptionChangedEvent(feedPrefs!!.volumeAdaptionSetting!!, feed!!.id))
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -297,32 +297,6 @@ class FeedSettingsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @OptIn(UnstableApi::class) private fun setupNewEpisodesAction() {
|
|
||||||
// if (feedPreferences == null) return
|
|
||||||
//
|
|
||||||
// findPreference<Preference>(PREF_NEW_EPISODES_ACTION)!!.onPreferenceChangeListener =
|
|
||||||
// Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any ->
|
|
||||||
// val code = (newValue as String).toInt()
|
|
||||||
// feedPreferences!!.newEpisodesAction = NewEpisodesAction.fromCode(code)
|
|
||||||
// DBWriter.setFeedPreferences(feedPreferences!!)
|
|
||||||
// updateNewEpisodesAction()
|
|
||||||
// false
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// private fun updateNewEpisodesAction() {
|
|
||||||
// if (feedPreferences == null || feedPreferences!!.newEpisodesAction == null) return
|
|
||||||
// val newEpisodesAction = findPreference<ListPreference>(PREF_NEW_EPISODES_ACTION)
|
|
||||||
// newEpisodesAction!!.value = "" + feedPreferences!!.newEpisodesAction!!.code
|
|
||||||
//
|
|
||||||
// when (feedPreferences!!.newEpisodesAction) {
|
|
||||||
// NewEpisodesAction.GLOBAL -> newEpisodesAction.setSummary(R.string.global_default)
|
|
||||||
//// NewEpisodesAction.ADD_TO_INBOX -> newEpisodesAction.setSummary(R.string.feed_new_episodes_action_add_to_inbox)
|
|
||||||
// NewEpisodesAction.NOTHING -> newEpisodesAction.setSummary(R.string.feed_new_episodes_action_nothing)
|
|
||||||
// else -> {}
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun setupKeepUpdatedPreference() {
|
@OptIn(UnstableApi::class) private fun setupKeepUpdatedPreference() {
|
||||||
if (feedPrefs == null) return
|
if (feedPrefs == null) return
|
||||||
val pref = findPreference<SwitchPreferenceCompat>("keepUpdated")
|
val pref = findPreference<SwitchPreferenceCompat>("keepUpdated")
|
||||||
|
|
|
@ -37,9 +37,7 @@ import kotlin.math.min
|
||||||
private var startDate : Long = 0L
|
private var startDate : Long = 0L
|
||||||
private var endDate : Long = Date().time
|
private var endDate : Long = Date().time
|
||||||
|
|
||||||
override fun getFragmentTag(): String {
|
var allHistory: List<Episode> = listOf()
|
||||||
return TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPrefName(): String {
|
override fun getPrefName(): String {
|
||||||
return TAG
|
return TAG
|
||||||
|
@ -89,10 +87,6 @@ import kotlin.math.min
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilter(): EpisodeFilter {
|
|
||||||
return EpisodeFilter.unfiltered()
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onMenuItemClick(item: MenuItem): Boolean {
|
@OptIn(UnstableApi::class) override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||||
if (super.onOptionsItemSelected(item)) return true
|
if (super.onOptionsItemSelected(item)) return true
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
@ -157,15 +151,15 @@ import kotlin.math.min
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadData(): List<Episode> {
|
override fun loadData(): List<Episode> {
|
||||||
val hList = getHistory(0, page * EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList()
|
allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList()
|
||||||
// FeedItemPermutors.getPermutor(sortOrder).reorder(hList)
|
return allHistory.subList(0, min(allHistory.size-1, page * EPISODES_PER_PAGE))
|
||||||
return hList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadMoreData(page: Int): List<Episode> {
|
override fun loadMoreData(page: Int): List<Episode> {
|
||||||
val hList = getHistory((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList()
|
val offset = (page - 1) * EPISODES_PER_PAGE
|
||||||
// FeedItemPermutors.getPermutor(sortOrder).reorder(hList)
|
if (offset >= allHistory.size) return listOf()
|
||||||
return hList
|
val toIndex = offset + EPISODES_PER_PAGE
|
||||||
|
return allHistory.subList(offset, min(allHistory.size, toIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadTotalItemCount(): Int {
|
override fun loadTotalItemCount(): Int {
|
||||||
|
@ -215,7 +209,7 @@ import kotlin.math.min
|
||||||
fun getHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time,
|
fun getHistory(offset: Int, limit: Int, start: Long = 0L, end: Long = Date().time,
|
||||||
sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD): List<Episode> {
|
sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD): List<Episode> {
|
||||||
Logd(TAG, "getHistory() called")
|
Logd(TAG, "getHistory() called")
|
||||||
val medias = realm.query(EpisodeMedia::class).query("lastPlayedTime > 0 AND lastPlayedTime <= $1", start, end).find()
|
val medias = realm.query(EpisodeMedia::class).query("lastPlayedTime > $0 AND lastPlayedTime <= $1", start, end).find()
|
||||||
var episodes: MutableList<Episode> = mutableListOf()
|
var episodes: MutableList<Episode> = mutableListOf()
|
||||||
for (m in medias) {
|
for (m in medias) {
|
||||||
if (m.episode != null) episodes.add(m.episode!!)
|
if (m.episode != null) episodes.add(m.episode!!)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory
|
import ac.mdiq.podcini.storage.algorithms.AutoCleanups
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.model.DatasetStats
|
import ac.mdiq.podcini.storage.model.DatasetStats
|
||||||
|
@ -413,7 +413,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
||||||
// queueSize = queue?.episodeIds?.size ?: 0
|
// queueSize = queue?.episodeIds?.size ?: 0
|
||||||
// }
|
// }
|
||||||
Logd(TAG, "getDatasetStats: queueSize: $queueSize")
|
Logd(TAG, "getDatasetStats: queueSize: $queueSize")
|
||||||
return DatasetStats(queueSize, numDownloadedItems, EpisodeCleanupAlgorithmFactory.build().getReclaimableItems(), numItems, numFeeds)
|
return DatasetStats(queueSize, numDownloadedItems, AutoCleanups.build().getReclaimableItems(), numItems, numFeeds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,10 +96,6 @@ import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
private var dialog: Dialog? = null
|
private var dialog: Dialog? = null
|
||||||
|
|
||||||
// private var download: Disposable? = null
|
|
||||||
// private var parser: Disposable? = null
|
|
||||||
// private var updater: Disposable? = null
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
||||||
binding.closeButton.visibility = View.INVISIBLE
|
binding.closeButton.visibility = View.INVISIBLE
|
||||||
|
@ -170,9 +166,6 @@ import kotlin.concurrent.Volatile
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
_binding = null
|
_binding = null
|
||||||
// updater?.dispose()
|
|
||||||
// download?.dispose()
|
|
||||||
// parser?.dispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onSaveInstanceState(outState: Bundle) {
|
@OptIn(UnstableApi::class) override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
@ -332,7 +325,7 @@ import kotlin.concurrent.Volatile
|
||||||
EventFlow.events.collectLatest { event ->
|
EventFlow.events.collectLatest { event ->
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
is FlowEvent.FeedListEvent -> onFeedListChanged(event)
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -348,7 +341,7 @@ import kotlin.concurrent.Volatile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) {
|
private fun onFeedListChanged(event: FlowEvent.FeedListEvent) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
val feeds = withContext(Dispatchers.IO) {
|
val feeds = withContext(Dispatchers.IO) {
|
||||||
|
|
|
@ -56,7 +56,7 @@ class OnlineSearchFragment : Fragment() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (searchProvider == null) Log.i(TAG,"Podcast searcher not found")
|
if (searchProvider == null) Logd(TAG,"Podcast searcher not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
|
import ac.mdiq.podcini.storage.database.Episodes.persistEpisode
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||||
|
@ -59,7 +59,7 @@ import org.apache.commons.lang3.StringUtils
|
||||||
* Displays the description of a Playable object in a Webview.
|
* Displays the description of a Playable object in a Webview.
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class PlayerDetailsFragment : Fragment() {
|
class PlayerDetailsFragment : Fragment() {
|
||||||
private lateinit var shownoteView: ShownotesWebView
|
private lateinit var shownoteView: ShownotesWebView
|
||||||
private var shownotesCleaner: ShownotesCleaner? = null
|
private var shownotesCleaner: ShownotesCleaner? = null
|
||||||
|
|
||||||
|
@ -67,8 +67,8 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var prevItem: Episode? = null
|
private var prevItem: Episode? = null
|
||||||
private var media: Playable? = null
|
private var playable: Playable? = null
|
||||||
private var item: Episode? = null
|
private var currentItem: Episode? = null
|
||||||
private var displayedChapterIndex = -1
|
private var displayedChapterIndex = -1
|
||||||
|
|
||||||
private var cleanedNotes: String? = null
|
private var cleanedNotes: String? = null
|
||||||
|
@ -80,8 +80,8 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
|
|
||||||
private val currentChapter: Chapter?
|
private val currentChapter: Chapter?
|
||||||
get() {
|
get() {
|
||||||
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||||
return media!!.getChapters()[displayedChapterIndex]
|
return playable!!.getChapters()[displayedChapterIndex]
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
@ -115,11 +115,13 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
|
Logd(TAG, "onStart()")
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
Logd(TAG, "onStop()")
|
||||||
super.onStop()
|
super.onStop()
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
}
|
}
|
||||||
|
@ -136,34 +138,34 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
return shownoteView.onContextItemSelected(item)
|
return shownoteView.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun load() {
|
internal fun updateInfo() {
|
||||||
// if (isLoading) return
|
// if (isLoading) return
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
Logd(TAG, "in load()")
|
Logd(TAG, "in updateInfo")
|
||||||
isLoading = true
|
isLoading = true
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
if (item == null) {
|
if (currentItem == null) {
|
||||||
media = curMedia
|
playable = curMedia
|
||||||
if (media != null && media is EpisodeMedia) {
|
if (playable != null && playable is EpisodeMedia) {
|
||||||
val episodeMedia = media as EpisodeMedia
|
val episodeMedia = playable as EpisodeMedia
|
||||||
item = episodeMedia.episode
|
currentItem = episodeMedia.episode
|
||||||
showHomeText = false
|
showHomeText = false
|
||||||
homeText = null
|
homeText = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item != null) {
|
if (currentItem != null) {
|
||||||
media = item!!.media
|
playable = currentItem!!.media
|
||||||
if (prevItem?.identifier != item!!.identifier) cleanedNotes = null
|
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
||||||
if (cleanedNotes == null) {
|
if (cleanedNotes == null) {
|
||||||
Logd(TAG, "calling load description ${item!!.description==null} ${item!!.title}")
|
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
||||||
cleanedNotes = shownotesCleaner?.processShownotes(item?.description ?: "", media?.getDuration()?:0)
|
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration()?:0)
|
||||||
}
|
}
|
||||||
prevItem = item
|
prevItem = currentItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
Logd(TAG, "subscribe: ${media?.getEpisodeTitle()}")
|
Logd(TAG, "subscribe: ${playable?.getEpisodeTitle()}")
|
||||||
displayMediaInfo(media!!)
|
displayMediaInfo(playable!!)
|
||||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||||
Logd(TAG, "Webview loaded")
|
Logd(TAG, "Webview loaded")
|
||||||
}
|
}
|
||||||
|
@ -177,17 +179,17 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
showHomeText = !showHomeText
|
showHomeText = !showHomeText
|
||||||
runOnIOScope {
|
runOnIOScope {
|
||||||
if (showHomeText) {
|
if (showHomeText) {
|
||||||
homeText = item!!.transcript
|
homeText = currentItem!!.transcript
|
||||||
if (homeText == null && item?.link != null) {
|
if (homeText == null && currentItem?.link != null) {
|
||||||
val url = item!!.link!!
|
val url = currentItem!!.link!!
|
||||||
val htmlSource = fetchHtmlSource(url)
|
val htmlSource = fetchHtmlSource(url)
|
||||||
val readability4J = Readability4J(item!!.link!!, htmlSource)
|
val readability4J = Readability4J(currentItem!!.link!!, htmlSource)
|
||||||
val article = readability4J.parse()
|
val article = readability4J.parse()
|
||||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||||
if (!readerhtml.isNullOrEmpty()) {
|
if (!readerhtml.isNullOrEmpty()) {
|
||||||
item!!.setTranscriptIfLonger(readerhtml)
|
currentItem!!.setTranscriptIfLonger(readerhtml)
|
||||||
homeText = item!!.transcript
|
homeText = currentItem!!.transcript
|
||||||
persistEpisode(item)
|
persistEpisode(currentItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!homeText.isNullOrEmpty()) {
|
if (!homeText.isNullOrEmpty()) {
|
||||||
|
@ -203,7 +205,7 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
|
||||||
} else {
|
} else {
|
||||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||||
cleanedNotes = shownotesCleaner?.processShownotes(item?.description ?: "", media?.getDuration() ?: 0)
|
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration() ?: 0)
|
||||||
if (!cleanedNotes.isNullOrEmpty()) {
|
if (!cleanedNotes.isNullOrEmpty()) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||||
|
@ -218,12 +220,12 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
||||||
Logd(TAG, "displayMediaInfo ${item?.title} ${media.getEpisodeTitle()}")
|
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
|
||||||
val pubDateStr = DateFormatter.formatAbbrev(context, media.getPubDate())
|
val pubDateStr = DateFormatter.formatAbbrev(context, media.getPubDate())
|
||||||
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||||
if (media is EpisodeMedia) {
|
if (media is EpisodeMedia) {
|
||||||
if (item?.feedId != null) {
|
if (currentItem?.feedId != null) {
|
||||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), item!!.feedId!!)
|
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!)
|
||||||
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -231,8 +233,8 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
||||||
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
||||||
binding.txtvEpisodeTitle.text = item?.title
|
binding.txtvEpisodeTitle.text = currentItem?.title
|
||||||
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(item?.title?:"") }
|
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(currentItem?.title?:"") }
|
||||||
binding.txtvEpisodeTitle.setOnClickListener {
|
binding.txtvEpisodeTitle.setOnClickListener {
|
||||||
val lines = binding.txtvEpisodeTitle.lineCount
|
val lines = binding.txtvEpisodeTitle.lineCount
|
||||||
val animUnit = 1500
|
val animUnit = 1500
|
||||||
|
@ -262,9 +264,9 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
private fun updateChapterControlVisibility() {
|
private fun updateChapterControlVisibility() {
|
||||||
var chapterControlVisible = false
|
var chapterControlVisible = false
|
||||||
when {
|
when {
|
||||||
media?.getChapters() != null -> chapterControlVisible = media!!.getChapters().isNotEmpty()
|
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
|
||||||
media is EpisodeMedia -> {
|
playable is EpisodeMedia -> {
|
||||||
val fm: EpisodeMedia? = (media as EpisodeMedia?)
|
val fm: EpisodeMedia? = (playable as EpisodeMedia?)
|
||||||
// If an item has chapters but they are not loaded yet, still display the button.
|
// If an item has chapters but they are not loaded yet, still display the button.
|
||||||
chapterControlVisible = fm?.episode != null && fm.episode!!.chapters.isNotEmpty()
|
chapterControlVisible = fm?.episode != null && fm.episode!!.chapters.isNotEmpty()
|
||||||
}
|
}
|
||||||
|
@ -278,9 +280,9 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshChapterData(chapterIndex: Int) {
|
private fun refreshChapterData(chapterIndex: Int) {
|
||||||
if (media != null && chapterIndex > -1) {
|
if (playable != null && chapterIndex > -1) {
|
||||||
if (media!!.getPosition() > media!!.getDuration() || chapterIndex >= media!!.getChapters().size - 1) {
|
if (playable!!.getPosition() > playable!!.getDuration() || chapterIndex >= playable!!.getChapters().size - 1) {
|
||||||
displayedChapterIndex = media!!.getChapters().size - 1
|
displayedChapterIndex = playable!!.getChapters().size - 1
|
||||||
binding.butNextChapter.visibility = View.INVISIBLE
|
binding.butNextChapter.visibility = View.INVISIBLE
|
||||||
} else {
|
} else {
|
||||||
displayedChapterIndex = chapterIndex
|
displayedChapterIndex = chapterIndex
|
||||||
|
@ -291,17 +293,17 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun displayCoverImage() {
|
private fun displayCoverImage() {
|
||||||
if (media == null) return
|
if (playable == null) return
|
||||||
if (displayedChapterIndex == -1 || media!!.getChapters().isEmpty() || media!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
if (displayedChapterIndex == -1 || playable!!.getChapters().isEmpty() || playable!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
||||||
val imageLoader = binding.imgvCover.context.imageLoader
|
val imageLoader = binding.imgvCover.context.imageLoader
|
||||||
val imageRequest = ImageRequest.Builder(requireContext())
|
val imageRequest = ImageRequest.Builder(requireContext())
|
||||||
.data(media!!.getImageLocation())
|
.data(playable!!.getImageLocation())
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
.placeholder(R.color.light_gray)
|
.placeholder(R.color.light_gray)
|
||||||
.listener(object : ImageRequest.Listener {
|
.listener(object : ImageRequest.Listener {
|
||||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||||
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
|
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
.error(R.mipmap.ic_launcher)
|
.error(R.mipmap.ic_launcher)
|
||||||
.target(binding.imgvCover)
|
.target(binding.imgvCover)
|
||||||
|
@ -314,7 +316,7 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
imageLoader.enqueue(imageRequest)
|
imageLoader.enqueue(imageRequest)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
val imgLoc = EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex)
|
val imgLoc = EmbeddedChapterImage.getModelFor(playable!!, displayedChapterIndex)
|
||||||
val imageLoader = binding.imgvCover.context.imageLoader
|
val imageLoader = binding.imgvCover.context.imageLoader
|
||||||
val imageRequest = ImageRequest.Builder(requireContext())
|
val imageRequest = ImageRequest.Builder(requireContext())
|
||||||
.data(imgLoc)
|
.data(imgLoc)
|
||||||
|
@ -323,7 +325,7 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
.listener(object : ImageRequest.Listener {
|
.listener(object : ImageRequest.Listener {
|
||||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||||
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
|
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
.error(R.mipmap.ic_launcher)
|
.error(R.mipmap.ic_launcher)
|
||||||
.target(binding.imgvCover)
|
.target(binding.imgvCover)
|
||||||
|
@ -343,19 +345,19 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
|
|
||||||
when {
|
when {
|
||||||
displayedChapterIndex < 1 -> seekTo(0)
|
displayedChapterIndex < 1 -> seekTo(0)
|
||||||
(position - 10000 * curSpeedMultiplier) < curr.start -> {
|
(curPosition - 10000 * curSpeedMultiplier) < curr.start -> {
|
||||||
refreshChapterData(displayedChapterIndex - 1)
|
refreshChapterData(displayedChapterIndex - 1)
|
||||||
if (media != null) seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
|
if (playable != null) seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||||
}
|
}
|
||||||
else -> seekTo(curr.start.toInt())
|
else -> seekTo(curr.start.toInt())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi private fun seekToNextChapter() {
|
@UnstableApi private fun seekToNextChapter() {
|
||||||
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media!!.getChapters().size) return
|
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= playable!!.getChapters().size) return
|
||||||
|
|
||||||
refreshChapterData(displayedChapterIndex + 1)
|
refreshChapterData(displayedChapterIndex + 1)
|
||||||
seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
|
seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -425,17 +427,18 @@ class PlayerDetailsFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
private fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(media, event.position)
|
if (playable?.getIdentifier() != event.media?.getIdentifier()) return
|
||||||
if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) {
|
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position)
|
||||||
|
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) {
|
||||||
refreshChapterData(newChapterIndex)
|
refreshChapterData(newChapterIndex)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setItem(item_: Episode) {
|
fun setItem(item_: Episode) {
|
||||||
Logd(TAG, "setItem ${item_.title}")
|
Logd(TAG, "setItem ${item_.title}")
|
||||||
if (item?.identifier != item_.identifier) {
|
if (currentItem?.identifier != item_.identifier) {
|
||||||
item = item_
|
currentItem = item_
|
||||||
showHomeText = false
|
showHomeText = false
|
||||||
homeText = null
|
homeText = null
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,6 @@ import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
@ -74,15 +72,13 @@ import java.util.*
|
||||||
private var _binding: QueueFragmentBinding? = null
|
private var _binding: QueueFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var infoBar: TextView
|
|
||||||
private lateinit var recyclerView: EpisodesRecyclerView
|
private lateinit var recyclerView: EpisodesRecyclerView
|
||||||
private lateinit var emptyView: EmptyViewHandler
|
private lateinit var emptyView: EmptyViewHandler
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
private lateinit var swipeActions: SwipeActions
|
private lateinit var swipeActions: SwipeActions
|
||||||
private lateinit var speedDialView: SpeedDialView
|
private lateinit var speedDialView: SpeedDialView
|
||||||
private lateinit var progressBar: ProgressBar
|
|
||||||
|
|
||||||
private var displayUpArrow = false
|
private var displayUpArrow = false
|
||||||
private var queueItems: MutableList<Episode> = mutableListOf()
|
private var queueItems: MutableList<Episode> = mutableListOf()
|
||||||
|
|
||||||
|
@ -112,10 +108,8 @@ import java.util.*
|
||||||
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
|
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
|
||||||
toolbar.inflateMenu(R.menu.queue)
|
toolbar.inflateMenu(R.menu.queue)
|
||||||
refreshToolbarState()
|
refreshToolbarState()
|
||||||
progressBar = binding.progressBar
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
progressBar.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
infoBar = binding.infoBar
|
|
||||||
recyclerView = binding.recyclerView
|
recyclerView = binding.recyclerView
|
||||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||||
if (animator != null && animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
if (animator != null && animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||||
|
@ -335,12 +329,12 @@ import java.util.*
|
||||||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||||
if (adapter != null) {
|
if (adapter != null) {
|
||||||
if (currentPlaying != null && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||||
else {
|
else {
|
||||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
Logd(TAG, "onPlaybackPositionEvent() ${event.TAG} search list")
|
||||||
for (i in 0 until adapter!!.itemCount) {
|
for (i in 0 until adapter!!.itemCount) {
|
||||||
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
||||||
if (holder != null && holder.isCurMedia) {
|
if (holder != null && event.media?.getIdentifier() == holder.episode?.media?.getIdentifier()) {
|
||||||
currentPlaying = holder
|
currentPlaying = holder
|
||||||
holder.notifyPlaybackPositionUpdated(event)
|
holder.notifyPlaybackPositionUpdated(event)
|
||||||
break
|
break
|
||||||
|
@ -370,7 +364,7 @@ import java.util.*
|
||||||
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
|
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
|
||||||
Log.d(TAG,"speedPresetChanged called")
|
Log.d(TAG,"speedPresetChanged called")
|
||||||
for (item in queueItems) {
|
for (item in queueItems) {
|
||||||
if (item.feed?.id == event.prefs.feedID) item.feed!!.preferences = event.prefs
|
if (item.feed?.id == event.feed.id) item.feed = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -470,14 +464,14 @@ import java.util.*
|
||||||
|
|
||||||
val selectedItem: Episode? = adapter!!.longPressedItem
|
val selectedItem: Episode? = adapter!!.longPressedItem
|
||||||
if (selectedItem == null) {
|
if (selectedItem == null) {
|
||||||
Log.i(TAG, "Selected item was null, ignoring selection")
|
Logd(TAG, "Selected item was null, ignoring selection")
|
||||||
return super.onContextItemSelected(item)
|
return super.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
if (adapter!!.onContextItemSelected(item)) return true
|
if (adapter!!.onContextItemSelected(item)) return true
|
||||||
|
|
||||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems.toList(), selectedItem.id)
|
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems.toList(), selectedItem.id)
|
||||||
if (pos < 0) {
|
if (pos < 0) {
|
||||||
Log.i(TAG, "Selected item no longer exist, ignoring selection")
|
Logd(TAG, "Selected item no longer exist, ignoring selection")
|
||||||
return super.onContextItemSelected(item)
|
return super.onContextItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -536,7 +530,7 @@ import java.util.*
|
||||||
info += " • "
|
info += " • "
|
||||||
info += Converter.getDurationStringLocalized(requireActivity(), timeLeft)
|
info += Converter.getDurationStringLocalized(requireActivity(), timeLeft)
|
||||||
}
|
}
|
||||||
infoBar.text = info
|
binding.infoBar.text = info
|
||||||
toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
|
toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -551,7 +545,7 @@ import java.util.*
|
||||||
|
|
||||||
queueItems.clear()
|
queueItems.clear()
|
||||||
queueItems.addAll(curQueue.episodes)
|
queueItems.addAll(curQueue.episodes)
|
||||||
progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
adapter?.setDummyViews(0)
|
adapter?.setDummyViews(0)
|
||||||
adapter?.updateItems(queueItems)
|
adapter?.updateItems(queueItems)
|
||||||
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||||
|
@ -562,13 +556,13 @@ import java.util.*
|
||||||
swipeActions.detach()
|
swipeActions.detach()
|
||||||
speedDialView.visibility = View.VISIBLE
|
speedDialView.visibility = View.VISIBLE
|
||||||
refreshToolbarState()
|
refreshToolbarState()
|
||||||
infoBar.visibility = View.GONE
|
binding.infoBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEndSelectMode() {
|
override fun onEndSelectMode() {
|
||||||
speedDialView.close()
|
speedDialView.close()
|
||||||
speedDialView.visibility = View.GONE
|
speedDialView.visibility = View.GONE
|
||||||
infoBar.visibility = View.VISIBLE
|
binding.infoBar.visibility = View.VISIBLE
|
||||||
swipeActions.attachTo(recyclerView)
|
swipeActions.attachTo(recyclerView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,7 +597,7 @@ import java.util.*
|
||||||
private fun reorderQueue(sortOrder: SortOrder?, broadcastUpdate: Boolean) : Job {
|
private fun reorderQueue(sortOrder: SortOrder?, broadcastUpdate: Boolean) : Job {
|
||||||
Logd(TAG, "reorderQueue called")
|
Logd(TAG, "reorderQueue called")
|
||||||
if (sortOrder == null) {
|
if (sortOrder == null) {
|
||||||
Log.w(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
|
Logd(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
|
||||||
return Job()
|
return Job()
|
||||||
}
|
}
|
||||||
val permutor = getPermutor(sortOrder)
|
val permutor = getPermutor(sortOrder)
|
||||||
|
|
|
@ -215,14 +215,6 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
||||||
val podcast: PodcastSearchResult? = getItem(position)
|
val podcast: PodcastSearchResult? = getItem(position)
|
||||||
holder.imageView!!.contentDescription = podcast?.title
|
holder.imageView!!.contentDescription = podcast?.title
|
||||||
|
|
||||||
// if (!podcast?.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
|
|
||||||
// .load(podcast?.imageUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .placeholder(R.color.light_gray)
|
|
||||||
// .transform(FitCenter(), RoundedCorners((8 * mainActivityRef.get()!!.resources.displayMetrics.density).toInt()))
|
|
||||||
// .dontAnimate())
|
|
||||||
// .into(holder.imageView!!)
|
|
||||||
|
|
||||||
holder.imageView?.load(podcast?.imageUrl) {
|
holder.imageView?.load(podcast?.imageUrl) {
|
||||||
placeholder(R.color.light_gray)
|
placeholder(R.color.light_gray)
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
|
|
|
@ -23,7 +23,6 @@ import kotlin.math.min
|
||||||
* Shows all episodes (possibly filtered by user).
|
* Shows all episodes (possibly filtered by user).
|
||||||
*/
|
*/
|
||||||
@UnstableApi class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
@UnstableApi class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
||||||
// val TAG = this::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
private val episodeList: MutableList<Episode> = mutableListOf()
|
private val episodeList: MutableList<Episode> = mutableListOf()
|
||||||
|
|
||||||
|
@ -31,17 +30,10 @@ import kotlin.math.min
|
||||||
val root = super.onCreateView(inflater, container, savedInstanceState)
|
val root = super.onCreateView(inflater, container, savedInstanceState)
|
||||||
Logd(TAG, "fragment onCreateView")
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
|
||||||
// val episodes_ = requireArguments().getSerializable(EXTRA_EPISODES) as? ArrayList<FeedItem>
|
|
||||||
// if (episodes_ != null) episodeList.addAll(episodes_)
|
|
||||||
|
|
||||||
toolbar.inflateMenu(R.menu.episodes)
|
toolbar.inflateMenu(R.menu.episodes)
|
||||||
toolbar.setTitle(R.string.episodes_label)
|
toolbar.setTitle(R.string.episodes_label)
|
||||||
updateToolbar()
|
updateToolbar()
|
||||||
listAdapter.setOnSelectModeListener(null)
|
listAdapter.setOnSelectModeListener(null)
|
||||||
// updateFilterUi()
|
|
||||||
// txtvInformation.setOnClickListener {
|
|
||||||
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
|
||||||
// }
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,21 +58,16 @@ import kotlin.math.min
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadMoreData(page: Int): List<Episode> {
|
override fun loadMoreData(page: Int): List<Episode> {
|
||||||
return episodeList.subList((page - 1) * EPISODES_PER_PAGE, min(episodeList.size, page * EPISODES_PER_PAGE))
|
val offset = (page - 1) * EPISODES_PER_PAGE
|
||||||
|
if (offset >= episodeList.size) return listOf()
|
||||||
|
val toIndex = offset + EPISODES_PER_PAGE
|
||||||
|
return episodeList.subList(offset, min(episodeList.size, toIndex))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadTotalItemCount(): Int {
|
override fun loadTotalItemCount(): Int {
|
||||||
return episodeList.size
|
return episodeList.size
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getFilter(): EpisodeFilter {
|
|
||||||
return EpisodeFilter.unfiltered()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFragmentTag(): String {
|
|
||||||
return TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPrefName(): String {
|
override fun getPrefName(): String {
|
||||||
return PREF_NAME
|
return PREF_NAME
|
||||||
}
|
}
|
||||||
|
@ -97,14 +84,6 @@ import kotlin.math.min
|
||||||
if (super.onOptionsItemSelected(item)) return true
|
if (super.onOptionsItemSelected(item)) return true
|
||||||
|
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
// R.id.filter_items -> {
|
|
||||||
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
// R.id.episodes_sort -> {
|
|
||||||
// AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
|
|
||||||
// return true
|
|
||||||
// }
|
|
||||||
else -> return false
|
else -> return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,44 +106,8 @@ import kotlin.math.min
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFilterUi() {
|
|
||||||
// swipeActions.setFilter(getFilter())
|
|
||||||
// when {
|
|
||||||
// getFilter().values.isNotEmpty() -> {
|
|
||||||
// txtvInformation.visibility = View.VISIBLE
|
|
||||||
// emptyView.setMessage(R.string.no_all_episodes_filtered_label)
|
|
||||||
// }
|
|
||||||
// else -> {
|
|
||||||
// txtvInformation.visibility = View.GONE
|
|
||||||
// emptyView.setMessage(R.string.no_all_episodes_label)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// toolbar.menu?.findItem(R.id.action_favorites)?.setIcon(
|
|
||||||
// if (getFilter().showIsFavorite) R.drawable.ic_star else R.drawable.ic_star_border)
|
|
||||||
}
|
|
||||||
|
|
||||||
// class AllEpisodesSortDialog : ItemSortDialog() {
|
|
||||||
// override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
// super.onCreate(savedInstanceState)
|
|
||||||
// sortOrder = allEpisodesSortOrder
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) {
|
|
||||||
// if (ascending == SortOrder.DATE_OLD_NEW || ascending == SortOrder.DURATION_SHORT_LONG) {
|
|
||||||
// super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// override fun onSelectionChanged() {
|
|
||||||
// super.onSelectionChanged()
|
|
||||||
// allEpisodesSortOrder = sortOrder
|
|
||||||
// EventBus.getDefault().post(FeedListUpdateEvent(0))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val PREF_NAME: String = "EpisodesListFragment"
|
const val PREF_NAME: String = "EpisodesListFragment"
|
||||||
const val EXTRA_EPISODES: String = "episodes_list"
|
|
||||||
|
|
||||||
fun newInstance(episodes: MutableList<Episode>): RemoteEpisodesFragment {
|
fun newInstance(episodes: MutableList<Episode>): RemoteEpisodesFragment {
|
||||||
val i = RemoteEpisodesFragment()
|
val i = RemoteEpisodesFragment()
|
||||||
|
|
|
@ -253,7 +253,7 @@ import java.lang.ref.WeakReference
|
||||||
EventFlow.events.collectLatest { event ->
|
EventFlow.events.collectLatest { event ->
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.FeedListUpdateEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> search()
|
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> search()
|
||||||
is FlowEvent.EpisodeEvent -> onEventMainThread(event)
|
is FlowEvent.EpisodeEvent -> onEventMainThread(event)
|
||||||
is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event)
|
is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event)
|
||||||
else -> {}
|
else -> {}
|
||||||
|
@ -295,13 +295,13 @@ import java.lang.ref.WeakReference
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||||
if (currentPlaying != null && currentPlaying!!.isCurMedia)
|
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia)
|
||||||
currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||||
else {
|
else {
|
||||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||||
for (i in 0 until adapter.itemCount) {
|
for (i in 0 until adapter.itemCount) {
|
||||||
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
val holder: EpisodeViewHolder? = recyclerView.findViewHolderForAdapterPosition(i) as? EpisodeViewHolder
|
||||||
if (holder != null && holder.isCurMedia) {
|
if (holder != null && event.media?.getIdentifier() == holder.episode?.media?.getIdentifier()) {
|
||||||
currentPlaying = holder
|
currentPlaying = holder
|
||||||
holder.notifyPlaybackPositionUpdated(event)
|
holder.notifyPlaybackPositionUpdated(event)
|
||||||
break
|
break
|
||||||
|
@ -509,13 +509,6 @@ import java.lang.ref.WeakReference
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (!podcast.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
|
|
||||||
// .load(podcast.imageUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .placeholder(R.color.light_gray)
|
|
||||||
// .fitCenter()
|
|
||||||
// .dontAnimate())
|
|
||||||
// .into(holder.imageView)
|
|
||||||
holder.imageView.load(podcast.imageUrl) {
|
holder.imageView.load(podcast.imageUrl) {
|
||||||
placeholder(R.color.light_gray)
|
placeholder(R.color.light_gray)
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
|
|
|
@ -5,10 +5,10 @@ import ac.mdiq.podcini.databinding.*
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.feedOrder
|
import ac.mdiq.podcini.preferences.UserPreferences.feedOrder
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.useGridLayout
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
import ac.mdiq.podcini.storage.database.Feeds.getTags
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedMap
|
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
|
@ -38,14 +38,12 @@ import android.widget.*
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.cardview.widget.CardView
|
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
import com.google.android.material.appbar.MaterialToolbar
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.elevation.SurfaceColors
|
import com.google.android.material.elevation.SurfaceColors
|
||||||
|
@ -72,14 +70,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var subscriptionRecycler: RecyclerView
|
private lateinit var subscriptionRecycler: RecyclerView
|
||||||
private lateinit var listAdapter: ListAdapter
|
private lateinit var listAdapter: SubscriptionsAdapter<*>
|
||||||
private lateinit var emptyView: EmptyViewHandler
|
private lateinit var emptyView: EmptyViewHandler
|
||||||
private lateinit var feedsInfoMsg: LinearLayout
|
|
||||||
private lateinit var feedsFilteredMsg: TextView
|
|
||||||
private lateinit var feedCount: TextView
|
|
||||||
private lateinit var toolbar: MaterialToolbar
|
private lateinit var toolbar: MaterialToolbar
|
||||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
|
||||||
private lateinit var progressBar: ProgressBar
|
|
||||||
private lateinit var speedDialView: SpeedDialView
|
private lateinit var speedDialView: SpeedDialView
|
||||||
|
|
||||||
private var tagFilterIndex = 1
|
private var tagFilterIndex = 1
|
||||||
|
@ -89,6 +82,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
private var feedList: MutableList<Feed> = mutableListOf()
|
private var feedList: MutableList<Feed> = mutableListOf()
|
||||||
private var feedListFiltered: List<Feed> = mutableListOf()
|
private var feedListFiltered: List<Feed> = mutableListOf()
|
||||||
|
|
||||||
|
private var useGrid: Boolean? = null
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
retainInstance = true
|
retainInstance = true
|
||||||
|
@ -120,20 +115,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
subscriptionRecycler.addItemDecoration(GridDividerItemDecorator())
|
subscriptionRecycler.addItemDecoration(GridDividerItemDecorator())
|
||||||
registerForContextMenu(subscriptionRecycler)
|
registerForContextMenu(subscriptionRecycler)
|
||||||
subscriptionRecycler.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
subscriptionRecycler.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||||
// subscriptionAdapter = object : SubscriptionsAdapter(activity as MainActivity) {
|
|
||||||
// override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
|
||||||
// super.onCreateContextMenu(menu, v, menuInfo)
|
|
||||||
// MenuItemUtils.setOnClickListeners(menu) { item: MenuItem ->
|
|
||||||
// this@SubscriptionsFragment.onContextItemSelected(item)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
listAdapter = ListAdapter()
|
|
||||||
val gridLayoutManager = GridLayoutManager(context, 1, RecyclerView.VERTICAL, false)
|
|
||||||
subscriptionRecycler.layoutManager = gridLayoutManager
|
|
||||||
|
|
||||||
listAdapter.setOnSelectModeListener(this)
|
initAdapter()
|
||||||
subscriptionRecycler.adapter = listAdapter
|
|
||||||
setupEmptyView()
|
setupEmptyView()
|
||||||
|
|
||||||
resetTags()
|
resetTags()
|
||||||
|
@ -163,31 +146,19 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
|
|
||||||
progressBar = binding.progressBar
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
progressBar.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
||||||
subscriptionAddButton.setOnClickListener {
|
subscriptionAddButton.setOnClickListener {
|
||||||
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(AddFeedFragment())
|
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(AddFeedFragment())
|
||||||
}
|
}
|
||||||
|
|
||||||
feedsInfoMsg = binding.feedsInfoMessage
|
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||||
// feedsInfoMsg.setOnClickListener {
|
binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
||||||
// SubscriptionsFilterDialog().show(
|
binding.swipeRefresh.setOnRefreshListener {
|
||||||
// childFragmentManager, "filter")
|
|
||||||
// }
|
|
||||||
feedsFilteredMsg = binding.feedsFilteredMessage
|
|
||||||
feedCount = binding.count
|
|
||||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
|
||||||
|
|
||||||
swipeRefreshLayout = binding.swipeRefresh
|
|
||||||
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
|
||||||
swipeRefreshLayout.setOnRefreshListener {
|
|
||||||
FeedUpdateManager.runOnceOrAsk(requireContext())
|
FeedUpdateManager.runOnceOrAsk(requireContext())
|
||||||
}
|
}
|
||||||
|
|
||||||
val speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
val speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||||
|
|
||||||
speedDialView = speedDialBinding.fabSD
|
speedDialView = speedDialBinding.fabSD
|
||||||
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
|
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
|
||||||
speedDialView.inflate(R.menu.nav_feed_action_speeddial)
|
speedDialView.inflate(R.menu.nav_feed_action_speeddial)
|
||||||
|
@ -201,14 +172,28 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
FeedMultiSelectActionHandler(activity as MainActivity, listAdapter.selectedItems.filterIsInstance<Feed>()).handleAction(actionItem.id)
|
FeedMultiSelectActionHandler(activity as MainActivity, listAdapter.selectedItems.filterIsInstance<Feed>()).handleAction(actionItem.id)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSubscriptions()
|
loadSubscriptions()
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initAdapter() {
|
||||||
|
if (useGrid != useGridLayout) {
|
||||||
|
useGrid = useGridLayout
|
||||||
|
var spanCount = 1
|
||||||
|
if (useGrid!!) {
|
||||||
|
listAdapter = GridAdapter()
|
||||||
|
spanCount = 3
|
||||||
|
} else listAdapter = ListAdapter()
|
||||||
|
subscriptionRecycler.layoutManager = GridLayoutManager(context, spanCount, RecyclerView.VERTICAL, false)
|
||||||
|
listAdapter.setOnSelectModeListener(this)
|
||||||
|
subscriptionRecycler.adapter = listAdapter
|
||||||
|
listAdapter.setItems(feedListFiltered)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
|
initAdapter()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,7 +227,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||||
listAdapter.setItems(feedListFiltered)
|
listAdapter.setItems(feedListFiltered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -259,7 +244,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
EventFlow.events.collectLatest { event ->
|
EventFlow.events.collectLatest { event ->
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
is FlowEvent.FeedListEvent -> onFeedListChanged(event)
|
||||||
is FlowEvent.EpisodePlayedEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
|
is FlowEvent.EpisodePlayedEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
|
||||||
is FlowEvent.FeedTagsChangedEvent -> resetTags()
|
is FlowEvent.FeedTagsChangedEvent -> resetTags()
|
||||||
else -> {}
|
else -> {}
|
||||||
|
@ -270,7 +255,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
EventFlow.stickyEvents.collectLatest { event ->
|
EventFlow.stickyEvents.collectLatest { event ->
|
||||||
Logd(TAG, "Received sticky event: ${event.TAG}")
|
Logd(TAG, "Received sticky event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.FeedUpdateRunningEvent -> swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning
|
is FlowEvent.FeedUpdateRunningEvent -> binding.swipeRefresh.isRefreshing = event.isFeedUpdateRunning
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -311,9 +296,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
if ( feedListFiltered.size > result.size) listAdapter.endSelectMode()
|
if ( feedListFiltered.size > result.size) listAdapter.endSelectMode()
|
||||||
feedList = result.toMutableList()
|
feedList = result.toMutableList()
|
||||||
filterOnTag()
|
filterOnTag()
|
||||||
progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
listAdapter.setItems(feedListFiltered)
|
listAdapter.setItems(feedListFiltered)
|
||||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||||
emptyView.updateVisibility()
|
emptyView.updateVisibility()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
@ -416,7 +401,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
val feed: Feed = listAdapter.getSelectedItem() ?: return false
|
val feed: Feed = listAdapter.selectedItem ?: return false
|
||||||
val itemId = item.itemId
|
val itemId = item.itemId
|
||||||
if (itemId == R.id.multi_select) {
|
if (itemId == R.id.multi_select) {
|
||||||
speedDialView.visibility = View.VISIBLE
|
speedDialView.visibility = View.VISIBLE
|
||||||
|
@ -426,8 +411,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() }
|
return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent?) {
|
private fun onFeedListChanged(event: FlowEvent.FeedListEvent) {
|
||||||
updateFeedMap()
|
// val feeds_ = realm.query(Feed::class,"id IN $0", event.feedIds).find()
|
||||||
|
// updateFeedMap(feeds_)
|
||||||
loadSubscriptions()
|
loadSubscriptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -535,12 +521,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
private inner class ListAdapter
|
private abstract inner class SubscriptionsAdapter<T : RecyclerView.ViewHolder?> : SelectableAdapter<T>(activity as MainActivity), View.OnCreateContextMenuListener {
|
||||||
: SelectableAdapter<ListAdapter.ViewHolder?>(activity as MainActivity), View.OnCreateContextMenuListener {
|
|
||||||
|
|
||||||
private var feedList: List<Feed>
|
protected var feedList: List<Feed>
|
||||||
private var selectedItem: Feed? = null
|
var selectedItem: Feed? = null
|
||||||
private var longPressedPosition: Int = 0 // used to init actionMode
|
protected var longPressedPosition: Int = 0 // used to init actionMode
|
||||||
val selectedItems: List<Any>
|
val selectedItems: List<Any>
|
||||||
get() {
|
get() {
|
||||||
val items = ArrayList<Feed>()
|
val items = ArrayList<Feed>()
|
||||||
|
@ -560,14 +545,49 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
fun getItem(position: Int): Any {
|
fun getItem(position: Int): Any {
|
||||||
return feedList[position]
|
return feedList[position]
|
||||||
}
|
}
|
||||||
fun getSelectedItem(): Feed? {
|
override fun getItemCount(): Int {
|
||||||
return selectedItem
|
return feedList.size
|
||||||
}
|
}
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
override fun getItemId(position: Int): Long {
|
||||||
|
if (position >= feedList.size) return RecyclerView.NO_ID // Dummy views
|
||||||
|
return feedList[position].id
|
||||||
|
}
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||||
|
if (selectedItem == null) return
|
||||||
|
val mainActRef = (activity as MainActivity)
|
||||||
|
val inflater: MenuInflater = mainActRef.menuInflater
|
||||||
|
if (inActionMode()) {
|
||||||
|
// inflater.inflate(R.menu.multi_select_context_popup, menu)
|
||||||
|
// menu.findItem(R.id.multi_select).setVisible(true)
|
||||||
|
} else {
|
||||||
|
inflater.inflate(R.menu.nav_feed_context, menu)
|
||||||
|
// menu.findItem(R.id.multi_select).setVisible(true)
|
||||||
|
menu.setHeaderTitle(selectedItem?.title)
|
||||||
|
}
|
||||||
|
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem ->
|
||||||
|
this@SubscriptionsFragment.onContextItemSelected(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
|
if (item.itemId == R.id.multi_select) {
|
||||||
|
startSelectMode(longPressedPosition)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fun setItems(listItems: List<Feed>) {
|
||||||
|
this.feedList = listItems
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ListAdapter : SubscriptionsAdapter<ViewHolderExpanded>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderExpanded {
|
||||||
val itemView: View = LayoutInflater.from(activity).inflate(R.layout.subscription_item, parent, false)
|
val itemView: View = LayoutInflater.from(activity).inflate(R.layout.subscription_item, parent, false)
|
||||||
return ViewHolder(itemView)
|
return ViewHolderExpanded(itemView)
|
||||||
}
|
}
|
||||||
@UnstableApi override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
@UnstableApi override fun onBindViewHolder(holder: ViewHolderExpanded, position: Int) {
|
||||||
val feed: Feed = feedList[position]
|
val feed: Feed = feedList[position]
|
||||||
holder.bind(feed)
|
holder.bind(feed)
|
||||||
if (inActionMode()) {
|
if (inActionMode()) {
|
||||||
|
@ -622,89 +642,149 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
override fun getItemCount(): Int {
|
}
|
||||||
return feedList.size
|
|
||||||
|
private inner class GridAdapter : SubscriptionsAdapter<ViewHolderBrief>() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderBrief {
|
||||||
|
val itemView: View = LayoutInflater.from(activity).inflate(R.layout.subscription_item_brief, parent, false)
|
||||||
|
return ViewHolderBrief(itemView)
|
||||||
}
|
}
|
||||||
override fun getItemId(position: Int): Long {
|
@UnstableApi override fun onBindViewHolder(holder: ViewHolderBrief, position: Int) {
|
||||||
if (position >= feedList.size) return RecyclerView.NO_ID // Dummy views
|
val feed: Feed = feedList[position]
|
||||||
return feedList[position].id
|
holder.bind(feed)
|
||||||
}
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
|
||||||
if (selectedItem == null) return
|
|
||||||
val mainActRef = (activity as MainActivity)
|
|
||||||
val inflater: MenuInflater = mainActRef.menuInflater
|
|
||||||
if (inActionMode()) {
|
if (inActionMode()) {
|
||||||
// inflater.inflate(R.menu.multi_select_context_popup, menu)
|
holder.selectCheckbox.visibility = View.VISIBLE
|
||||||
// menu.findItem(R.id.multi_select).setVisible(true)
|
holder.selectView.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
holder.selectCheckbox.setChecked(isSelected(position))
|
||||||
|
holder.selectCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||||
|
setSelected(holder.bindingAdapterPosition, isChecked)
|
||||||
|
}
|
||||||
|
holder.coverImage.alpha = 0.6f
|
||||||
|
holder.count.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
inflater.inflate(R.menu.nav_feed_context, menu)
|
holder.selectView.visibility = View.GONE
|
||||||
// menu.findItem(R.id.multi_select).setVisible(true)
|
holder.coverImage.alpha = 1.0f
|
||||||
menu.setHeaderTitle(selectedItem?.title)
|
|
||||||
}
|
}
|
||||||
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem ->
|
holder.coverImage.setOnClickListener {
|
||||||
this@SubscriptionsFragment.onContextItemSelected(item)
|
if (inActionMode()) holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||||
|
else {
|
||||||
|
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||||
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
holder.coverImage.setOnLongClickListener {
|
||||||
fun onContextItemSelected(item: MenuItem): Boolean {
|
longPressedPosition = holder.bindingAdapterPosition
|
||||||
if (item.itemId == R.id.multi_select) {
|
selectedItem = feed
|
||||||
startSelectMode(longPressedPosition)
|
startSelectMode(longPressedPosition)
|
||||||
return true
|
true
|
||||||
}
|
}
|
||||||
return false
|
holder.itemView.setOnTouchListener { _: View?, e: MotionEvent ->
|
||||||
}
|
if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) {
|
||||||
fun setItems(listItems: List<Feed>) {
|
if (!inActionMode()) {
|
||||||
this.feedList = listItems
|
longPressedPosition = holder.bindingAdapterPosition
|
||||||
notifyDataSetChanged()
|
selectedItem = feed
|
||||||
}
|
}
|
||||||
|
}
|
||||||
private inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
false
|
||||||
val binding = SubscriptionItemBinding.bind(itemView)
|
|
||||||
private val title = binding.titleLabel
|
|
||||||
private val producer = binding.producerLabel
|
|
||||||
val count: TextView = binding.countLabel
|
|
||||||
|
|
||||||
val coverImage: ImageView = binding.coverImage
|
|
||||||
val infoCard: LinearLayout = binding.infoCard
|
|
||||||
val selectView: FrameLayout = binding.selectContainer
|
|
||||||
val selectCheckbox: CheckBox = binding.selectCheckBox
|
|
||||||
private val card: CardView = binding.outerContainer
|
|
||||||
|
|
||||||
private val errorIcon: View = binding.errorIcon
|
|
||||||
|
|
||||||
fun bind(drawerItem: Feed) {
|
|
||||||
val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background)
|
|
||||||
selectView.background = drawable // Setting this in XML crashes API <= 21
|
|
||||||
title.text = drawerItem.title
|
|
||||||
producer.text = drawerItem.author
|
|
||||||
coverImage.contentDescription = drawerItem.title
|
|
||||||
coverImage.setImageDrawable(null)
|
|
||||||
|
|
||||||
val counter = drawerItem.episodes.size
|
|
||||||
count.text = NumberFormat.getInstance().format(counter.toLong()) + " episodes"
|
|
||||||
count.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
val mainActRef = (activity as MainActivity)
|
|
||||||
val coverLoader = CoverLoader(mainActRef)
|
|
||||||
val feed: Feed = drawerItem
|
|
||||||
coverLoader.withUri(feed.imageUrl)
|
|
||||||
errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
coverLoader.withCoverView(coverImage)
|
|
||||||
coverLoader.load()
|
|
||||||
|
|
||||||
val density: Float = mainActRef.resources.displayMetrics.density
|
|
||||||
card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
|
|
||||||
|
|
||||||
val textHPadding = 20
|
|
||||||
val textVPadding = 5
|
|
||||||
title.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
|
||||||
producer.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
|
||||||
count.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
|
||||||
|
|
||||||
val textSize = 14
|
|
||||||
title.textSize = textSize.toFloat()
|
|
||||||
}
|
}
|
||||||
|
holder.itemView.setOnClickListener {
|
||||||
|
if (inActionMode()) holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||||
|
else {
|
||||||
|
// val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||||
|
// mainActivityRef.get()?.loadChildFragment(fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ViewHolderExpanded(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
val binding = SubscriptionItemBinding.bind(itemView)
|
||||||
|
val count: TextView = binding.countLabel
|
||||||
|
|
||||||
|
val coverImage: ImageView = binding.coverImage
|
||||||
|
val infoCard: LinearLayout = binding.infoCard
|
||||||
|
val selectView: FrameLayout = binding.selectContainer
|
||||||
|
val selectCheckbox: CheckBox = binding.selectCheckBox
|
||||||
|
|
||||||
|
private val errorIcon: View = binding.errorIcon
|
||||||
|
|
||||||
|
fun bind(drawerItem: Feed) {
|
||||||
|
val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background)
|
||||||
|
selectView.background = drawable // Setting this in XML crashes API <= 21
|
||||||
|
binding.titleLabel.text = drawerItem.title
|
||||||
|
binding.producerLabel.text = drawerItem.author
|
||||||
|
coverImage.contentDescription = drawerItem.title
|
||||||
|
coverImage.setImageDrawable(null)
|
||||||
|
|
||||||
|
val counter = drawerItem.episodes.size
|
||||||
|
count.text = NumberFormat.getInstance().format(counter.toLong()) + " episodes"
|
||||||
|
count.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val mainActRef = (activity as MainActivity)
|
||||||
|
val coverLoader = CoverLoader(mainActRef)
|
||||||
|
val feed: Feed = drawerItem
|
||||||
|
coverLoader.withUri(feed.imageUrl)
|
||||||
|
errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
coverLoader.withCoverView(coverImage)
|
||||||
|
coverLoader.load()
|
||||||
|
|
||||||
|
val density: Float = mainActRef.resources.displayMetrics.density
|
||||||
|
binding.outerContainer.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
|
||||||
|
|
||||||
|
val textHPadding = 20
|
||||||
|
val textVPadding = 5
|
||||||
|
binding.titleLabel.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
||||||
|
binding.producerLabel.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
||||||
|
count.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
||||||
|
|
||||||
|
val textSize = 14
|
||||||
|
binding.titleLabel.textSize = textSize.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ViewHolderBrief(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
val binding = SubscriptionItemBriefBinding.bind(itemView)
|
||||||
|
private val title = binding.titleLabel
|
||||||
|
val count: TextView = binding.countLabel
|
||||||
|
|
||||||
|
val coverImage: ImageView = binding.coverImage
|
||||||
|
val selectView: FrameLayout = binding.selectContainer
|
||||||
|
val selectCheckbox: CheckBox = binding.selectCheckBox
|
||||||
|
|
||||||
|
private val errorIcon: View = binding.errorIcon
|
||||||
|
|
||||||
|
fun bind(drawerItem: Feed) {
|
||||||
|
val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context, R.drawable.ic_checkbox_background)
|
||||||
|
selectView.background = drawable // Setting this in XML crashes API <= 21
|
||||||
|
title.text = drawerItem.title
|
||||||
|
coverImage.contentDescription = drawerItem.title
|
||||||
|
coverImage.setImageDrawable(null)
|
||||||
|
|
||||||
|
val counter = drawerItem.episodes.size
|
||||||
|
count.text = NumberFormat.getInstance().format(counter.toLong())
|
||||||
|
count.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
val mainActRef = (activity as MainActivity)
|
||||||
|
val coverLoader = CoverLoader(mainActRef)
|
||||||
|
val feed: Feed = drawerItem
|
||||||
|
coverLoader.withUri(feed.imageUrl)
|
||||||
|
errorIcon.visibility = if (feed.lastUpdateFailed) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
coverLoader.withCoverView(coverImage)
|
||||||
|
coverLoader.load()
|
||||||
|
|
||||||
|
val density: Float = mainActRef.resources.displayMetrics.density
|
||||||
|
binding.outerContainer.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActRef, 1 * density))
|
||||||
|
|
||||||
|
val textHPadding = 20
|
||||||
|
val textVPadding = 5
|
||||||
|
title.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
||||||
|
count.setPadding(textHPadding, textVPadding, textHPadding, textVPadding)
|
||||||
|
|
||||||
|
val textSize = 14
|
||||||
|
title.textSize = textSize.toFloat()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ac.mdiq.podcini.playback.PlaybackController.Companion.duration
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.curPosition
|
||||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
|
@ -79,17 +79,40 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
var controller: PlaybackController? = null
|
var controller: PlaybackController? = null
|
||||||
var isFavorite = false
|
var isFavorite = false
|
||||||
|
|
||||||
|
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||||
|
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
||||||
|
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
||||||
|
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||||
|
if (event.x > v.measuredWidth / 2.0f) {
|
||||||
|
onFastForward()
|
||||||
|
showSkipAnimation(true)
|
||||||
|
} else {
|
||||||
|
onRewind()
|
||||||
|
showSkipAnimation(false)
|
||||||
|
}
|
||||||
|
if (videoControlsShowing) {
|
||||||
|
hideVideoControls(false)
|
||||||
|
if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||||
|
videoControlsShowing = false
|
||||||
|
}
|
||||||
|
return@OnTouchListener true
|
||||||
|
}
|
||||||
|
toggleVideoControlsVisibility()
|
||||||
|
if (videoControlsShowing) setupVideoControlsToggler()
|
||||||
|
|
||||||
|
lastScreenTap = System.currentTimeMillis()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
@OptIn(UnstableApi::class) override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||||
root = binding.root
|
root = binding.root
|
||||||
|
|
||||||
controller = newPlaybackController()
|
controller = newPlaybackController()
|
||||||
controller!!.init()
|
controller!!.init()
|
||||||
// loadMediaInfo()
|
// loadMediaInfo()
|
||||||
|
|
||||||
setupView()
|
setupView()
|
||||||
|
|
||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,15 +127,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
setupVideoAspectRatio()
|
setupVideoAspectRatio()
|
||||||
if (videoSurfaceCreated && controller != null) {
|
if (videoSurfaceCreated && controller != null) {
|
||||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||||
setVideoSurface(binding.videoView.holder)
|
// setVideoSurface(binding.videoView.holder)
|
||||||
|
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun loadMediaInfo() {
|
override fun loadMediaInfo() {
|
||||||
this@VideoEpisodeFragment.loadMediaInfo()
|
this@VideoEpisodeFragment.loadMediaInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlaybackEnd() {
|
override fun onPlaybackEnd() {
|
||||||
activity?.finish()
|
activity?.finish()
|
||||||
}
|
}
|
||||||
|
@ -131,7 +153,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
|
||||||
// Controller released; we will not receive buffering updates
|
// Controller released; we will not receive buffering updates
|
||||||
binding.progressBar.visibility = View.GONE
|
binding.progressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
@ -151,7 +172,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
_binding = null
|
_binding = null
|
||||||
controller?.release()
|
controller?.release()
|
||||||
controller = null // prevent leak
|
controller = null // prevent leak
|
||||||
// scope.cancel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var eventSink: Job? = null
|
private var eventSink: Job? = null
|
||||||
|
@ -204,7 +224,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
|
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
|
||||||
Logd(TAG, "loadMediaInfo called")
|
Logd(TAG, "loadMediaInfo called")
|
||||||
if (curMedia == null) return
|
if (curMedia == null) return
|
||||||
|
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||||
Logd(TAG, "Closing, no longer video")
|
Logd(TAG, "Closing, no longer video")
|
||||||
destroyingDueToReload = true
|
destroyingDueToReload = true
|
||||||
|
@ -245,7 +264,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInBackground(): Episode? {
|
private fun loadInBackground(): Episode? {
|
||||||
|
@ -266,11 +284,9 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
showTimeLeft = shouldShowRemainingTime()
|
showTimeLeft = shouldShowRemainingTime()
|
||||||
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
||||||
|
|
||||||
binding.durationLabel.setOnClickListener {
|
binding.durationLabel.setOnClickListener {
|
||||||
showTimeLeft = !showTimeLeft
|
showTimeLeft = !showTimeLeft
|
||||||
val media = curMedia ?: return@setOnClickListener
|
val media = curMedia ?: return@setOnClickListener
|
||||||
|
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||||
val length: String
|
val length: String
|
||||||
if (showTimeLeft) {
|
if (showTimeLeft) {
|
||||||
|
@ -281,7 +297,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
length = getDurationStringLong(duration)
|
length = getDurationStringLong(duration)
|
||||||
}
|
}
|
||||||
binding.durationLabel.text = length
|
binding.durationLabel.text = length
|
||||||
|
|
||||||
setShowRemainTimeSetting(showTimeLeft)
|
setShowRemainTimeSetting(showTimeLeft)
|
||||||
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||||
}
|
}
|
||||||
|
@ -304,15 +319,12 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
binding.bottomControlsContainer.fitsSystemWindows = true
|
||||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
|
|
||||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||||
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
||||||
binding.videoView.setAvailableSize(binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
binding.videoView.setAvailableSize(binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
||||||
}
|
}
|
||||||
|
|
||||||
webvDescription = binding.webvDescription
|
webvDescription = binding.webvDescription
|
||||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||||
// val cMedia = getMedia
|
// val cMedia = getMedia
|
||||||
|
@ -325,45 +337,11 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
// }
|
// }
|
||||||
// registerForContextMenu(webvDescription)
|
// registerForContextMenu(webvDescription)
|
||||||
// webvDescription.visibility = View.GONE
|
// webvDescription.visibility = View.GONE
|
||||||
|
binding.toggleViews.setOnClickListener { (activity as? VideoplayerActivity)?.toggleViews() }
|
||||||
binding.toggleViews.setOnClickListener {
|
|
||||||
(activity as? VideoplayerActivity)?.toggleViews()
|
|
||||||
}
|
|
||||||
binding.audioOnly.setOnClickListener {
|
binding.audioOnly.setOnClickListener {
|
||||||
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
||||||
(activity as? VideoplayerActivity)?.finish()
|
(activity as? VideoplayerActivity)?.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
|
||||||
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
|
||||||
|
|
||||||
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
|
||||||
|
|
||||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
|
||||||
if (event.x > v.measuredWidth / 2.0f) {
|
|
||||||
onFastForward()
|
|
||||||
showSkipAnimation(true)
|
|
||||||
} else {
|
|
||||||
onRewind()
|
|
||||||
showSkipAnimation(false)
|
|
||||||
}
|
|
||||||
if (videoControlsShowing) {
|
|
||||||
hideVideoControls(false)
|
|
||||||
if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
|
||||||
videoControlsShowing = false
|
|
||||||
}
|
|
||||||
return@OnTouchListener true
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleVideoControlsVisibility()
|
|
||||||
if (videoControlsShowing) setupVideoControlsToggler()
|
|
||||||
|
|
||||||
lastScreenTap = System.currentTimeMillis()
|
|
||||||
true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleVideoControlsVisibility() {
|
fun toggleVideoControlsVisibility() {
|
||||||
|
@ -393,17 +371,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||||
binding.skipAnimationImage.layoutParams = params
|
binding.skipAnimationImage.layoutParams = params
|
||||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
override fun onAnimationStart(animation: Animation) {}
|
override fun onAnimationStart(animation: Animation) {}
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
binding.skipAnimationImage.visibility = View.GONE
|
binding.skipAnimationImage.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation) {}
|
override fun onAnimationRepeat(animation: Animation) {}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -417,7 +392,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
Logd(TAG, "Videoview holder created")
|
Logd(TAG, "Videoview holder created")
|
||||||
videoSurfaceCreated = true
|
videoSurfaceCreated = true
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) setVideoSurface(holder)
|
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) setVideoSurface(holder)
|
||||||
|
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||||
setupVideoAspectRatio()
|
setupVideoAspectRatio()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,27 +407,24 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
fun notifyVideoSurfaceAbandoned() {
|
fun notifyVideoSurfaceAbandoned() {
|
||||||
// playbackService?.notifyVideoSurfaceAbandoned()
|
// playbackService?.notifyVideoSurfaceAbandoned()
|
||||||
playbackService?.mediaPlayer?.pause(abandonFocus = true, reinit = false)
|
playbackService?.mPlayer?.pause(abandonFocus = true, reinit = false)
|
||||||
playbackService?.mediaPlayer?.resetVideoSurface()
|
playbackService?.mPlayer?.resetVideoSurface()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setVideoSurface(holder: SurfaceHolder?) {
|
// fun setVideoSurface(holder: SurfaceHolder?) {
|
||||||
playbackService?.mediaPlayer?.setVideoSurface(holder)
|
// playbackService?.mPlayer?.setVideoSurface(holder)
|
||||||
}
|
// }
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onRewind() {
|
fun onRewind() {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||||
val curr = position
|
|
||||||
seekTo(curr - rewindSecs * 1000)
|
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onPlayPause() {
|
fun onPlayPause() {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
|
||||||
controller!!.playPause()
|
controller!!.playPause()
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
@ -459,9 +432,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
fun onFastForward() {
|
fun onFastForward() {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||||
val curr = position
|
|
||||||
seekTo(curr + fastForwardSecs * 1000)
|
|
||||||
setupVideoControlsToggler()
|
setupVideoControlsToggler()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,11 +483,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
private fun onPositionObserverUpdate() {
|
private fun onPositionObserverUpdate() {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||||
val currentPosition = converter.convert(position)
|
val currentPosition = converter.convert(curPosition)
|
||||||
val duration_ = converter.convert(duration)
|
val duration_ = converter.convert(duration)
|
||||||
val remainingTime = converter.convert(duration - position)
|
val remainingTime = converter.convert(duration - curPosition)
|
||||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
||||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||||
|
@ -537,7 +507,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
if (controller == null) return
|
if (controller == null) return
|
||||||
|
|
||||||
if (fromUser) {
|
if (fromUser) {
|
||||||
prog = progress / (seekBar.max.toFloat())
|
prog = progress / (seekBar.max.toFloat())
|
||||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||||
|
@ -559,7 +528,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
seekTo((prog * duration).toInt())
|
seekTo((prog * duration).toInt())
|
||||||
|
|
||||||
binding.seekCardView.scaleX = 1f
|
binding.seekCardView.scaleX = 1f
|
||||||
binding.seekCardView.scaleY = 1f
|
binding.seekCardView.scaleY = 1f
|
||||||
binding.seekCardView.animate()
|
binding.seekCardView.animate()
|
||||||
|
@ -574,6 +542,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
|
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
val videoSize: Pair<Int, Int>?
|
val videoSize: Pair<Int, Int>?
|
||||||
get() = playbackService?.mediaPlayer?.getVideoSize()
|
get() = playbackService?.mPlayer?.getVideoSize()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -50,14 +50,6 @@ abstract class StatisticsListAdapter protected constructor(@JvmField protected v
|
||||||
} else {
|
} else {
|
||||||
val holder = h as StatisticsHolder
|
val holder = h as StatisticsHolder
|
||||||
val statsItem = statisticsData!![position - 1]
|
val statsItem = statisticsData!![position - 1]
|
||||||
// if (!statsItem.feed.imageUrl.isNullOrBlank()) Glide.with(context)
|
|
||||||
// .load(statsItem.feed.imageUrl)
|
|
||||||
// .apply(RequestOptions()
|
|
||||||
// .placeholder(R.color.light_gray)
|
|
||||||
// .error(R.color.light_gray)
|
|
||||||
// .fitCenter()
|
|
||||||
// .dontAnimate())
|
|
||||||
// .into(holder.image)
|
|
||||||
holder.image.load(statsItem.feed.imageUrl) {
|
holder.image.load(statsItem.feed.imageUrl) {
|
||||||
placeholder(R.color.light_gray)
|
placeholder(R.color.light_gray)
|
||||||
error(R.mipmap.ic_launcher)
|
error(R.mipmap.ic_launcher)
|
||||||
|
|
|
@ -75,7 +75,7 @@ class CoverLoader(private val activity: MainActivity) {
|
||||||
.data(uri)
|
.data(uri)
|
||||||
.setHeader("User-Agent", "Mozilla/5.0")
|
.setHeader("User-Agent", "Mozilla/5.0")
|
||||||
.listener(object : ImageRequest.Listener {
|
.listener(object : ImageRequest.Listener {
|
||||||
override fun onError(request: ImageRequest, throwable: ErrorResult) {
|
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||||
Logd("CoverLoader", "Trying to get fallback image")
|
Logd("CoverLoader", "Trying to get fallback image")
|
||||||
val fallbackImageRequest = ImageRequest.Builder(activity)
|
val fallbackImageRequest = ImageRequest.Builder(activity)
|
||||||
.data(fallbackUri)
|
.data(fallbackUri)
|
||||||
|
@ -99,13 +99,13 @@ class CoverLoader(private val activity: MainActivity) {
|
||||||
override fun onStart(placeholder: Drawable?) {
|
override fun onStart(placeholder: Drawable?) {
|
||||||
|
|
||||||
}
|
}
|
||||||
override fun onError(errorDrawable: Drawable?) {
|
override fun onError(error: Drawable?) {
|
||||||
setTitleVisibility(fallbackTitle.get(), true)
|
setTitleVisibility(fallbackTitle.get(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSuccess(resource: Drawable) {
|
override fun onSuccess(result: Drawable) {
|
||||||
val ivCover = cover.get()
|
val ivCover = cover.get()
|
||||||
ivCover!!.setImageDrawable(resource)
|
ivCover!!.setImageDrawable(result)
|
||||||
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
|
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||||
import ac.mdiq.podcini.ui.activity.starter.PlaybackSpeedActivityStarter
|
import ac.mdiq.podcini.ui.activity.starter.PlaybackSpeedActivityStarter
|
||||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||||
import ac.mdiq.podcini.util.Converter.getDurationStringLong
|
import ac.mdiq.podcini.util.Converter.getDurationStringLong
|
||||||
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||||
import android.appwidget.AppWidgetManager
|
import android.appwidget.AppWidgetManager
|
||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
|
@ -45,11 +46,12 @@ object WidgetUpdater {
|
||||||
* Update the widgets with the given parameters. Must be called in a background thread.
|
* Update the widgets with the given parameters. Must be called in a background thread.
|
||||||
*/
|
*/
|
||||||
fun updateWidget(context: Context, widgetState: WidgetState?) {
|
fun updateWidget(context: Context, widgetState: WidgetState?) {
|
||||||
if (!isEnabled(context) || widgetState == null) return
|
if (!isEnabled() || widgetState == null) return
|
||||||
|
Logd(TAG, "in updateWidget")
|
||||||
|
|
||||||
val startMediaPlayer = if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO)
|
val startMediaPlayer =
|
||||||
VideoPlayerActivityStarter(context).pendingIntent
|
if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO) VideoPlayerActivityStarter(context).pendingIntent
|
||||||
else MainActivityStarter(context).withOpenPlayer().pendingIntent
|
else MainActivityStarter(context).withOpenPlayer().pendingIntent
|
||||||
|
|
||||||
val startPlaybackSpeedDialog = PlaybackSpeedActivityStarter(context).pendingIntent
|
val startPlaybackSpeedDialog = PlaybackSpeedActivityStarter(context).pendingIntent
|
||||||
val views = RemoteViews(context.packageName, R.layout.player_widget)
|
val views = RemoteViews(context.packageName, R.layout.player_widget)
|
||||||
|
@ -61,26 +63,9 @@ object WidgetUpdater {
|
||||||
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
|
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
|
||||||
views.setOnClickPendingIntent(R.id.butPlaybackSpeed, startPlaybackSpeedDialog)
|
views.setOnClickPendingIntent(R.id.butPlaybackSpeed, startPlaybackSpeedDialog)
|
||||||
|
|
||||||
val radius = context.resources.getDimensionPixelSize(R.dimen.widget_inner_radius)
|
|
||||||
// val options = RequestOptions()
|
|
||||||
// .dontAnimate()
|
|
||||||
// .transform(FitCenter(), RoundedCorners(radius))
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val imgLoc = widgetState.media.getImageLocation()
|
val imgLoc = widgetState.media.getImageLocation()
|
||||||
val imgLoc1 = getFallbackImageLocation(widgetState.media)
|
val imgLoc1 = getFallbackImageLocation(widgetState.media)
|
||||||
// icon = Glide.with(context)
|
|
||||||
// .asBitmap()
|
|
||||||
// .load(imgLoc)
|
|
||||||
// .error(Glide.with(context)
|
|
||||||
// .asBitmap()
|
|
||||||
// .load(imgLoc1)
|
|
||||||
// .apply(options)
|
|
||||||
// .submit(iconSize, iconSize)[500, TimeUnit.MILLISECONDS])
|
|
||||||
// .apply(options)
|
|
||||||
// .submit(iconSize, iconSize)
|
|
||||||
// .get(500, TimeUnit.MILLISECONDS)
|
|
||||||
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = ImageRequest.Builder(context)
|
||||||
.data(imgLoc)
|
.data(imgLoc)
|
||||||
|
@ -162,6 +147,7 @@ object WidgetUpdater {
|
||||||
val widgetIds = manager.getAppWidgetIds(playerWidget)
|
val widgetIds = manager.getAppWidgetIds(playerWidget)
|
||||||
|
|
||||||
for (id in widgetIds) {
|
for (id in widgetIds) {
|
||||||
|
Logd(TAG, "updating widget $id")
|
||||||
val options = manager.getAppWidgetOptions(id)
|
val options = manager.getAppWidgetOptions(id)
|
||||||
// val prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
// val prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
|
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
package ac.mdiq.podcini.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.BatteryManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Tom on 1/5/15.
|
|
||||||
*/
|
|
||||||
object PowerUtils {
|
|
||||||
/**
|
|
||||||
* @return true if the device is charging
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun deviceCharging(context: Context): Boolean {
|
|
||||||
// from http://developer.android.com/training/monitoring-device-state/battery-monitoring.html
|
|
||||||
val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
|
||||||
val batteryStatus = context.registerReceiver(null, iFilter)
|
|
||||||
|
|
||||||
val status = batteryStatus!!.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
|
||||||
return (status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package ac.mdiq.podcini.util.config
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import ac.mdiq.podcini.PodciniApp
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationCallbacksImpl : ApplicationCallbacks {
|
|
||||||
override fun getApplicationInstance(): Application {
|
|
||||||
return PodciniApp.getInstance()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
package ac.mdiq.podcini.util.error
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Thrown if a feed has invalid attribute values.
|
|
||||||
*/
|
|
||||||
class InvalidFeedException(message: String?) : Exception(message) {
|
|
||||||
companion object {
|
|
||||||
private const val serialVersionUID = 1L
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -5,12 +5,14 @@ import ac.mdiq.podcini.net.download.DownloadStatus
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.FeedPreferences
|
import ac.mdiq.podcini.storage.model.FeedPreferences
|
||||||
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||||
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
@ -26,7 +28,7 @@ import kotlin.math.max
|
||||||
sealed class FlowEvent {
|
sealed class FlowEvent {
|
||||||
val TAG = this::class.simpleName ?: "FlowEvent"
|
val TAG = this::class.simpleName ?: "FlowEvent"
|
||||||
|
|
||||||
data class PlaybackPositionEvent(val position: Int, val duration: Int) : FlowEvent()
|
data class PlaybackPositionEvent(val media: Playable?, val position: Int, val duration: Int) : FlowEvent()
|
||||||
|
|
||||||
data class PlaybackServiceEvent(val action: Action) : FlowEvent() {
|
data class PlaybackServiceEvent(val action: Action) : FlowEvent() {
|
||||||
enum class Action { SERVICE_STARTED, SERVICE_SHUT_DOWN, }
|
enum class Action { SERVICE_STARTED, SERVICE_SHUT_DOWN, }
|
||||||
|
@ -124,34 +126,25 @@ sealed class FlowEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeedListUpdateEvent(val feedIds: List<Long> = emptyList()) : FlowEvent() {
|
data class FeedListEvent(val action: Action, val feedIds: List<Long> = emptyList()) : FlowEvent() {
|
||||||
constructor(feed: Feed) : this(listOf(feed.id))
|
enum class Action { ADDED, REMOVED, ERROR, UNKNOWN }
|
||||||
constructor(feedId: Long) : this(listOf(feedId))
|
|
||||||
constructor(feeds: List<Feed>, junk: String = "") : this(feeds.map { it.id })
|
constructor(action: Action, feedId: Long) : this(action, listOf(feedId))
|
||||||
|
|
||||||
fun contains(feed: Feed): Boolean {
|
fun contains(feed: Feed): Boolean {
|
||||||
return feedIds.contains(feed.id)
|
return feedIds.contains(feed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||||
|
|
||||||
// data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent()
|
// data class SkipIntroEndingChangedEvent(val skipIntro: Int, val skipEnding: Int, val feedId: Long) : FlowEvent()
|
||||||
|
|
||||||
data class VolumeAdaptionChangedEvent(val volumeAdaptionSetting: VolumeAdaptionSetting, val feedId: Long) : FlowEvent()
|
// handled together in FeedPrefsChangeEvent
|
||||||
|
// data class VolumeAdaptionChangedEvent(val volumeAdaptionSetting: VolumeAdaptionSetting, val feedId: Long) : FlowEvent()
|
||||||
|
data class FeedPrefsChangeEvent(val feed: Feed) : FlowEvent()
|
||||||
|
|
||||||
// TODO: consider merging the two
|
|
||||||
data class SpeedChangedEvent(val newSpeed: Float) : FlowEvent()
|
data class SpeedChangedEvent(val newSpeed: Float) : FlowEvent()
|
||||||
data class FeedPrefsChangeEvent(val prefs: FeedPreferences) : FlowEvent()
|
|
||||||
|
|
||||||
data class EpisodesFilterOrSortEvent(val action: Action, val feed: Feed) : FlowEvent() {
|
|
||||||
enum class Action { FILTER_CHANGED, SORT_ORDER_CHANGED }
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE)
|
|
||||||
.append("action", action)
|
|
||||||
.append("feedId", feed.id)
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DownloadLogEvent(val dummy: Unit = Unit) : FlowEvent()
|
data class DownloadLogEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||||
|
|
||||||
|
@ -160,8 +153,6 @@ sealed class FlowEvent {
|
||||||
get() = map.keys
|
get() = map.keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// data class NewEpisodeDownloadEvent(val url: String) : FlowEvent() {}
|
|
||||||
|
|
||||||
// TODO: need better handling at receving end
|
// TODO: need better handling at receving end
|
||||||
data class EpisodePlayedEvent(val episode: Episode? = null) : FlowEvent()
|
data class EpisodePlayedEvent(val episode: Episode? = null) : FlowEvent()
|
||||||
|
|
||||||
|
@ -180,8 +171,6 @@ sealed class FlowEvent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class FeedsSortedEvent(val dummy: Unit = Unit) : FlowEvent()
|
|
||||||
|
|
||||||
data class FeedTagsChangedEvent(val dummy: Unit = Unit) : FlowEvent()
|
data class FeedTagsChangedEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||||
|
|
||||||
data class FeedUpdateRunningEvent(val isFeedUpdateRunning: Boolean) : FlowEvent()
|
data class FeedUpdateRunningEvent(val isFeedUpdateRunning: Boolean) : FlowEvent()
|
||||||
|
@ -195,12 +184,9 @@ sealed class FlowEvent {
|
||||||
data class SyncServiceEvent(val messageResId: Int, val message: String = "") : FlowEvent()
|
data class SyncServiceEvent(val messageResId: Int, val message: String = "") : FlowEvent()
|
||||||
|
|
||||||
data class DiscoveryDefaultUpdateEvent(val dummy: Unit = Unit) : FlowEvent()
|
data class DiscoveryDefaultUpdateEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||||
|
|
||||||
data class DiscoveryCompletedEvent(val dummy: Unit = Unit) : FlowEvent()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object EventFlow {
|
object EventFlow {
|
||||||
val collectorCount = MutableStateFlow(0)
|
|
||||||
val events: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 0)
|
val events: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 0)
|
||||||
val stickyEvents: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 1)
|
val stickyEvents: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 1)
|
||||||
val keyEvents: MutableSharedFlow<KeyEvent> = MutableSharedFlow(replay = 0)
|
val keyEvents: MutableSharedFlow<KeyEvent> = MutableSharedFlow(replay = 0)
|
||||||
|
@ -211,7 +197,7 @@ object EventFlow {
|
||||||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted: $event")
|
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted: $event")
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
events.emit(event)
|
events.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -222,7 +208,7 @@ object EventFlow {
|
||||||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted sticky: $event")
|
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted sticky: $event")
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
stickyEvents.emit(event)
|
stickyEvents.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -233,7 +219,7 @@ object EventFlow {
|
||||||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted key: $event")
|
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted key: $event")
|
||||||
}
|
}
|
||||||
GlobalScope.launch(Dispatchers.Default) {
|
CoroutineScope(Dispatchers.Default).launch {
|
||||||
keyEvents.emit(event)
|
keyEvents.emit(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
package ac.mdiq.podcini.util.sorting
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares the pubDate of two FeedItems for sorting.
|
|
||||||
*/
|
|
||||||
class EpisodePubdateComparator : Comparator<Episode> {
|
|
||||||
/**
|
|
||||||
* Returns a new instance of this comparator in reverse order.
|
|
||||||
*/
|
|
||||||
override fun compare(lhs: Episode, rhs: Episode): Int {
|
|
||||||
return when {
|
|
||||||
rhs.pubDate == null && lhs.pubDate == null -> 0
|
|
||||||
rhs.pubDate == null -> 1
|
|
||||||
lhs.pubDate == null -> -1
|
|
||||||
else -> rhs.pubDate.compareTo(lhs.pubDate) ?: -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,64 +19,38 @@ object EpisodesPermutors {
|
||||||
var permutor: Permutor<Episode>? = null
|
var permutor: Permutor<Episode>? = null
|
||||||
|
|
||||||
when (sortOrder) {
|
when (sortOrder) {
|
||||||
SortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
SortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f1).compareTo(itemTitle(f2)) }
|
||||||
itemTitle(f1).compareTo(itemTitle(f2))
|
SortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemTitle(f2).compareTo(itemTitle(f1)) }
|
||||||
}
|
SortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f1).compareTo(pubDate(f2)) }
|
||||||
SortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
SortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(f1)) }
|
||||||
itemTitle(f2).compareTo(itemTitle(f1))
|
SortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f1).compareTo(duration(f2)) }
|
||||||
}
|
SortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? -> duration(f2).compareTo(duration(f1)) }
|
||||||
SortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
SortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f1).compareTo(itemLink(f2)) }
|
||||||
pubDate(f1).compareTo(pubDate(f2))
|
SortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> itemLink(f2).compareTo(itemLink(f1)) }
|
||||||
}
|
SortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f1).compareTo(playDate(f2)) }
|
||||||
SortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
SortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> playDate(f2).compareTo(playDate(f1)) }
|
||||||
pubDate(f2).compareTo(pubDate(f1))
|
SortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f1).compareTo(completeDate(f2)) }
|
||||||
}
|
SortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
|
||||||
SortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
duration(f1).compareTo(duration(f2))
|
|
||||||
}
|
|
||||||
SortOrder.DURATION_LONG_SHORT -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
duration(f2).compareTo(duration(f1))
|
|
||||||
}
|
|
||||||
SortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
itemLink(f1).compareTo(itemLink(f2))
|
|
||||||
}
|
|
||||||
SortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
itemLink(f2).compareTo(itemLink(f1))
|
|
||||||
}
|
|
||||||
SortOrder.PLAYED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
playDate(f1).compareTo(playDate(f2))
|
|
||||||
}
|
|
||||||
SortOrder.PLAYED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
playDate(f2).compareTo(playDate(f1))
|
|
||||||
}
|
|
||||||
SortOrder.COMPLETED_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
completeDate(f1).compareTo(completeDate(f2))
|
|
||||||
}
|
|
||||||
SortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
completeDate(f2).compareTo(completeDate(f1))
|
|
||||||
}
|
|
||||||
|
|
||||||
SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
|
||||||
feedTitle(f1).compareTo(feedTitle(f2))
|
SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
|
||||||
}
|
|
||||||
SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
feedTitle(f2).compareTo(feedTitle(f1))
|
|
||||||
}
|
|
||||||
SortOrder.RANDOM -> permutor = object : Permutor<Episode> {
|
SortOrder.RANDOM -> permutor = object : Permutor<Episode> {
|
||||||
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) queue.shuffle()}
|
override fun reorder(queue: MutableList<Episode>?) {
|
||||||
|
if (!queue.isNullOrEmpty()) queue.shuffle()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
|
SortOrder.SMART_SHUFFLE_OLD_NEW -> permutor = object : Permutor<Episode> {
|
||||||
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true) }
|
override fun reorder(queue: MutableList<Episode>?) {
|
||||||
|
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
SortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
|
SortOrder.SMART_SHUFFLE_NEW_OLD -> permutor = object : Permutor<Episode> {
|
||||||
override fun reorder(queue: MutableList<Episode>?) {if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false) }
|
override fun reorder(queue: MutableList<Episode>?) {
|
||||||
}
|
if (!queue.isNullOrEmpty()) smartShuffle(queue as MutableList<Episode?>, false)
|
||||||
SortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
}
|
||||||
size(f1).compareTo(size(f2))
|
|
||||||
}
|
|
||||||
SortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
|
||||||
size(f2).compareTo(size(f1))
|
|
||||||
}
|
}
|
||||||
|
SortOrder.SIZE_SMALL_LARGE -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f1).compareTo(size(f2)) }
|
||||||
|
SortOrder.SIZE_LARGE_SMALL -> comparator = Comparator { f1: Episode?, f2: Episode? -> size(f2).compareTo(size(f1)) }
|
||||||
}
|
}
|
||||||
if (comparator != null) {
|
if (comparator != null) {
|
||||||
val comparator2: Comparator<Episode> = comparator
|
val comparator2: Comparator<Episode> = comparator
|
||||||
|
@ -87,7 +61,6 @@ object EpisodesPermutors {
|
||||||
return permutor!!
|
return permutor!!
|
||||||
}
|
}
|
||||||
|
|
||||||
// Null-safe accessors
|
|
||||||
private fun pubDate(item: Episode?): Date {
|
private fun pubDate(item: Episode?): Date {
|
||||||
return if (item == null) Date() else Date(item.pubDate)
|
return if (item == null) Date() else Date(item.pubDate)
|
||||||
}
|
}
|
||||||
|
@ -154,9 +127,9 @@ object EpisodesPermutors {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort each individual list by PubDate (ascending/descending)
|
// Sort each individual list by PubDate (ascending/descending)
|
||||||
val itemComparator: Comparator<Episode> = if (ascending)
|
val itemComparator: Comparator<Episode> =
|
||||||
Comparator { f1: Episode, f2: Episode -> f1.pubDate?.compareTo(f2.pubDate)?:-1 }
|
if (ascending) Comparator { f1: Episode, f2: Episode -> f1.pubDate?.compareTo(f2.pubDate)?:-1 }
|
||||||
else Comparator { f1: Episode, f2: Episode -> f2.pubDate?.compareTo(f1.pubDate)?:-1 }
|
else Comparator { f1: Episode, f2: Episode -> f2.pubDate?.compareTo(f1.pubDate)?:-1 }
|
||||||
|
|
||||||
val feeds: MutableList<List<Episode>> = ArrayList()
|
val feeds: MutableList<List<Episode>> = ArrayList()
|
||||||
for ((_, value) in map) {
|
for ((_, value) in map) {
|
||||||
|
@ -190,4 +163,18 @@ object EpisodesPermutors {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for passing around list permutor method. This is used for cases where a simple comparator
|
||||||
|
* won't work (e.g. Random, Smart Shuffle, etc).
|
||||||
|
*
|
||||||
|
* @param <E> the type of elements in the list
|
||||||
|
</E> */
|
||||||
|
interface Permutor<E> {
|
||||||
|
/**
|
||||||
|
* Reorders the specified list.
|
||||||
|
* @param queue A (modifiable) list of elements to be reordered
|
||||||
|
*/
|
||||||
|
fun reorder(queue: MutableList<E>?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue