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
|
||||
|
||||
[o:podcini:p:podcini:r:core-values]
|
||||
file_filter = ui/i18n/src/main/res/values-<lang>/strings.xml
|
||||
source_file = ui/i18n/src/main/res/values/strings.xml
|
||||
file_filter = app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = app/src/main/res/values/strings.xml
|
||||
source_lang = en
|
||||
trans.ar = ui/i18n/src/main/res/values-ar/strings.xml
|
||||
trans.ast_ES = ui/i18n/src/main/res/values-ast/strings.xml
|
||||
trans.br = ui/i18n/src/main/res/values-br/strings.xml
|
||||
trans.ca = ui/i18n/src/main/res/values-ca/strings.xml
|
||||
trans.cs_CZ = ui/i18n/src/main/res/values-cs/strings.xml
|
||||
trans.da = ui/i18n/src/main/res/values-da/strings.xml
|
||||
trans.de = ui/i18n/src/main/res/values-de/strings.xml
|
||||
trans.es = ui/i18n/src/main/res/values-es/strings.xml
|
||||
trans.et = ui/i18n/src/main/res/values-et/strings.xml
|
||||
trans.eu = ui/i18n/src/main/res/values-eu/strings.xml
|
||||
trans.fa = ui/i18n/src/main/res/values-fa/strings.xml
|
||||
trans.fi = ui/i18n/src/main/res/values-fi/strings.xml
|
||||
trans.fr = ui/i18n/src/main/res/values-fr/strings.xml
|
||||
trans.gl = ui/i18n/src/main/res/values-gl/strings.xml
|
||||
trans.he_IL = ui/i18n/src/main/res/values-iw/strings.xml
|
||||
trans.hi_IN = ui/i18n/src/main/res/values-hi/strings.xml
|
||||
trans.hu = ui/i18n/src/main/res/values-hu/strings.xml
|
||||
trans.id = ui/i18n/src/main/res/values-in/strings.xml
|
||||
trans.it_IT = ui/i18n/src/main/res/values-it/strings.xml
|
||||
trans.ja = ui/i18n/src/main/res/values-ja/strings.xml
|
||||
trans.ko = ui/i18n/src/main/res/values-ko/strings.xml
|
||||
trans.lt = ui/i18n/src/main/res/values-lt/strings.xml
|
||||
trans.nb_NO = ui/i18n/src/main/res/values-nb/strings.xml
|
||||
trans.nl = ui/i18n/src/main/res/values-nl/strings.xml
|
||||
trans.pl_PL = ui/i18n/src/main/res/values-pl/strings.xml
|
||||
trans.pt = ui/i18n/src/main/res/values-pt/strings.xml
|
||||
trans.pt_BR = ui/i18n/src/main/res/values-pt-rBR/strings.xml
|
||||
trans.ro_RO = ui/i18n/src/main/res/values-ro/strings.xml
|
||||
trans.ru_RU = ui/i18n/src/main/res/values-ru/strings.xml
|
||||
trans.sk = ui/i18n/src/main/res/values-sk/strings.xml
|
||||
trans.sl_SI = ui/i18n/src/main/res/values-sl/strings.xml
|
||||
trans.sv_SE = ui/i18n/src/main/res/values-sv/strings.xml
|
||||
trans.tr = ui/i18n/src/main/res/values-tr/strings.xml
|
||||
trans.uk_UA = ui/i18n/src/main/res/values-uk/strings.xml
|
||||
trans.zh_CN = ui/i18n/src/main/res/values-zh-rCN/strings.xml
|
||||
trans.zh_HK = ui/i18n/src/main/res/values-zh-rHK/strings.xml
|
||||
trans.zh_TW = ui/i18n/src/main/res/values-zh-rTW/strings.xml
|
||||
trans.ar = app/src/main/res/values-ar/strings.xml
|
||||
trans.ast_ES = app/src/main/res/values-ast/strings.xml
|
||||
trans.br = app/src/main/res/values-br/strings.xml
|
||||
trans.ca = app/src/main/res/values-ca/strings.xml
|
||||
trans.cs_CZ = app/src/main/res/values-cs/strings.xml
|
||||
trans.da = app/src/main/res/values-da/strings.xml
|
||||
trans.de = app/src/main/res/values-de/strings.xml
|
||||
trans.es = app/src/main/res/values-es/strings.xml
|
||||
trans.et = app/src/main/res/values-et/strings.xml
|
||||
trans.eu = app/src/main/res/values-eu/strings.xml
|
||||
trans.fa = app/src/main/res/values-fa/strings.xml
|
||||
trans.fi = app/src/main/res/values-fi/strings.xml
|
||||
trans.fr = app/src/main/res/values-fr/strings.xml
|
||||
trans.gl = app/src/main/res/values-gl/strings.xml
|
||||
trans.he_IL = app/src/main/res/values-iw/strings.xml
|
||||
trans.hi_IN = app/src/main/res/values-hi/strings.xml
|
||||
trans.hu = app/src/main/res/values-hu/strings.xml
|
||||
trans.id = app/src/main/res/values-in/strings.xml
|
||||
trans.it_IT = app/src/main/res/values-it/strings.xml
|
||||
trans.ja = app/src/main/res/values-ja/strings.xml
|
||||
trans.ko = app/src/main/res/values-ko/strings.xml
|
||||
trans.lt = app/src/main/res/values-lt/strings.xml
|
||||
trans.nb_NO = app/src/main/res/values-nb/strings.xml
|
||||
trans.nl = app/src/main/res/values-nl/strings.xml
|
||||
trans.pl_PL = app/src/main/res/values-pl/strings.xml
|
||||
trans.pt = app/src/main/res/values-pt/strings.xml
|
||||
trans.pt_BR = app/src/main/res/values-pt-rBR/strings.xml
|
||||
trans.ro_RO = app/src/main/res/values-ro/strings.xml
|
||||
trans.ru_RU = app/src/main/res/values-ru/strings.xml
|
||||
trans.sk = app/src/main/res/values-sk/strings.xml
|
||||
trans.sl_SI = app/src/main/res/values-sl/strings.xml
|
||||
trans.sv_SE = app/src/main/res/values-sv/strings.xml
|
||||
trans.tr = app/src/main/res/values-tr/strings.xml
|
||||
trans.uk_UA = app/src/main/res/values-uk/strings.xml
|
||||
trans.zh_CN = app/src/main/res/values-zh-rCN/strings.xml
|
||||
trans.zh_HK = app/src/main/res/values-zh-rHK/strings.xml
|
||||
trans.zh_TW = app/src/main/res/values-zh-rTW/strings.xml
|
||||
|
||||
[o:podcini:p:podcini:r:description]
|
||||
file_filter = app/src/main/play/listings/<lang>/full-description.txt
|
||||
|
|
|
@ -24,7 +24,7 @@ How to submit a feature request
|
|||
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
|
||||
---------------------
|
||||
|
|
|
@ -125,8 +125,8 @@ android {
|
|||
buildConfig true
|
||||
}
|
||||
defaultConfig {
|
||||
versionCode 3020202
|
||||
versionName "6.0.2"
|
||||
versionCode 3020203
|
||||
versionName "6.0.3"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -2,8 +2,8 @@ package de.test.podcini.service.playback
|
|||
|
||||
import ac.mdiq.podcini.storage.utils.MediaType
|
||||
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.MediaPlayerCallback
|
||||
|
||||
class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback {
|
||||
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.model.Playable
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerCallback
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerCallback
|
||||
|
||||
open class DefaultMediaPlayerCallback : MediaPlayerCallback {
|
||||
override fun statusChanged(newInfo: MediaPlayerInfo?) {
|
||||
|
|
|
@ -100,7 +100,7 @@ class MediaPlayerBaseTest {
|
|||
}
|
||||
|
||||
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,
|
||||
VolumeAdaptionSetting.OFF, null, null)
|
||||
f.preferences = prefs
|
||||
|
|
|
@ -62,7 +62,7 @@ class TaskManagerTest {
|
|||
|
||||
private fun writeTestQueue(pref: String): List<Episode>? {
|
||||
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()
|
||||
for (i in 0 until NUM_ITEMS) {
|
||||
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.preferences.UserPreferences.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.downloadAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.downloadAlgorithm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
|
@ -47,7 +47,7 @@ class AutoDownloadTest {
|
|||
@Throws(Exception::class)
|
||||
fun tearDown() {
|
||||
// setDownloadAlgorithm(Episodes.AutomaticDownloadAlgorithm())
|
||||
downloadAlgorithm = Episodes.AutomaticDownloadAlgorithm()
|
||||
downloadAlgorithm = AutoDownloads.AutoDownloadAlgorithm()
|
||||
EspressoTestUtils.tryKillPlaybackService()
|
||||
stubFeedsServer!!.tearDown()
|
||||
}
|
||||
|
@ -103,11 +103,11 @@ class AutoDownloadTest {
|
|||
// .until { item.media!!.id == currentlyPlayingFeedMediaId }
|
||||
}
|
||||
|
||||
private class StubDownloadAlgorithm : Episodes.AutomaticDownloadAlgorithm() {
|
||||
private class StubDownloadAlgorithm : AutoDownloads.AutoDownloadAlgorithm() {
|
||||
var currentlyPlayingAtDownload: Long = -1
|
||||
private set
|
||||
|
||||
override fun autoDownloadEpisodeMedia(context: Context): Runnable? {
|
||||
override fun autoDownloadEpisodeMedia(context: Context): Runnable {
|
||||
return Runnable {
|
||||
if (currentlyPlayingAtDownload == -1L) {
|
||||
// currentlyPlayingAtDownload = currentlyPlayingFeedMediaId
|
||||
|
|
|
@ -12,11 +12,11 @@ import androidx.test.filters.LargeTest
|
|||
import androidx.test.rule.ActivityTestRule
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APNullCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.APQueueCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.build
|
||||
import ac.mdiq.podcini.storage.algorithms.EpisodeCleanupAlgorithmFactory.ExceptFavoriteCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APNullCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.APQueueCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.build
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoCleanups.ExceptFavoriteCleanupAlgorithm
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
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) {
|
||||
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/feed/src/$i", false)
|
||||
"http://example.com/feed/src/$i")
|
||||
|
||||
// create items
|
||||
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
|
||||
* been called yet.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @param downloadEpisodes true if episodes should also be marked as downloaded.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
|
@ -161,13 +158,10 @@ class UITestUtils(private val context: Context) {
|
|||
// might be a flaky test, this is actually not that severe
|
||||
return
|
||||
}
|
||||
if (!feedDataHosted) {
|
||||
addHostedFeedData()
|
||||
}
|
||||
if (!feedDataHosted) addHostedFeedData()
|
||||
|
||||
val queue: MutableList<Episode> = ArrayList()
|
||||
for (feed in hostedFeeds) {
|
||||
feed.downloaded = (true)
|
||||
if (downloadEpisodes) {
|
||||
for (item in feed.episodes) {
|
||||
if (item.media != null) {
|
||||
|
@ -191,7 +185,7 @@ class UITestUtils(private val context: Context) {
|
|||
// adapter.setCompleteFeed(*hostedFeeds.toTypedArray<Feed>())
|
||||
// adapter.setQueue(queue)
|
||||
// adapter.close()
|
||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(hostedFeeds))
|
||||
// EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.UNKNOWN, hostedFeeds))
|
||||
EventFlow.postEvent(FlowEvent.QueueEvent.setQueue(queue))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.playback.cast
|
|||
|
||||
import android.content.Context
|
||||
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
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:backupAgent=".storage.backup.OpmlBackupAgent"
|
||||
android:backupAgent=".preferences.OpmlBackupAgent"
|
||||
android:restoreAnyVersion="true"
|
||||
android:theme="@style/Theme.Podcini.Splash"
|
||||
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.ui.activity.SplashActivity
|
||||
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.ClientConfigurator
|
||||
import ac.mdiq.podcini.util.error.CrashReportWriter
|
||||
|
@ -49,6 +49,12 @@ class PodciniApp : Application() {
|
|||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
|
||||
class ApplicationCallbacksImpl : ApplicationCallbacks {
|
||||
override fun getApplicationInstance(): Application {
|
||||
return PodciniApp.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
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.isNetworkRestricted
|
||||
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
|
||||
|
||||
@UnstableApi
|
||||
|
|
|
@ -152,7 +152,7 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
feed.downloadUrl != null -> prepareUrl(feed.downloadUrl!!)
|
||||
else -> null
|
||||
}
|
||||
this.title = feed.getHumanReadableIdentifier()
|
||||
this.title = feed.getTextIdentifier()
|
||||
this.feedfileId = feed.id
|
||||
this.feedfileType = feed.getTypeAsInt()
|
||||
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.isVpnOverWifi
|
||||
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.Feeds
|
||||
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.ui.utils.NotificationUtils
|
||||
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.FlowEvent
|
||||
import android.Manifest
|
||||
|
@ -129,14 +129,11 @@ object FeedUpdateManager {
|
|||
|
||||
@OptIn(UnstableApi::class)
|
||||
class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
|
||||
// private val newEpisodesNotification = NewEpisodesNotification()
|
||||
private val notificationManager = NotificationManagerCompat.from(context)
|
||||
|
||||
@UnstableApi
|
||||
override fun doWork(): Result {
|
||||
ClientConfigurator.initialize(applicationContext)
|
||||
// newEpisodesNotification.loadCountersBeforeRefresh()
|
||||
|
||||
val toUpdate: MutableList<Feed>
|
||||
val feedId = inputData.getLong(EXTRA_FEED_ID, -1L)
|
||||
var allAreLocal = true
|
||||
|
@ -158,7 +155,6 @@ object FeedUpdateManager {
|
|||
toUpdate.add(feed) // Needs to be updatable, so no singletonList
|
||||
force = true
|
||||
}
|
||||
|
||||
if (!inputData.getBoolean(EXTRA_EVEN_ON_MOBILE, false) && !allAreLocal) {
|
||||
if (!networkAvailable() || !isFeedRefreshAllowed) {
|
||||
Logd(TAG, "Blocking automatic update")
|
||||
|
@ -166,12 +162,10 @@ object FeedUpdateManager {
|
|||
}
|
||||
}
|
||||
refreshFeeds(toUpdate, force)
|
||||
|
||||
notificationManager.cancel(R.id.notification_updating_feeds)
|
||||
Episodes.autodownloadEpisodeMedia(applicationContext)
|
||||
autodownloadEpisodeMedia(applicationContext)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun createNotification(toUpdate: List<Feed?>?): Notification {
|
||||
val context = applicationContext
|
||||
var contentText = ""
|
||||
|
@ -190,11 +184,9 @@ object FeedUpdateManager {
|
|||
.addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
|
||||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun refreshFeeds(toUpdate: MutableList<Feed>, force: Boolean) {
|
||||
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()
|
||||
return
|
||||
}
|
||||
|
||||
while (toUpdate.isNotEmpty()) {
|
||||
if (isStopped) return
|
||||
|
||||
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
||||
val feed = unmanagedCopy(toUpdate[0])
|
||||
try {
|
||||
|
@ -230,18 +220,15 @@ object FeedUpdateManager {
|
|||
toUpdate.removeAt(0)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
@Throws(Exception::class)
|
||||
fun refreshFeed(feed: Feed, force: Boolean) {
|
||||
val nextPage = (inputData.getBoolean(EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
|
||||
if (nextPage) feed.pageNr += 1
|
||||
|
||||
val builder = create(feed)
|
||||
builder.setForce(force || feed.lastUpdateFailed)
|
||||
if (nextPage) builder.source = feed.nextPageLink
|
||||
val request = builder.build()
|
||||
|
||||
val downloader = DefaultDownloaderFactory().create(request) ?: throw Exception("Unable to create downloader")
|
||||
downloader.call()
|
||||
if (!downloader.result.isSuccessful) {
|
||||
|
@ -251,24 +238,18 @@ object FeedUpdateManager {
|
|||
LogsAndStats.addDownloadStatus(downloader.result)
|
||||
return
|
||||
}
|
||||
|
||||
val feedSyncTask = FeedSyncTask(applicationContext, request)
|
||||
val success = feedSyncTask.run()
|
||||
|
||||
if (!success) {
|
||||
Logd(TAG, "update failed: unsuccessful")
|
||||
Feeds.persistFeedLastUpdateFailed(feed, true)
|
||||
LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||
return
|
||||
}
|
||||
|
||||
if (request.feedfileId == null) return // No download logs for new subscriptions
|
||||
|
||||
// we create a 'successful' download log if the feed's last refresh failed
|
||||
val log = LogsAndStats.getFeedDownloadLog(request.feedfileId)
|
||||
if (log.isNotEmpty() && !log[0].isSuccessful) LogsAndStats.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||
|
||||
// newEpisodesNotification.showIfNeeded(applicationContext, feedSyncTask.savedFeed!!)
|
||||
if (!request.source.isNullOrEmpty()) {
|
||||
when {
|
||||
!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")
|
||||
}
|
||||
override fun call(): FeedHandlerResult? {
|
||||
Logd(TAG, "in call()")
|
||||
Logd(TAG, "in FeedParserTask call()")
|
||||
val feed = Feed(request.source, request.lastModified)
|
||||
feed.fileUrl = request.destination
|
||||
feed.id = request.feedfileId
|
||||
feed.downloaded = true
|
||||
if (feed.preferences == null) feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL,
|
||||
VolumeAdaptionSetting.OFF, request.username, request.password)
|
||||
|
||||
if (request.arguments != null) feed.pageNr = request.arguments.getInt(DownloadRequest.REQUEST_ARG_PAGE_NR, 0)
|
||||
|
||||
var reason: DownloadError? = null
|
||||
var reasonDetailed: String? = null
|
||||
val feedHandler = FeedHandler()
|
||||
|
||||
var result: FeedHandlerResult? = null
|
||||
try {
|
||||
result = feedHandler.parseFeed(feed)
|
||||
|
@ -344,31 +321,33 @@ object FeedUpdateManager {
|
|||
}
|
||||
}
|
||||
if (isSuccessful) {
|
||||
downloadStatus = DownloadResult(feed.id, feed.getHumanReadableIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
|
||||
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
|
||||
return result
|
||||
} else {
|
||||
downloadStatus = DownloadResult(feed.id, feed.getHumanReadableIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND,
|
||||
isSuccessful, reasonDetailed?:"")
|
||||
downloadStatus = DownloadResult(feed.id, feed.getTextIdentifier()?:"", reason?: DownloadError.ERROR_NOT_FOUND, isSuccessful, reasonDetailed?:"")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the feed was parsed correctly.
|
||||
*/
|
||||
@Throws(InvalidFeedException::class)
|
||||
private fun checkFeedData(feed: Feed) {
|
||||
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) {
|
||||
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 {
|
||||
private val TAG: String = FeedParserTask::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
|
@ -379,6 +358,10 @@ object FeedUpdateManager {
|
|||
private set
|
||||
private val task = FeedParserTask(request)
|
||||
private var feedHandlerResult: FeedHandlerResult? = null
|
||||
val downloadStatus: DownloadResult
|
||||
get() = task.downloadStatus
|
||||
val redirectUrl: String
|
||||
get() = feedHandlerResult?.redirectUrl?:""
|
||||
|
||||
fun run(): Boolean {
|
||||
feedHandlerResult = task.call()
|
||||
|
@ -386,12 +369,6 @@ object FeedUpdateManager {
|
|||
savedFeed = Feeds.updateFeed(context, feedHandlerResult!!.feed, false)
|
||||
return true
|
||||
}
|
||||
|
||||
val downloadStatus: DownloadResult
|
||||
get() = task.downloadStatus
|
||||
|
||||
val redirectUrl: String
|
||||
get() = feedHandlerResult?.redirectUrl?:""
|
||||
}
|
||||
|
||||
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 feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
||||
if (feedItem == null) {
|
||||
Log.i(TAG, "Unknown feed item: $action")
|
||||
Logd(TAG, "Unknown feed item: $action")
|
||||
return null
|
||||
}
|
||||
if (feedItem.media == null) {
|
||||
Log.i(TAG, "Feed item has no media: $action")
|
||||
Logd(TAG, "Feed item has no media: $action")
|
||||
return null
|
||||
}
|
||||
var idRemove: Long? = null
|
||||
|
|
|
@ -157,7 +157,7 @@ import kotlin.math.min
|
|||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "port $hostPort in use, ignored")
|
||||
Logd(TAG, "port $hostPort in use, ignored")
|
||||
loginFail = true
|
||||
}
|
||||
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 feedItem = getEpisodeByGuidOrUrl(guid, action.episode?:"")
|
||||
if (feedItem == null) {
|
||||
Log.i(TAG, "Unknown feed item: $action")
|
||||
Logd(TAG, "Unknown feed item: $action")
|
||||
return null
|
||||
}
|
||||
if (feedItem.media == null) {
|
||||
Log.i(TAG, "Feed item has no media: $action")
|
||||
Logd(TAG, "Feed item has no media: $action")
|
||||
return null
|
||||
}
|
||||
// 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.service.PlaybackService
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isRunning
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.storage.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.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
|
@ -39,6 +43,71 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
private var initialized = false
|
||||
private var eventsRegistered = false
|
||||
|
||||
private val mConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
if (service is LocalBinder) {
|
||||
playbackService = service.service
|
||||
onPlaybackServiceConnected()
|
||||
if (!released) {
|
||||
queryService()
|
||||
Logd(TAG, "Connection to Service established")
|
||||
} else Logd(TAG, "Connection to playback service has been established, but controller has already been released")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
playbackService = null
|
||||
initialized = false
|
||||
Logd(TAG, "Disconnected from Service")
|
||||
}
|
||||
}
|
||||
|
||||
private var prevStatus = PlayerStatus.STOPPED
|
||||
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
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
|
||||
fun init() {
|
||||
Logd(TAG, "controller init")
|
||||
|
@ -46,7 +115,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
procFlowEvents()
|
||||
eventsRegistered = true
|
||||
}
|
||||
if (PlaybackService.isRunning) initServiceRunning()
|
||||
if (isRunning) initServiceRunning()
|
||||
else updatePlayButtonShowsPlay(true)
|
||||
}
|
||||
|
||||
|
@ -133,76 +202,11 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
*/
|
||||
private fun bindToService() {
|
||||
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)
|
||||
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() {}
|
||||
|
||||
/**
|
||||
|
@ -258,7 +262,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
if (curMedia == null) return
|
||||
if (playbackService == null) {
|
||||
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 (playbackService == null) {
|
||||
PlaybackServiceStarter(activity, curMedia!!).start()
|
||||
Log.w(TAG, "playbackservice was null, restarted!")
|
||||
Logd(TAG, "playbackservice was null, restarted!")
|
||||
return
|
||||
}
|
||||
when (MediaPlayerBase.status) {
|
||||
PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
|
||||
PlayerStatus.PLAYING -> {
|
||||
playbackService?.mediaPlayer?.pause(true, reinit = false)
|
||||
playbackService?.mPlayer?.pause(true, reinit = false)
|
||||
playbackService?.isSpeedForward = false
|
||||
playbackService?.isFallbackSpeed = false
|
||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
||||
}
|
||||
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||
playbackService?.mediaPlayer?.resume()
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
// if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!))
|
||||
}
|
||||
PlayerStatus.PREPARING -> isStartWhenPrepared = !isStartWhenPrepared
|
||||
PlayerStatus.INITIALIZED -> {
|
||||
if (playbackService != null) isStartWhenPrepared = true
|
||||
playbackService?.mediaPlayer?.prepare()
|
||||
playbackService?.mPlayer?.prepare()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
}
|
||||
else -> {
|
||||
|
@ -300,35 +304,35 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
|
||||
var playbackService: PlaybackService? = null
|
||||
|
||||
val position: Int
|
||||
get() = playbackService?.currentPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
||||
val curPosition: Int
|
||||
get() = playbackService?.curPosition ?: curMedia?.getPosition() ?: Playable.INVALID_TIME
|
||||
|
||||
val duration: Int
|
||||
get() = playbackService?.duration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||
get() = playbackService?.curDuration ?: curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||
|
||||
val curSpeedMultiplier: Float
|
||||
get() = playbackService?.currentPlaybackSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
||||
get() = playbackService?.curSpeed ?: getCurrentPlaybackSpeed(curMedia)
|
||||
|
||||
val isPlayingVideoLocally: Boolean
|
||||
get() = when {
|
||||
PlaybackService.isCasting -> false
|
||||
isCasting -> false
|
||||
playbackService != null -> currentMediaType == MediaType.VIDEO
|
||||
else -> curMedia?.getMediaType() == MediaType.VIDEO
|
||||
}
|
||||
|
||||
private var isStartWhenPrepared: Boolean
|
||||
get() = playbackService?.mediaPlayer?.startWhenPrepared?.get() ?: false
|
||||
get() = playbackService?.mPlayer?.startWhenPrepared?.get() ?: false
|
||||
set(s) {
|
||||
playbackService?.mediaPlayer?.startWhenPrepared?.set(s)
|
||||
playbackService?.mPlayer?.startWhenPrepared?.set(s)
|
||||
}
|
||||
|
||||
private val mPlayerInfo: MediaPlayerInfo?
|
||||
get() = playbackService?.mediaPlayer?.playerInfo
|
||||
get() = playbackService?.mPlayer?.playerInfo
|
||||
|
||||
fun seekTo(time: Int) {
|
||||
if (playbackService != null) {
|
||||
playbackService!!.mediaPlayer?.seekTo(time)
|
||||
if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(time, duration))
|
||||
playbackService!!.mPlayer?.seekTo(time)
|
||||
// if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, time, duration))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,24 +341,24 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
when (MediaPlayerBase.status) {
|
||||
PlayerStatus.PLAYING -> {
|
||||
MediaPlayerBase.status = PlayerStatus.FALLBACK
|
||||
fallbackSpeed_(speed)
|
||||
setToFallback(speed)
|
||||
}
|
||||
PlayerStatus.FALLBACK -> {
|
||||
MediaPlayerBase.status = PlayerStatus.PLAYING
|
||||
fallbackSpeed_(speed)
|
||||
setToFallback(speed)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun fallbackSpeed_(speed: Float) {
|
||||
if (playbackService?.mediaPlayer == null || playbackService!!.isSpeedForward) return
|
||||
private fun setToFallback(speed: Float) {
|
||||
if (playbackService?.mPlayer == null || playbackService!!.isSpeedForward) return
|
||||
|
||||
if (!playbackService!!.isFallbackSpeed) {
|
||||
playbackService!!.normalSpeed = playbackService!!.mediaPlayer!!.getPlaybackSpeed()
|
||||
playbackService!!.mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||
} else playbackService!!.mediaPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||
playbackService!!.normalSpeed = playbackService!!.mPlayer!!.getPlaybackSpeed()
|
||||
playbackService!!.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||
} else playbackService!!.mPlayer!!.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||
|
||||
playbackService!!.isFallbackSpeed = !playbackService!!.isFallbackSpeed
|
||||
}
|
||||
|
@ -362,5 +366,28 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
fun sleepTimerActive(): Boolean {
|
||||
return playbackService?.taskManager?.isSleepTimerActive ?: false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an intent which starts an audio- or videoplayer, depending on the
|
||||
* type of media that is being played. If the playbackservice is not
|
||||
* running, the type of the last played media will be looked up.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPlayerActivityIntent(context: Context): Intent {
|
||||
val showVideoPlayer = if (isRunning) currentMediaType == MediaType.VIDEO && !isCasting
|
||||
else curState.curIsVideo
|
||||
return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent
|
||||
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as [.getPlayerActivityIntent], but here the type of activity
|
||||
* depends on the medaitype that is provided as an argument.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
|
||||
return if (mediaType == MediaType.VIDEO && !isCasting) VideoPlayerActivityStarter(context).intent
|
||||
else MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,10 +27,6 @@ object InTheatre {
|
|||
}
|
||||
|
||||
var curMedia: Playable? = null
|
||||
// get() {
|
||||
// if (field == null) field = loadPlayableFromPreferences()
|
||||
// return field
|
||||
// }
|
||||
set(value) {
|
||||
field = value
|
||||
if (field is EpisodeMedia) {
|
||||
|
|
|
@ -49,18 +49,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
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?) {
|
||||
if (playable != null && playable !== curMedia) {
|
||||
curMedia = playable
|
||||
|
@ -154,12 +142,15 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
|
||||
/**
|
||||
* 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) {
|
||||
val currentPosition = getPosition()
|
||||
if (currentPosition != Playable.INVALID_TIME) seekTo(currentPosition + d)
|
||||
else Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta")
|
||||
fun seekDelta(delta: Int) {
|
||||
val curPosition = getPosition()
|
||||
if (curPosition != Playable.INVALID_TIME) {
|
||||
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
|
||||
protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int = Playable.INVALID_TIME) {
|
||||
Logd(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
|
||||
|
||||
this.oldStatus = status
|
||||
status = newStatus
|
||||
if (newMedia != null) setPlayable(newMedia)
|
||||
|
||||
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
|
||||
when {
|
||||
oldStatus == PlayerStatus.PLAYING && newStatus != PlayerStatus.PLAYING -> callback.onPlaybackPause(newMedia, position)
|
||||
oldStatus != PlayerStatus.PLAYING && newStatus == PlayerStatus.PLAYING -> callback.onPlaybackStart(newMedia, position)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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?)
|
||||
|
||||
companion object {
|
||||
|
@ -354,7 +319,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
val newPosition = currentPosition - rewindTime.toInt()
|
||||
return max(newPosition.toDouble(), 0.0).toInt()
|
||||
} else return currentPosition
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -363,19 +327,11 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
@JvmStatic
|
||||
fun getCurrentPlaybackSpeed(media: Playable?): Float {
|
||||
var playbackSpeed = FeedPreferences.SPEED_USE_GLOBAL
|
||||
var mediaType: MediaType? = null
|
||||
val mediaType: MediaType? = media?.getMediaType()
|
||||
if (media != null) {
|
||||
mediaType = media.getMediaType()
|
||||
playbackSpeed = curState.curTempSpeed
|
||||
if (playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL && media is EpisodeMedia) {
|
||||
val item = media.episode
|
||||
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 (media.episode?.feed?.preferences != null) playbackSpeed = media.episode!!.feed!!.preferences!!.playSpeed
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
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),
|
||||
PREPARING(19),
|
||||
PAUSED(30),
|
||||
FALLBACK(35),
|
||||
PLAYING(40),
|
||||
INDETERMINATE(0), // player is currently changing its state, listeners should wait until the state is left
|
||||
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),
|
||||
SEEKING(29),
|
||||
INITIALIZING(9), // playback service is loading the Playable's metadata
|
||||
INITIALIZED(10); // playback service was started, data source of media player was set
|
||||
PAUSED(30),
|
||||
FALLBACK(35),
|
||||
PLAYING(40);
|
||||
|
||||
fun isAtLeast(other: PlayerStatus?): Boolean {
|
||||
return other == null || this.statusValue >= other.statusValue
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ac.mdiq.podcini.playback.service
|
||||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder
|
||||
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.curMedia
|
||||
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.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
|
@ -58,7 +58,6 @@ import java.util.concurrent.CountDownLatch
|
|||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
|
||||
/**
|
||||
* Manages the MediaPlayer object of the PlaybackService.
|
||||
*/
|
||||
|
@ -82,13 +81,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
|
||||
private val formats: List<Format>
|
||||
get() {
|
||||
val formats: MutableList<Format> = arrayListOf()
|
||||
val formats_: MutableList<Format> = arrayListOf()
|
||||
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList()
|
||||
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
|
||||
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
|
||||
|
@ -139,7 +138,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
private fun prepareWR() {
|
||||
Logd(TAG, "prepareWR() called")
|
||||
if (mediaSource == null && mediaItem == null) return
|
||||
|
||||
if (mediaSource != null) exoPlayer?.setMediaSource(mediaSource!!, false)
|
||||
else exoPlayer?.setMediaItem(mediaItem!!)
|
||||
exoPlayer?.prepare()
|
||||
|
@ -164,7 +162,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
exoPlayer?.setAudioAttributes(b.build(), true)
|
||||
}
|
||||
|
||||
private fun metadata(p: Playable): MediaMetadata {
|
||||
private fun buildMetadata(p: Playable): MediaMetadata {
|
||||
val builder = MediaMetadata.Builder()
|
||||
.setArtist(p.getFeedTitle())
|
||||
.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
|
||||
* not do anything.
|
||||
* Whether playback starts immediately depends on the given parameters. See below for more details.
|
||||
*
|
||||
* States:
|
||||
* 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
|
||||
* '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
|
||||
* will enter the ERROR state.
|
||||
*
|
||||
* 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 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
|
||||
|
@ -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.
|
||||
*/
|
||||
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 (!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.")
|
||||
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
|
||||
this.isStreaming = stream
|
||||
mediaType = curMedia!!.getMediaType()
|
||||
|
@ -263,7 +252,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
createMediaPlayer()
|
||||
this.startWhenPrepared.set(startWhenPrepared)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, curMedia)
|
||||
val metadata = metadata(curMedia!!)
|
||||
val metadata = buildMetadata(curMedia!!)
|
||||
try {
|
||||
callback.ensureMediaInfoLoaded(curMedia!!)
|
||||
callback.onMediaChanged(false)
|
||||
|
@ -313,7 +302,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
val newPosition = calculatePositionWithRewind(curMedia!!.getPosition(), curMedia!!.getLastPlayedTime())
|
||||
seekTo(newPosition)
|
||||
}
|
||||
// play()
|
||||
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepareWR()
|
||||
exoPlayer?.play()
|
||||
// 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())
|
||||
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, Action.END))
|
||||
if (isStreaming && reinit) reinit()
|
||||
} else {
|
||||
Logd(TAG, "Ignoring call to pause: Player is in $status state")
|
||||
}
|
||||
} else Logd(TAG, "Ignoring call to pause: Player is in $status state")
|
||||
}
|
||||
|
||||
override fun prepare() {
|
||||
|
@ -346,7 +332,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
if (curMedia != null) {
|
||||
val pos = curMedia!!.getPosition()
|
||||
if (pos > 0) seekTo(pos)
|
||||
if (curMedia!!.getDuration() <= 0) {
|
||||
if (curMedia != null && curMedia!!.getDuration() <= 0) {
|
||||
Logd(TAG, "Setting duration of media")
|
||||
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) {
|
||||
var t = t0
|
||||
override fun seekTo(t: Int) {
|
||||
var t = t
|
||||
if (t < 0) t = 0
|
||||
Logd(TAG, "seekTo() called")
|
||||
Logd(TAG, "seekTo() called $t")
|
||||
|
||||
if (t >= getDuration()) {
|
||||
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()
|
||||
endPlayback(true, wasSkipped = true, true, toStoppedState = true)
|
||||
t = getPosition()
|
||||
// return
|
||||
}
|
||||
|
||||
when (status) {
|
||||
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||
Logd(TAG, "seekTo() called $t")
|
||||
if (seekLatch != null && seekLatch!!.count > 0) {
|
||||
try {
|
||||
seekLatch!!.await(3, TimeUnit.SECONDS)
|
||||
|
@ -391,8 +380,9 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
}
|
||||
seekLatch = CountDownLatch(1)
|
||||
statusBeforeSeeking = status
|
||||
setPlayerStatus(PlayerStatus.SEEKING, curMedia, getPosition())
|
||||
setPlayerStatus(PlayerStatus.SEEKING, curMedia, t)
|
||||
exoPlayer?.seekTo(t.toLong())
|
||||
if (curMedia != null) EventFlow.postEvent(FlowEvent.PlaybackPositionEvent(curMedia, t, curMedia!!.getDuration()))
|
||||
audioSeekCompleteListener?.run()
|
||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) curMedia?.setPosition(t)
|
||||
try {
|
||||
|
@ -411,25 +401,13 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
}
|
||||
|
||||
override fun getDuration(): Int {
|
||||
var retVal = 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
|
||||
return curMedia?.getDuration() ?: Playable.INVALID_TIME
|
||||
}
|
||||
|
||||
override fun getPosition(): Int {
|
||||
var retVal = Playable.INVALID_TIME
|
||||
// Log.d(TAG, "getPosition() ${playable?.getIdentifier()} $status")
|
||||
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
||||
|
||||
if (retVal <= 0) {
|
||||
val playablePos = curMedia?.getPosition() ?: -1
|
||||
if (playablePos >= 0) retVal = playablePos
|
||||
}
|
||||
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
|
||||
return retVal
|
||||
}
|
||||
|
||||
|
@ -461,16 +439,14 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
volumeRight *= adaptionFactor
|
||||
}
|
||||
}
|
||||
|
||||
if (volumeLeft > 1) {
|
||||
exoPlayer!!.volume = 1f
|
||||
exoPlayer?.volume = 1f
|
||||
loudnessEnhancer?.setEnabled(true)
|
||||
loudnessEnhancer?.setTargetGain((1000 * (volumeLeft - 1)).toInt())
|
||||
} else {
|
||||
exoPlayer!!.volume = volumeLeft
|
||||
exoPlayer?.volume = volumeLeft
|
||||
loudnessEnhancer?.setEnabled(false)
|
||||
}
|
||||
|
||||
Logd(TAG, "Media player volume was set to $volumeLeft $volumeRight")
|
||||
}
|
||||
|
||||
|
@ -543,6 +519,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
}
|
||||
|
||||
override fun createMediaPlayer() {
|
||||
Logd(TAG, "createMediaPlayer()")
|
||||
release()
|
||||
if (curMedia == null) {
|
||||
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
|
||||
val position = getPosition()
|
||||
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
|
||||
var nextMedia: Playable? = null
|
||||
if (shouldContinue) {
|
||||
// Load next episode if previous episode was in the queue and if there
|
||||
// is an episode in the queue left.
|
||||
// Load next episode if previous episode was in the queue and if there is an episode in the queue left.
|
||||
// Start playback immediately if continuous playback is enabled
|
||||
nextMedia = callback.getNextInQueue(currentMedia)
|
||||
if (nextMedia != null) {
|
||||
Logd(TAG, "has nextMedia. call callback.onPlaybackEnded false")
|
||||
// curMedia = null
|
||||
if (wasSkipped) setPlayerStatus(PlayerStatus.INDETERMINATE, null)
|
||||
callback.onPlaybackEnded(nextMedia.getMediaType(), false)
|
||||
// setting media to null signals to playMediaObject() that
|
||||
// we're taking care of post-playback processing
|
||||
// setting media to null signals to playMediaObject that we're taking care of post-playback processing
|
||||
curMedia = null
|
||||
playMediaObject(nextMedia, !nextMedia.localFileAvailable(), isPlaying, isPlaying)
|
||||
}
|
||||
|
@ -585,7 +560,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
callback.onPlaybackEnded(null, true)
|
||||
curMedia = null
|
||||
exoPlayer?.stop()
|
||||
// stop()
|
||||
releaseWifiLockIfNecessary()
|
||||
if (status == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
|
||||
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_ENDED: Int = -2
|
||||
const val ERROR_CODE_OFFSET: Int = 1000
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun updateTile() {
|
||||
private fun updateTile() {
|
||||
val qsTile = qsTile
|
||||
if (qsTile == null) Logd(TAG, "Ignored call to update QS tile: getQsTile() returned null.")
|
||||
else {
|
||||
// val isPlaying = PlaybackService.isRunning && MediaPlayerBase.status == PlayerStatus.PLAYING
|
||||
val isPlaying = (PlaybackService.isRunning && curState.curPlayerStatus == PLAYER_STATUS_PLAYING)
|
||||
qsTile.state = if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
|
|
|
@ -351,7 +351,7 @@ class TaskManager(private val context: Context, private val callback: PSTMCallba
|
|||
/**
|
||||
* 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 UPDATE_INTERVAL = 1000L
|
||||
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.preferences.UserPreferences.isAutoBackupOPML
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.transport.OpmlReader
|
||||
import ac.mdiq.podcini.storage.transport.OpmlWriter
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.backup.BackupAgentHelper
|
||||
import android.app.backup.BackupDataInputStream
|
||||
|
@ -45,7 +45,7 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
|||
*/
|
||||
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")
|
||||
val byteStream = ByteArrayOutputStream()
|
||||
var digester: MessageDigest? = null
|
||||
|
@ -66,17 +66,14 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
|||
if (digester != null) {
|
||||
val newChecksum = digester.digest()
|
||||
Logd(TAG, "New checksum: " + BigInteger(1, newChecksum).toString(16))
|
||||
|
||||
// Get the old checksum
|
||||
if (oldState != null) {
|
||||
val inState = FileInputStream(oldState.fileDescriptor)
|
||||
val len = inState.read()
|
||||
|
||||
if (len != -1) {
|
||||
val oldChecksum = ByteArray(len)
|
||||
IOUtils.read(inState, oldChecksum, 0, len)
|
||||
Logd(TAG, "Old checksum: " + BigInteger(1, oldChecksum).toString(16))
|
||||
|
||||
if (oldChecksum.contentEquals(newChecksum)) {
|
||||
Logd(TAG, "Checksums are the same; won't backup")
|
||||
return
|
||||
|
@ -99,22 +96,18 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
|||
|
||||
@OptIn(UnstableApi::class) override fun restoreEntity(data: BackupDataInputStream) {
|
||||
Logd(TAG, "Backup restore")
|
||||
|
||||
if (OPML_ENTITY_KEY != data.key) {
|
||||
Logd(TAG, "Unknown entity key: " + data.key)
|
||||
return
|
||||
}
|
||||
|
||||
var digester: MessageDigest? = null
|
||||
var reader: Reader
|
||||
|
||||
try {
|
||||
digester = MessageDigest.getInstance("MD5")
|
||||
reader = InputStreamReader(DigestInputStream(data, digester), Charset.forName("UTF-8"))
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
reader = InputStreamReader(data, Charset.forName("UTF-8"))
|
||||
}
|
||||
|
||||
try {
|
||||
val opmlElements = OpmlReader().readDocument(reader)
|
||||
mChecksum = digester?.digest()?: byteArrayOf()
|
||||
|
@ -139,7 +132,6 @@ class OpmlBackupAgent : BackupAgentHelper() {
|
|||
|
||||
/**
|
||||
* Writes the new state description, which is the checksum of the OPML file.
|
||||
*
|
||||
* @param newState
|
||||
* @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_HIDDEN_DRAWER_ITEMS: String = "prefHiddenDrawerItems"
|
||||
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_EXPANDED_NOTIFICATION: String = "prefExpandNotify"
|
||||
private const val PREF_USE_EPISODE_COVER: String = "prefEpisodeCover"
|
||||
|
@ -239,6 +240,9 @@ object UserPreferences {
|
|||
.apply()
|
||||
}
|
||||
|
||||
val useGridLayout: Boolean
|
||||
get() = appPrefs.getBoolean(PREF_FEED_GRID_LAYOUT, false)
|
||||
|
||||
/**
|
||||
* @return `true` if episodes should use their own cover, `false` otherwise
|
||||
*/
|
||||
|
|
|
@ -1,20 +1,28 @@
|
|||
package ac.mdiq.podcini.preferences.fragments
|
||||
|
||||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
|
||||
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.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.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.transport.DatabaseTransporter
|
||||
import ac.mdiq.podcini.storage.transport.PreferencesTransporter
|
||||
import ac.mdiq.podcini.storage.transport.ExportWriter
|
||||
import ac.mdiq.podcini.storage.transport.EpisodeProgressReader
|
||||
import ac.mdiq.podcini.storage.transport.EpisodesProgressWriter
|
||||
import ac.mdiq.podcini.storage.transport.FavoritesWriter
|
||||
import ac.mdiq.podcini.storage.transport.HtmlWriter
|
||||
import ac.mdiq.podcini.storage.transport.OpmlWriter
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.*
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.storage.utils.SortOrder
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -23,16 +31,20 @@ import android.content.DialogInterface
|
|||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.ShareCompat.IntentBuilder
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
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.launch
|
||||
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.nio.channels.FileChannel
|
||||
import java.nio.charset.Charset
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
@ -87,10 +104,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.import_export_pref)
|
||||
}
|
||||
|
||||
// override fun onStop() {
|
||||
// super.onStop()
|
||||
// }
|
||||
|
||||
private fun dateStampFilename(fname: String): String {
|
||||
return String.format(fname, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()))
|
||||
}
|
||||
|
@ -136,7 +149,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
exportPreferences()
|
||||
true
|
||||
}
|
||||
|
||||
findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
|
||||
true
|
||||
|
@ -166,7 +178,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
try {
|
||||
val output = worker.exportFile()
|
||||
withContext(Dispatchers.Main) {
|
||||
showExportSuccessSnackbar(output?.uri, exportType.contentType)
|
||||
showExportSuccessSnackbar(output.uri, exportType.contentType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showExportErrorDialog(e)
|
||||
|
@ -412,7 +424,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
class DocumentFileExportWorker(private val exportWriter: ExportWriter, private val context: Context, private val outputFileUri: Uri) {
|
||||
|
||||
suspend fun exportFile(): DocumentFile {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val output = DocumentFile.fromSingleUri(context, outputFileUri)
|
||||
|
@ -429,20 +440,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
} catch (e: IOException) {
|
||||
throw e
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close()
|
||||
} catch (e: IOException) {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
if (writer != null) try { 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.
|
||||
*/
|
||||
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),
|
||||
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context)
|
||||
|
||||
suspend fun exportFile(): File? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
if (output.exists()) {
|
||||
val success = output.delete()
|
||||
Log.w(TAG, "Overwriting previously exported file: $success")
|
||||
Logd(TAG, "Overwriting previously exported file: $success")
|
||||
}
|
||||
|
||||
var writer: OutputStreamWriter? = null
|
||||
|
@ -476,7 +473,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPORT_DIR = "export/"
|
||||
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 {
|
||||
private val TAG: String = ImportExportPreferencesFragment::class.simpleName ?: "Anonymous"
|
||||
private const val PREF_OPML_EXPORT = "prefOpmlExport"
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
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.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.content.SharedPreferences
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
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
|
||||
|
||||
class PlayerWidget : AppWidgetProvider() {
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
getSharedPrefs(context)
|
||||
Logd(TAG, "Widget enabled")
|
||||
setEnabled(context, true)
|
||||
setEnabled(true)
|
||||
WidgetUpdaterWorker.enqueueWork(context)
|
||||
scheduleWorkaround(context)
|
||||
}
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
||||
getSharedPrefs(context)
|
||||
WidgetUpdaterWorker.enqueueWork(context)
|
||||
|
||||
if (!prefs!!.getBoolean(KEY_WORKAROUND_ENABLED, false)) {
|
||||
|
@ -36,7 +36,7 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
Logd(TAG, "Widget disabled")
|
||||
setEnabled(context, false)
|
||||
setEnabled(false)
|
||||
}
|
||||
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
|
@ -57,7 +57,7 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
super.onDeleted(context, appWidgetIds)
|
||||
}
|
||||
|
||||
private fun setEnabled(context: Context, enabled: Boolean) {
|
||||
private fun setEnabled(enabled: Boolean) {
|
||||
prefs!!.edit().putBoolean(KEY_ENABLED, enabled).apply()
|
||||
}
|
||||
|
||||
|
@ -91,8 +91,7 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isEnabled(context: Context): Boolean {
|
||||
// val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
fun isEnabled(): Boolean {
|
||||
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.net.download.serviceinterface.DownloadServiceInterface
|
||||
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
|
||||
|
||||
// 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.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
|
||||
fun build(): EpisodeCleanupAlgorithm {
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @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 {
|
||||
@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 ->
|
||||
val l = lhs.getPubDate()
|
||||
|
@ -56,9 +74,7 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
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
|
||||
}
|
||||
|
||||
val delete = if (candidates.size > numberOfEpisodesToDelete) candidates.subList(0, numberOfEpisodesToDelete) else candidates
|
||||
|
||||
val delete = if (candidates.size > numToRemove) candidates.subList(0, numToRemove) else candidates
|
||||
for (item in delete) {
|
||||
if (item.media == null) continue
|
||||
try {
|
||||
|
@ -69,23 +85,10 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
val cacheSize = episodeCacheSize
|
||||
if (cacheSize != UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED) {
|
||||
|
@ -94,7 +97,6 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = ExceptFavoriteCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
|
@ -105,47 +107,6 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
* but only if space is needed.
|
||||
*/
|
||||
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>
|
||||
get() {
|
||||
val candidates: MutableList<Episode> = ArrayList()
|
||||
|
@ -157,11 +118,40 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
}
|
||||
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 {
|
||||
return getNumEpisodesToCleanup(0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = APQueueCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
|
@ -171,20 +161,17 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
* A cleanup algorithm that never removes anything
|
||||
*/
|
||||
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
|
||||
Log.i(TAG, "performCleanup: Not removing anything")
|
||||
return 0
|
||||
}
|
||||
|
||||
public override fun getDefaultCleanupParameter(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getReclaimableItems(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
companion object {
|
||||
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. */
|
||||
|
||||
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>
|
||||
get() {
|
||||
val candidates: MutableList<Episode> = ArrayList()
|
||||
|
@ -249,27 +193,54 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
for (item in downloadedItems) {
|
||||
if (item.media != null && item.media!!.downloaded && !idsInQueues.contains(item.id) && item.isPlayed() && !item.isFavorite) {
|
||||
val media = item.media
|
||||
// make sure this candidate was played at least the proper amount of days prior
|
||||
// to now
|
||||
if (media?.playbackCompletionDate != null && media.playbackCompletionDate!!.before(mostRecentDateForDeletion))
|
||||
candidates.add(item)
|
||||
// make sure this candidate was played at least the proper amount of days prior to now
|
||||
if (media?.playbackCompletionDate != null && media.playbackCompletionDate!!.before(mostRecentDateForDeletion)) candidates.add(item)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return getNumEpisodesToCleanup(0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = APCleanupAlgorithm::class.simpleName ?: "Anonymous"
|
||||
private fun minusHours(baseDate: Date, numberOfHours: Int): Date {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.time = baseDate
|
||||
|
||||
cal.add(Calendar.HOUR_OF_DAY, -1 * numberOfHours)
|
||||
|
||||
return cal.time
|
||||
}
|
||||
}
|
||||
|
@ -286,22 +257,17 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
* @return The number of episodes that were deleted.
|
||||
*/
|
||||
protected abstract fun performCleanup(context: Context, numToRemove: Int): Int
|
||||
|
||||
fun performCleanup(context: Context): Int {
|
||||
return performCleanup(context, getDefaultCleanupParameter())
|
||||
}
|
||||
|
||||
protected abstract fun getDefaultCleanupParameter(): Int
|
||||
/**
|
||||
* 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
|
||||
* method should not have any effects.
|
||||
*/
|
||||
|
||||
|
||||
protected abstract fun getDefaultCleanupParameter(): Int
|
||||
/**
|
||||
* Cleans up just enough episodes to make room for the requested number
|
||||
*
|
||||
* @param context Can be used for accessing the database
|
||||
* @param amountOfRoomNeeded the number of episodes we need space for
|
||||
* @return The number of epiosdes that were deleted
|
||||
|
@ -309,12 +275,10 @@ object EpisodeCleanupAlgorithmFactory {
|
|||
fun makeRoomForEpisodes(context: Context, amountOfRoomNeeded: Int): Int {
|
||||
return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded))
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of episodes/items that *could* be cleaned up, if needed
|
||||
*/
|
||||
abstract fun getReclaimableItems(): Int
|
||||
|
||||
/**
|
||||
* @param amountOfRoomNeeded the number of episodes we want to download
|
||||
* @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.sync.model.EpisodeAction
|
||||
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.curState
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.writeNoMediaPlaying
|
||||
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.storage.algorithms.EpisodeCleanupAlgorithmFactory
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromAllQueues
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueueSync
|
||||
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.util.IntentUtils.sendLocalBroadcast
|
||||
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.FlowEvent
|
||||
import ac.mdiq.podcini.util.sorting.EpisodesPermutors.getPermutor
|
||||
|
@ -43,28 +35,12 @@ import androidx.documentfile.provider.DocumentFile
|
|||
import androidx.media3.common.util.UnstableApi
|
||||
import kotlinx.coroutines.Job
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
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
|
||||
|
||||
object Episodes {
|
||||
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 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@OptIn(UnstableApi::class) @JvmStatic
|
||||
fun deleteMediaOfEpisode(context: Context, episode: Episode) : Job {
|
||||
|
@ -228,7 +109,7 @@ object Episodes {
|
|||
private fun deleteMediaSync(context: Context, episode: Episode): Boolean {
|
||||
Logd(TAG, "deleteMediaSync called")
|
||||
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
|
||||
val url = media.fileUrl
|
||||
when {
|
||||
|
@ -312,17 +193,7 @@ object Episodes {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (removedFromQueue.isNotEmpty()) {
|
||||
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()
|
||||
if (removedFromQueue.isNotEmpty()) removeFromAllQueues(*removedFromQueue.toTypedArray())
|
||||
|
||||
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
|
||||
* 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.
|
||||
*
|
||||
* @param episode Episode that should be added to the playback history.
|
||||
* @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.queue.SynchronizationQueueSink
|
||||
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.LogsAndStats.addDownloadStatus
|
||||
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.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||
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.*
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction
|
||||
import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.TAG_ROOT
|
||||
import ac.mdiq.podcini.storage.utils.VolumeAdaptionSetting
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.event.EventFlow
|
||||
import ac.mdiq.podcini.util.event.FlowEvent
|
||||
import ac.mdiq.podcini.util.sorting.EpisodePubdateComparator
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import io.realm.kotlin.ext.asFlow
|
||||
import io.realm.kotlin.notifications.*
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.math.abs
|
||||
|
||||
object Feeds {
|
||||
private val TAG: String = Feeds::class.simpleName ?: "Anonymous"
|
||||
// internal val feeds: MutableList<Feed> = mutableListOf()
|
||||
private val feedMap: MutableMap<Long, Feed> = mutableMapOf()
|
||||
private val tags: MutableList<String> = mutableListOf()
|
||||
|
||||
|
@ -47,11 +44,22 @@ object Feeds {
|
|||
return tags
|
||||
}
|
||||
|
||||
fun updateFeedMap() {
|
||||
Logd(TAG, "updateFeedMap called")
|
||||
val feeds_ = realm.query(Feed::class).find()
|
||||
feedMap.clear()
|
||||
feedMap.putAll(feeds_.associateBy { it.id })
|
||||
fun updateFeedMap(feeds: List<Feed> = listOf(), wipe: Boolean = false) {
|
||||
Logd(TAG, "updateFeedMap called feeds: ${feeds.size} wipe: $wipe")
|
||||
when {
|
||||
feeds.isEmpty() -> {
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -59,21 +67,87 @@ object Feeds {
|
|||
val tagsSet = mutableSetOf<String>()
|
||||
val feedsCopy = feedMap.values
|
||||
for (feed in feedsCopy) {
|
||||
if (feed.preferences != null) {
|
||||
for (tag in feed.preferences!!.tags) {
|
||||
if (tag != TAG_ROOT) tagsSet.add(tag)
|
||||
}
|
||||
}
|
||||
if (feed.preferences != null) tagsSet.addAll(feed.preferences!!.tags.filter { it != TAG_ROOT })
|
||||
}
|
||||
tags.clear()
|
||||
tags.addAll(tagsSet)
|
||||
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> {
|
||||
Logd(TAG, "getFeedListDownloadUrls called")
|
||||
val result: MutableList<String> = mutableListOf()
|
||||
// val feeds = realm.query(Feed::class).find()
|
||||
for (f in feedMap.values) {
|
||||
val url = f.downloadUrl
|
||||
if (url != null && !url.startsWith(Feed.PREFIX_LOCAL_FOLDER)) result.add(url)
|
||||
|
@ -81,11 +155,7 @@ object Feeds {
|
|||
return result
|
||||
}
|
||||
|
||||
// TODO: some callers don't need to copy
|
||||
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]
|
||||
return if (f != null) {
|
||||
if (copy) realm.copyFromRealm(f)
|
||||
|
@ -118,14 +188,13 @@ object Feeds {
|
|||
@Synchronized
|
||||
fun updateFeed(context: Context, newFeed: Feed, removeUnlistedItems: Boolean): Feed? {
|
||||
Logd(TAG, "updateFeed called")
|
||||
// TODO: check further on enclosing in realm write block
|
||||
var resultFeed: Feed?
|
||||
val unlistedItems: MutableList<Episode> = ArrayList()
|
||||
|
||||
// Look up feed in the feedslist
|
||||
val savedFeed = searchFeedByIdentifyingValueOrID(newFeed, true)
|
||||
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}")
|
||||
resultFeed = newFeed
|
||||
} else {
|
||||
|
@ -217,7 +286,6 @@ object Feeds {
|
|||
Logd(TAG, "Marking episode published on $pubDate new, prior most recent date = $priorMostRecentDate")
|
||||
episode.setNew()
|
||||
}
|
||||
// idLong += 1
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,16 +314,16 @@ object Feeds {
|
|||
// Update with default values that are set in database
|
||||
resultFeed = searchFeedByIdentifyingValueOrID(newFeed)
|
||||
} else persistFeedsSync(savedFeed)
|
||||
updateFeedMap()
|
||||
// updateFeedMap()
|
||||
if (removeUnlistedItems) runBlocking { deleteEpisodes(context, unlistedItems).join() }
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: ExecutionException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (savedFeed != null) EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(savedFeed))
|
||||
else EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(emptyList<Long>()))
|
||||
// TODO: feedMonitor likely takes care of this
|
||||
// if (savedFeed != null) EventFlow.postEvent(FlowEvent.FeedListEvent(savedFeed))
|
||||
// else EventFlow.postEvent(FlowEvent.FeedListEvent(emptyList<Long>()))
|
||||
|
||||
return resultFeed
|
||||
}
|
||||
|
@ -302,7 +370,7 @@ object Feeds {
|
|||
return runOnIOScope {
|
||||
feed.lastUpdateFailed = lastUpdateFailed
|
||||
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)
|
||||
}
|
||||
// updateFeedMap(feeds.toList())
|
||||
}
|
||||
for (feed in feeds) {
|
||||
if (!feed.isLocalFeed && feed.downloadUrl != null)
|
||||
SynchronizationQueueSink.enqueueFeedAddedIfSyncActive(context, feed.downloadUrl!!)
|
||||
if (!feed.isLocalFeed && feed.downloadUrl != null) SynchronizationQueueSink.enqueueFeedAddedIfSyncActive(context, feed.downloadUrl!!)
|
||||
}
|
||||
val backupManager = BackupManager(context)
|
||||
backupManager.dataChanged()
|
||||
}
|
||||
|
||||
private fun persistFeedsSync(vararg feeds: Feed) {
|
||||
Logd(TAG, "persistCompleteFeeds called")
|
||||
Logd(TAG, "persistFeedsSync called")
|
||||
for (feed in feeds) {
|
||||
upsertBlk(feed) {}
|
||||
}
|
||||
}
|
||||
|
||||
fun persistFeedPreferences(feed: Feed) : Job {
|
||||
Logd(TAG, "persistCompleteFeeds called")
|
||||
Logd(TAG, "persistFeedPreferences called")
|
||||
return runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
realm.write {
|
||||
findLatest(feed_)?.let { it.preferences = feed.preferences }
|
||||
findLatest(feed_)?.let {
|
||||
it.preferences = feed.preferences
|
||||
// updateFeedMap(listOf(it))
|
||||
}
|
||||
}
|
||||
} else upsert(feed) {}
|
||||
if (feed.preferences != null) EventFlow.postEvent(FlowEvent.FeedPrefsChangeEvent(feed.preferences!!))
|
||||
} else {
|
||||
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()
|
||||
if (episodes.isNotEmpty()) episodes.forEach { e -> delete(e) }
|
||||
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 (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
|
||||
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")
|
||||
|
||||
val medias = realm.query(EpisodeMedia::class).find()
|
||||
val groupdMedias = medias.groupBy {
|
||||
it.episode?.feedId ?: 0L
|
||||
}
|
||||
val groupdMedias = medias.groupBy { it.episode?.feedId ?: 0L }
|
||||
val result = StatisticsResult()
|
||||
result.oldestDate = Long.MAX_VALUE
|
||||
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.isQueueKeepSorted
|
||||
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.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
|
@ -186,7 +186,7 @@ object Queues {
|
|||
queue.episodes.addAll(qItems)
|
||||
}
|
||||
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?
|
||||
if (queue.id == curQueue.id && context != null) autodownloadEpisodeMedia(context)
|
||||
|
|
|
@ -16,9 +16,12 @@ import kotlinx.coroutines.*
|
|||
import kotlin.coroutines.ContinuationInterceptor
|
||||
|
||||
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
|
||||
|
||||
init {
|
||||
|
@ -33,7 +36,7 @@ object RealmDB {
|
|||
DownloadResult::class,
|
||||
Chapter::class))
|
||||
.name("Podcini.realm")
|
||||
.schemaVersion(3)
|
||||
.schemaVersion(SCHEMA_VERSION_NUMBER)
|
||||
.build()
|
||||
realm = Realm.open(config)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
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.realmSetOf
|
||||
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 org.apache.commons.lang3.builder.ToStringBuilder
|
||||
import org.apache.commons.lang3.builder.ToStringStyle
|
||||
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
@ -54,7 +52,6 @@ class Episode : RealmObject {
|
|||
@Ignore
|
||||
var feed: Feed? = null
|
||||
get() {
|
||||
// Logd(TAG, "feed.get() ${field == null} ${title}")
|
||||
if (field == null && feedId != null) field = getFeed(feedId!!)
|
||||
return field
|
||||
}
|
||||
|
@ -138,26 +135,6 @@ class Episode : RealmObject {
|
|||
// 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.
|
||||
*/
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.utils.MediaType
|
||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ChapterReader
|
||||
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.showStackTrace
|
||||
import android.content.Context
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
@ -116,14 +116,14 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
this.downloaded = downloaded
|
||||
}
|
||||
|
||||
constructor(id: Long, item: Episode?, duration: Int, position: Int,
|
||||
size: Long, mime_type: String?, file_url: String?, download_url: String?,
|
||||
downloaded: Boolean, playbackCompletionDate: Date?, played_duration: Int,
|
||||
hasEmbeddedPicture: Boolean?, lastPlayedTime: Long)
|
||||
: this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, playbackCompletionDate, played_duration, lastPlayedTime) {
|
||||
|
||||
this.hasEmbeddedPicture = hasEmbeddedPicture
|
||||
}
|
||||
// constructor(id: Long, item: Episode?, duration: Int, position: Int,
|
||||
// size: Long, mime_type: String?, file_url: String?, download_url: String?,
|
||||
// downloaded: Boolean, playbackCompletionDate: Date?, played_duration: Int,
|
||||
// hasEmbeddedPicture: Boolean?, lastPlayedTime: Long)
|
||||
// : this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, playbackCompletionDate, played_duration, lastPlayedTime) {
|
||||
//
|
||||
// this.hasEmbeddedPicture = hasEmbeddedPicture
|
||||
// }
|
||||
|
||||
fun getHumanReadableIdentifier(): String? {
|
||||
return if (episode?.title != null) episode!!.title else downloadUrl
|
||||
|
@ -165,16 +165,16 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
return duration
|
||||
}
|
||||
|
||||
override fun setDuration(duration: Int) {
|
||||
this.duration = duration
|
||||
override fun setDuration(newDuration: Int) {
|
||||
this.duration = newDuration
|
||||
}
|
||||
override fun getPosition(): Int {
|
||||
return position
|
||||
}
|
||||
|
||||
override fun setPosition(position: Int) {
|
||||
this.position = position
|
||||
if (position > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false)
|
||||
override fun setPosition(newPosition: Int) {
|
||||
this.position = newPosition
|
||||
if (newPosition > 0 && episode != null && episode!!.isNew) episode!!.setPlayed(false)
|
||||
}
|
||||
|
||||
override fun getLastPlayedTime(): Long {
|
||||
|
@ -247,8 +247,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
// }
|
||||
|
||||
override fun getEpisodeTitle(): String {
|
||||
if (episode == null) return "No title"
|
||||
return if (episode!!.title != null) episode!!.title!! else episode!!.identifyingValue?:"No title"
|
||||
return episode?.title ?: episode?.identifyingValue ?: "No title"
|
||||
}
|
||||
|
||||
override fun getChapters(): List<Chapter> {
|
||||
|
@ -264,8 +263,7 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
}
|
||||
|
||||
override fun getFeedTitle(): String {
|
||||
if (episode == null) return ""
|
||||
return episode!!.feed?.title?:""
|
||||
return episode?.feed?.title?:""
|
||||
}
|
||||
|
||||
override fun getIdentifier(): Any {
|
||||
|
@ -368,6 +366,8 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = EpisodeMedia::class.simpleName ?: "Anonymous"
|
||||
|
||||
const val FEEDFILETYPE_FEEDMEDIA: Int = 2
|
||||
const val PLAYABLE_TYPE_FEEDMEDIA: Int = 1
|
||||
const val FILENAME_PREFIX_EMBEDDED_COVER: String = "metadata-retriever:"
|
||||
|
|
|
@ -29,7 +29,7 @@ class Feed : RealmObject {
|
|||
|
||||
var fileUrl: String? = null
|
||||
var downloadUrl: String? = null
|
||||
var downloaded: Boolean = false
|
||||
// var downloaded: Boolean = false
|
||||
|
||||
/**
|
||||
* 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?,
|
||||
description: String?, paymentLinks: String?, author: String?, language: String?,
|
||||
type: String?, feedIdentifier: String?, imageUrl: String?, fileUrl: String?,
|
||||
downloadUrl: String?, downloaded: Boolean, paged: Boolean, nextPageLink: String?,
|
||||
filter: String?, sortOrder: SortOrder?, lastUpdateFailed: Boolean) {
|
||||
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?) {
|
||||
this.id = id
|
||||
this.fileUrl = fileUrl
|
||||
this.downloadUrl = downloadUrl
|
||||
this.downloaded = downloaded
|
||||
this.eigenTitle = title
|
||||
this.customTitle = customTitle
|
||||
this.lastUpdate = lastUpdate
|
||||
this.link = link
|
||||
this.description = description
|
||||
this.paymentLinks = extractPaymentLinks(paymentLinks)
|
||||
this.paymentLinks = extractPaymentLinks(paymentLink)
|
||||
this.author = author
|
||||
this.language = language
|
||||
this.type = type
|
||||
this.identifier = feedIdentifier
|
||||
this.imageUrl = imageUrl
|
||||
this.isPaged = paged
|
||||
this.isPaged = false
|
||||
this.nextPageLink = nextPageLink
|
||||
// if (filter != null) this.episodeFilter = EpisodeFilter(filter)
|
||||
// else this.episodeFilter = EpisodeFilter()
|
||||
this.preferences?.filterString = filter ?: ""
|
||||
this.preferences?.filterString = ""
|
||||
this.sortOrder = sortOrder
|
||||
this.preferences?.sortOrderCode = sortOrder?.code ?: 0
|
||||
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.
|
||||
*/
|
||||
|
@ -228,7 +196,6 @@ class Feed : RealmObject {
|
|||
this.lastUpdate = lastUpdate
|
||||
fileUrl = null
|
||||
this.downloadUrl = url
|
||||
downloaded = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -247,7 +214,7 @@ class Feed : RealmObject {
|
|||
preferences = FeedPreferences(0, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password)
|
||||
}
|
||||
|
||||
fun getHumanReadableIdentifier(): String? {
|
||||
fun getTextIdentifier(): String? {
|
||||
return when {
|
||||
!customTitle.isNullOrEmpty() -> customTitle
|
||||
!eigenTitle.isNullOrEmpty() -> eigenTitle
|
||||
|
|
|
@ -22,9 +22,6 @@ class FeedPreferences(@Index var feedID: Long,
|
|||
@Ignore @JvmField var volumeAdaptionSetting: VolumeAdaptionSetting?,
|
||||
var username: String?,
|
||||
var password: String?,
|
||||
/**
|
||||
* @return the filter for this feed
|
||||
*/
|
||||
@Ignore @JvmField var filter: FeedEpisodesFilter,
|
||||
var playSpeed: Float,
|
||||
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 {
|
||||
openStream(playable, context).use { inVal ->
|
||||
val chapters = readId3ChaptersFrom(inVal)
|
||||
if (chapters.isNotEmpty()) {
|
||||
Log.i(TAG, "Chapters loaded")
|
||||
return chapters
|
||||
}
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Unable to load ID3 chapters: " + e.message)
|
||||
|
@ -81,10 +78,7 @@ object ChapterUtils {
|
|||
try {
|
||||
openStream(playable, context).use { inVal ->
|
||||
val chapters = readOggChaptersFromInputStream(inVal)
|
||||
if (chapters.isNotEmpty()) {
|
||||
Log.i(TAG, "Chapters loaded")
|
||||
return chapters
|
||||
}
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Unable to load vorbis chapters: " + e.message)
|
||||
|
@ -151,7 +145,7 @@ object ChapterUtils {
|
|||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||
enumerateEmptyChapterTitles(chapters)
|
||||
if (!chaptersValid(chapters)) {
|
||||
Log.i(TAG, "Chapter data was invalid")
|
||||
Logd(TAG, "Chapter data was invalid")
|
||||
return emptyList()
|
||||
}
|
||||
return chapters
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
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.PlaybackServiceStarter
|
||||
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.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
|
@ -40,7 +40,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
}
|
||||
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mediaPlayer?.resume()
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
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.PlaybackServiceStarter
|
||||
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.utils.MediaType
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -30,7 +30,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
}
|
||||
|
||||
if (playbackService?.isServiceReady() == true && InTheatre.isCurMedia(media)) {
|
||||
playbackService?.mediaPlayer?.resume()
|
||||
playbackService?.mPlayer?.resume()
|
||||
playbackService?.taskManager?.restartSleepTimer()
|
||||
} else {
|
||||
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.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.preferences.UsageStatistics.logAction
|
||||
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.RemoteMedia
|
||||
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.FlowEvent
|
||||
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.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
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.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
|
@ -95,7 +96,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
private lateinit var mainView: View
|
||||
private lateinit var navDrawerFragment: NavDrawerFragment
|
||||
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
||||
private lateinit var audioPlayerFragmentView: View
|
||||
private lateinit var audioPlayerView: View
|
||||
private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||
private lateinit var navDrawer: View
|
||||
private lateinit var dummyView : View
|
||||
|
@ -130,6 +131,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
SwipeActions.getSharedPrefs(this@MainActivity)
|
||||
QueueFragment.getSharedPrefs(this@MainActivity)
|
||||
updateFeedMap()
|
||||
monitorFeeds()
|
||||
// InTheatre.apply { }
|
||||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||
|
@ -196,11 +198,11 @@ class MainActivity : CastEnabledActivity() {
|
|||
transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG)
|
||||
transaction.commit()
|
||||
navDrawer = findViewById(R.id.navDrawerFragment)
|
||||
audioPlayerFragmentView = findViewById(R.id.audioplayerFragment)
|
||||
audioPlayerView = findViewById(R.id.audioplayerFragment)
|
||||
|
||||
runOnIOScope { checkFirstLaunch() }
|
||||
|
||||
this.bottomSheet = BottomSheetBehavior.from(audioPlayerFragmentView) as LockableBottomSheetBehavior<*>
|
||||
this.bottomSheet = BottomSheetBehavior.from(audioPlayerView) as LockableBottomSheetBehavior<*>
|
||||
this.bottomSheet.isHideable = false
|
||||
this.bottomSheet.isDraggable = false
|
||||
this.bottomSheet.setBottomSheetCallback(bottomSheetCallback)
|
||||
|
@ -382,7 +384,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||
|
||||
private fun updateInsets() {
|
||||
setPlayerVisible(audioPlayerFragmentView.visibility == View.VISIBLE)
|
||||
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
|
||||
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom
|
||||
}
|
||||
|
@ -403,7 +405,11 @@ class MainActivity : CastEnabledActivity() {
|
|||
val playerParams = playerView?.layoutParams as? MarginLayoutParams
|
||||
playerParams?.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
|
||||
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?) {
|
||||
|
@ -669,7 +675,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
val s: Snackbar
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
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)
|
||||
|
||||
s.show()
|
||||
|
|
|
@ -5,8 +5,8 @@ import ac.mdiq.podcini.databinding.OpmlSelectionBinding
|
|||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce
|
||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.transport.OpmlElement
|
||||
import ac.mdiq.podcini.storage.transport.OpmlReader
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlElement
|
||||
import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlReader
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.Manifest
|
||||
|
|
|
@ -5,12 +5,12 @@ import ac.mdiq.podcini.databinding.AudioControlsBinding
|
|||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||
import ac.mdiq.podcini.playback.PlaybackController
|
||||
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.seekTo
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
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.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||
|
@ -83,7 +83,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
finish()
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -434,7 +434,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
||||
butAudioTracks.setOnClickListener {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
@ -455,13 +455,13 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
|
||||
private val audioTracks: List<String>
|
||||
get() {
|
||||
val tracks = playbackService?.mediaPlayer?.getAudioTracks()
|
||||
val tracks = playbackService?.mPlayer?.getAudioTracks()
|
||||
if (tracks.isNullOrEmpty()) return emptyList()
|
||||
return tracks.filterNotNull().map { it }
|
||||
}
|
||||
|
||||
private val selectedAudioTrack: Int
|
||||
get() = playbackService?.mediaPlayer?.getSelectedAudioTrack() ?: -1
|
||||
get() = playbackService?.mPlayer?.getSelectedAudioTrack() ?: -1
|
||||
|
||||
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
||||
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.Companion.prefs
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
||||
class WidgetConfigActivity : AppCompatActivity() {
|
||||
|
||||
|
@ -63,9 +64,7 @@ class WidgetConfigActivity : AppCompatActivity() {
|
|||
val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||
widgetPreview.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {}
|
||||
})
|
||||
|
||||
|
@ -96,6 +95,8 @@ class WidgetConfigActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun setInitialState() {
|
||||
PlayerWidget.getSharedPrefs(this)
|
||||
|
||||
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||
ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, true)
|
||||
ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, true)
|
||||
|
@ -123,6 +124,7 @@ class WidgetConfigActivity : AppCompatActivity() {
|
|||
private fun confirmCreateWidget() {
|
||||
val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||
|
||||
Logd("WidgetConfigActivity", "confirmCreateWidget appWidgetId $appWidgetId")
|
||||
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||
val editor = prefs!!.edit()
|
||||
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.unmanagedCopy
|
||||
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.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
|
||||
|
@ -25,12 +26,27 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
|||
private val TAG: String = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
protected val activity: Activity?
|
||||
get() = mainActivityRef.get()
|
||||
|
||||
private var episodes: List<Episode> = ArrayList()
|
||||
private var feed: Feed? = null
|
||||
var longPressedItem: Episode? = null
|
||||
private var longPressedPosition: Int = 0 // used to init actionMode
|
||||
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 {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
@ -40,8 +56,9 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
|||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateItems(items: List<Episode>) {
|
||||
fun updateItems(items: List<Episode>, feed_: Feed? = null) {
|
||||
episodes = items
|
||||
feed = feed_
|
||||
notifyDataSetChanged()
|
||||
updateTitle()
|
||||
}
|
||||
|
@ -72,6 +89,7 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
|||
beforeBindViewHolder(holder, pos)
|
||||
|
||||
val item: Episode = unmanagedCopy(episodes[pos])
|
||||
if (feed != null) item.feed = feed
|
||||
holder.bind(item)
|
||||
|
||||
// holder.infoCard.setOnCreateContextMenuListener(this)
|
||||
|
@ -154,9 +172,6 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
|||
return item
|
||||
}
|
||||
|
||||
protected val activity: Activity?
|
||||
get() = mainActivityRef.get()
|
||||
|
||||
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = activity!!.menuInflater
|
||||
if (inActionMode()) {
|
||||
|
@ -188,16 +203,4 @@ open class EpisodesAdapter(mainActivity: MainActivity)
|
|||
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
|
||||
} 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) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
|
|
|
@ -22,13 +22,6 @@ class SimpleIconListAdapter<T : SimpleIconListAdapter.ListItem>(private val cont
|
|||
val binding = SimpleIconListItemBinding.bind(view!!)
|
||||
binding.title.text = item.title
|
||||
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)
|
||||
return view
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ object RemoveFeedDialog {
|
|||
for (feed in feeds) {
|
||||
deleteFeed(context, feed.id, false)
|
||||
}
|
||||
EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feeds))
|
||||
// EventFlow.postEvent(FlowEvent.FeedListEvent(FlowEvent.FeedListEvent.Action.REMOVED, feeds))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "Feed(s) deleted")
|
||||
|
|
|
@ -4,6 +4,8 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curEpisode
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curState
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.currentMediaType
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
|
@ -138,7 +140,7 @@ import java.util.*
|
|||
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
isSkipSilence = isChecked
|
||||
// setSkipSilence(isChecked)
|
||||
playbackService?.mediaPlayer?.setPlaybackParams(playbackService!!.currentPlaybackSpeed, isChecked)
|
||||
playbackService?.mPlayer?.setPlaybackParams(playbackService!!.curSpeed, isChecked)
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
@ -217,13 +219,13 @@ import java.util.*
|
|||
if (currentMediaType == MediaType.VIDEO) {
|
||||
curState.curTempSpeed = speed
|
||||
videoPlaybackSpeed = speed
|
||||
playbackService!!.mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||
} else {
|
||||
if (codeArray != null && codeArray.size == 3) {
|
||||
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
|
||||
if (codeArray[2]) UserPreferences.setPlaybackSpeed(speed)
|
||||
if (codeArray[1]) {
|
||||
val episode = (playbackService!!.playable as? EpisodeMedia)?.episode ?: playbackService!!.currentitem
|
||||
val episode = (curMedia as? EpisodeMedia)?.episode ?: curEpisode
|
||||
if (episode != null) {
|
||||
var feed = episode.feed
|
||||
if (feed != null) {
|
||||
|
@ -240,11 +242,11 @@ import java.util.*
|
|||
}
|
||||
if (codeArray[0]) {
|
||||
curState.curTempSpeed = speed
|
||||
playbackService!!.mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||
playbackService!!.mPlayer?.setPlaybackParams(speed, isSkipSilence)
|
||||
}
|
||||
} else {
|
||||
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> {
|
||||
allEpisodes = getEpisodes(0, Int.MAX_VALUE, getFilter(), allEpisodesSortOrder, false)
|
||||
Logd(TAG, "loadData() allEpisodes.size ${allEpisodes.size}")
|
||||
return allEpisodes.subList(0, page * EPISODES_PER_PAGE)
|
||||
// return getEpisodes(0, page * EPISODES_PER_PAGE, getFilter(), allEpisodesSortOrder)
|
||||
return allEpisodes.subList(0, min(allEpisodes.size-1, page * EPISODES_PER_PAGE))
|
||||
}
|
||||
|
||||
override fun loadMoreData(page: Int): List<Episode> {
|
||||
val offset = (page - 1) * EPISODES_PER_PAGE
|
||||
Logd(TAG, "loadMoreData() page: $page $offset ${allEpisodes.size}")
|
||||
if (offset >= allEpisodes.size) return listOf()
|
||||
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((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE)
|
||||
// return getEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, getFilter(), allEpisodesSortOrder)
|
||||
}
|
||||
|
||||
override fun loadTotalItemCount(): Int {
|
||||
|
@ -86,10 +80,6 @@ import kotlin.math.min
|
|||
return EpisodeFilter(prefFilterAllEpisodes)
|
||||
}
|
||||
|
||||
override fun getFragmentTag(): String {
|
||||
return TAG
|
||||
}
|
||||
|
||||
override fun getPrefName(): String {
|
||||
return PREF_NAME
|
||||
}
|
||||
|
@ -162,7 +152,7 @@ import kotlin.math.min
|
|||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
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.databinding.AudioplayerFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding
|
||||
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.duration
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlayingVideoLocally
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.playbackService
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.position
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.seekTo
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.loadPlayableFromPreferences
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
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.isSkipSilence
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
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.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.ui.actions.menuhandler.EpisodeMenuHandler
|
||||
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.view.ChapterSeekBar
|
||||
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.Logd
|
||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||
|
@ -52,7 +49,6 @@ import android.content.Intent
|
|||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import android.widget.TextView
|
||||
|
@ -93,23 +89,20 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
private var playerDetailsFragment: PlayerDetailsFragment? = null
|
||||
|
||||
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 playerView2: View? = null
|
||||
private var playerUI1: PlayerUIFragment? = 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 txtvSeek: TextView
|
||||
|
||||
private var controller: PlaybackController? = null
|
||||
private var seekedToChapterStart = false
|
||||
// private var currentChapterIndex = -1
|
||||
private var duration = 0
|
||||
|
||||
private var currentMedia: Playable? = null
|
||||
private var currentitem: Episode? = null
|
||||
|
||||
private var isShowPlay: Boolean = false
|
||||
var isCollapsed = true
|
||||
|
@ -136,25 +129,21 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
controller = createController()
|
||||
controller!!.init()
|
||||
|
||||
playerFragment1 = InternalPlayerFragment.newInstance(controller!!)
|
||||
playerUI1 = PlayerUIFragment.newInstance(controller!!)
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.playerFragment1, playerFragment1!!, "InternalPlayerFragment1")
|
||||
.replace(R.id.playerFragment1, playerUI1!!, "InternalPlayerFragment1")
|
||||
.commit()
|
||||
playerView1 = binding.root.findViewById(R.id.playerFragment1)
|
||||
playerView1?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
playerUIView1 = binding.root.findViewById(R.id.playerFragment1)
|
||||
playerUIView1?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
|
||||
playerFragment2 = InternalPlayerFragment.newInstance(controller!!)
|
||||
playerUI2 = PlayerUIFragment.newInstance(controller!!)
|
||||
childFragmentManager.beginTransaction()
|
||||
.replace(R.id.playerFragment2, playerFragment2!!, "InternalPlayerFragment2")
|
||||
.replace(R.id.playerFragment2, playerUI2!!, "InternalPlayerFragment2")
|
||||
.commit()
|
||||
playerView2 = binding.root.findViewById(R.id.playerFragment2)
|
||||
playerView2?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
|
||||
playerUIView2 = binding.root.findViewById(R.id.playerFragment2)
|
||||
playerUIView2?.setBackgroundColor(SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density))
|
||||
onCollaped()
|
||||
|
||||
cardViewSeek = binding.cardViewSeek
|
||||
txtvSeek = binding.txtvSeek
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
@ -184,18 +173,18 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
transaction.replace(R.id.itemDescription, playerDetailsFragment!!).commit()
|
||||
}
|
||||
isCollapsed = false
|
||||
playerFragment = playerFragment2
|
||||
playerFragment?.updateUi(currentMedia)
|
||||
playerFragment?.butPlay?.setIsShowPlay(isShowPlay)
|
||||
playerDetailsFragment?.load()
|
||||
playerUI = playerUI2
|
||||
playerUI?.updateUi(currentMedia)
|
||||
playerUI?.butPlay?.setIsShowPlay(isShowPlay)
|
||||
playerDetailsFragment?.updateInfo()
|
||||
}
|
||||
|
||||
fun onCollaped() {
|
||||
Logd(TAG, "onCollaped()")
|
||||
isCollapsed = true
|
||||
playerFragment = playerFragment1
|
||||
playerFragment?.updateUi(currentMedia)
|
||||
playerFragment?.butPlay?.setIsShowPlay(isShowPlay)
|
||||
playerUI = playerUI1
|
||||
playerUI?.updateUi(currentMedia)
|
||||
playerUI?.butPlay?.setIsShowPlay(isShowPlay)
|
||||
}
|
||||
|
||||
private fun setChapterDividers(media: Playable?) {
|
||||
|
@ -218,11 +207,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
// }
|
||||
|
||||
fun loadMediaInfo(includingChapters: Boolean) {
|
||||
val actMain = (activity as MainActivity)
|
||||
if (curMedia == null) {
|
||||
(activity as MainActivity).setPlayerVisible(false)
|
||||
if (actMain.isPlayerVisible()) actMain.setPlayerVisible(false)
|
||||
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())) {
|
||||
Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters")
|
||||
lifecycleScope.launch {
|
||||
|
@ -232,9 +225,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
}
|
||||
currentMedia = media
|
||||
if (currentMedia is EpisodeMedia) {
|
||||
val item = (currentMedia as EpisodeMedia).episode
|
||||
if (item != null) playerDetailsFragment?.setItem(item)
|
||||
}
|
||||
updateUi()
|
||||
playerFragment?.updateUi(currentMedia)
|
||||
if (!includingChapters) loadMediaInfo(true)
|
||||
playerUI?.updateUi(currentMedia)
|
||||
// TODO: disable for now
|
||||
// if (!includingChapters) loadMediaInfo(true)
|
||||
}.invokeOnCompletion { throwable ->
|
||||
if (throwable!= null) {
|
||||
Log.e(TAG, Log.getStackTraceString(throwable))
|
||||
|
@ -247,16 +245,16 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
return object : PlaybackController(requireActivity()) {
|
||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
||||
isShowPlay = showPlay
|
||||
playerFragment?.butPlay?.setIsShowPlay(showPlay)
|
||||
playerUI?.butPlay?.setIsShowPlay(showPlay)
|
||||
// playerFragment2?.butPlay?.setIsShowPlay(showPlay)
|
||||
}
|
||||
override fun loadMediaInfo() {
|
||||
this@AudioPlayerFragment.loadMediaInfo(false)
|
||||
if (!isCollapsed) playerDetailsFragment?.load()
|
||||
if (!isCollapsed) playerDetailsFragment?.updateInfo()
|
||||
}
|
||||
override fun onPlaybackEnd() {
|
||||
isShowPlay = true
|
||||
playerFragment?.butPlay?.setIsShowPlay(true)
|
||||
playerUI?.butPlay?.setIsShowPlay(true)
|
||||
// playerFragment2?.butPlay?.setIsShowPlay(true)
|
||||
(activity as MainActivity).setPlayerVisible(false)
|
||||
}
|
||||
|
@ -274,13 +272,20 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
retainInstance = true
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
Logd(TAG, "onResume() isCollapsed: $isCollapsed")
|
||||
super.onResume()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
Logd(TAG, "onStart() isCollapsed: $isCollapsed")
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
loadMediaInfo(false)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Logd(TAG, "onStop()")
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
// 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) {
|
||||
Logd(TAG, "onEvenStartPlay ${event.episode.title}")
|
||||
currentitem = event.episode
|
||||
if (currentMedia?.getIdentifier() == null || currentitem!!.media?.getIdentifier() != currentMedia?.getIdentifier())
|
||||
playerDetailsFragment?.setItem(currentitem!!)
|
||||
val currentitem = event.episode
|
||||
if (currentMedia?.getIdentifier() == null || currentitem.media?.getIdentifier() != currentMedia?.getIdentifier()) {
|
||||
currentMedia = currentitem.media
|
||||
playerDetailsFragment?.setItem(currentitem)
|
||||
}
|
||||
(activity as MainActivity).setPlayerVisible(true)
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
Logd(TAG, "cancelFlowEvents")
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
Logd(TAG, "procFlowEvents")
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
|
@ -327,25 +336,27 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
is FlowEvent.PlaybackServiceEvent -> {
|
||||
if (event.action == FlowEvent.PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)
|
||||
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
playerFragment?.onPlaybackServiceChanged(event)
|
||||
playerUI?.onPlaybackServiceChanged(event)
|
||||
}
|
||||
is FlowEvent.PlayEvent -> onEvenStartPlay(event)
|
||||
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
|
||||
is FlowEvent.FavoritesEvent -> loadMediaInfo(false)
|
||||
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
||||
|
||||
is FlowEvent.PlaybackPositionEvent -> playerFragment?.onPositionUpdate(event)
|
||||
is FlowEvent.SpeedChangedEvent -> playerFragment?.updatePlaybackSpeedButton(event)
|
||||
|
||||
is FlowEvent.PlaybackPositionEvent -> onPositionUpdate(event)
|
||||
is FlowEvent.SpeedChangedEvent -> playerUI?.updatePlaybackSpeedButton(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onPositionUpdate(event: FlowEvent.PlaybackPositionEvent) {
|
||||
// if (!isCollapsed) loadMediaInfo(false)
|
||||
playerUI?.onPositionUpdate(event)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (controller == null) return
|
||||
|
||||
when {
|
||||
fromUser -> {
|
||||
val prog: Float = progress / (seekBar.max.toFloat())
|
||||
|
@ -362,10 +373,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
// updateUi(controller!!.getMedia)
|
||||
// sbPosition.highlightCurrentChapter()
|
||||
// }
|
||||
txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${Converter.getDurationStringLong(position)}")
|
||||
} else txtvSeek.text = Converter.getDurationStringLong(position)
|
||||
binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${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
|
||||
toolbar.menu?.findItem(R.id.open_feed_item)?.setVisible(isEpisodeMedia)
|
||||
var item = currentitem
|
||||
if (item == null && isEpisodeMedia) item = (media as EpisodeMedia).episode
|
||||
val item = if (isEpisodeMedia) (media as EpisodeMedia).episode else null
|
||||
EpisodeMenuHandler.onPrepareMenu(toolbar.menu, item)
|
||||
|
||||
val mediaType = curMedia?.getMediaType()
|
||||
|
@ -419,15 +429,16 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
|
||||
override fun onMenuItemClick(menuItem: MenuItem): Boolean {
|
||||
val media: Playable = curMedia ?: return false
|
||||
|
||||
var feedItem = currentitem
|
||||
if (feedItem == null && media is EpisodeMedia) feedItem = media.episode
|
||||
// feedItem: FeedItem? = if (media is EpisodeMedia) media.item else null
|
||||
val feedItem = if (media is EpisodeMedia) media.episode else null
|
||||
if (feedItem != null && EpisodeMenuHandler.onMenuItemClicked(this, menuItem.itemId, feedItem)) return true
|
||||
|
||||
val itemId = menuItem.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 -> {
|
||||
controller!!.playPause()
|
||||
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
||||
|
@ -463,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
|
||||
fun fadePlayerToToolbar(slideOffset: Float) {
|
||||
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?.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()
|
||||
|
@ -471,65 +482,37 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
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"
|
||||
|
||||
private var _binding: InternalPlayerFragmentBinding? = null
|
||||
private var _binding: PlayerUiFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var imgvCover: ImageView
|
||||
var butPlay: PlayButton? = null
|
||||
|
||||
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 sbPosition: ChapterSeekBar
|
||||
|
||||
private var prevMedia: Playable? = null
|
||||
|
||||
private var showTimeLeft = false
|
||||
|
||||
@UnstableApi
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = InternalPlayerFragmentBinding.inflate(inflater)
|
||||
_binding = PlayerUiFragmentBinding.inflate(inflater)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
|
||||
episodeTitle = binding.titleView
|
||||
butPlaybackSpeed = binding.butPlaybackSpeed
|
||||
txtvPlaybackSpeed = binding.txtvPlaybackSpeed
|
||||
imgvCover = binding.imgvCover
|
||||
butPlay = binding.butPlay
|
||||
butRev = binding.butRev
|
||||
txtvRev = binding.txtvRev
|
||||
butFF = binding.butFF
|
||||
txtvFF = binding.txtvFF
|
||||
butSkip = binding.butSkip
|
||||
txtvSkip = binding.txtvSkip
|
||||
sbPosition = binding.sbPosition
|
||||
txtvPosition = binding.txtvPosition
|
||||
txtvLength = binding.txtvLength
|
||||
|
||||
setupLengthTextView()
|
||||
setupControlButtons()
|
||||
butPlaybackSpeed.setOnClickListener {
|
||||
binding.butPlaybackSpeed.setOnClickListener {
|
||||
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
|
||||
}
|
||||
sbPosition.setOnSeekBarChangeListener(this)
|
||||
|
||||
binding.internalPlayerFragment.setOnClickListener {
|
||||
Logd(TAG, "internalPlayerFragment was clicked")
|
||||
binding.playerUiFragment.setOnClickListener {
|
||||
Logd(TAG, "playerUiFragment was clicked")
|
||||
val media = curMedia
|
||||
if (media != null) {
|
||||
val mediaType = media.getMediaType()
|
||||
|
@ -540,32 +523,28 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
} else {
|
||||
controller?.playPause()
|
||||
// controller!!.ensureService()
|
||||
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), mediaType)
|
||||
val intent = getPlayerActivityIntent(requireContext(), mediaType)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
butPlay?.setOnClickListener {
|
||||
if (controller == null) return@setOnClickListener
|
||||
|
||||
val media = curMedia
|
||||
if (media != null) {
|
||||
if (media.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
||||
// val media = curMedia
|
||||
if (curMedia != null) {
|
||||
if (curMedia?.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
|
||||
controller!!.playPause()
|
||||
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media.getMediaType()))
|
||||
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
|
||||
} else controller!!.playPause()
|
||||
|
||||
if (!isControlButtonsSet) {
|
||||
sbPosition.visibility = View.VISIBLE
|
||||
isControlButtonsSet = true
|
||||
|
@ -573,17 +552,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun setupControlButtons() {
|
||||
butRev.setOnClickListener {
|
||||
if (controller != null && isPlaybackServiceReady()) {
|
||||
val curr: Int = position
|
||||
seekTo(curr - UserPreferences.rewindSecs * 1000)
|
||||
binding.butRev.setOnClickListener {
|
||||
if (controller != null && playbackService?.isServiceReady() == true) {
|
||||
playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000)
|
||||
sbPosition.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
butRev.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
|
||||
binding.butRev.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, binding.txtvRev)
|
||||
true
|
||||
}
|
||||
butPlay?.setOnLongClickListener {
|
||||
|
@ -593,61 +570,53 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
true
|
||||
}
|
||||
butFF.setOnClickListener {
|
||||
if (controller != null && isPlaybackServiceReady()) {
|
||||
val curr: Int = position
|
||||
seekTo(curr + UserPreferences.fastForwardSecs * 1000)
|
||||
binding.butFF.setOnClickListener {
|
||||
if (controller != null && playbackService?.isServiceReady() == true) {
|
||||
playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000)
|
||||
sbPosition.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
butFF.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF)
|
||||
binding.butFF.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, binding.txtvFF)
|
||||
true
|
||||
}
|
||||
butSkip.setOnClickListener {
|
||||
binding.butSkip.setOnClickListener {
|
||||
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
|
||||
val speedForward = UserPreferences.speedforwardSpeed
|
||||
if (speedForward > 0.1f) speedForward(speedForward)
|
||||
}
|
||||
}
|
||||
butSkip.setOnLongClickListener {
|
||||
binding.butSkip.setOnLongClickListener {
|
||||
activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun speedForward(speed: Float) {
|
||||
// playbackService?.speedForward(speed)
|
||||
if (playbackService?.mediaPlayer == null || playbackService?.isFallbackSpeed == true) return
|
||||
|
||||
if (playbackService?.mPlayer == null || playbackService?.isFallbackSpeed == true) return
|
||||
if (playbackService?.isSpeedForward == false) {
|
||||
playbackService?.normalSpeed = playbackService?.mediaPlayer!!.getPlaybackSpeed()
|
||||
playbackService?.mediaPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||
} else playbackService?.mediaPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||
|
||||
playbackService?.normalSpeed = playbackService?.mPlayer!!.getPlaybackSpeed()
|
||||
playbackService?.mPlayer!!.setPlaybackParams(speed, isSkipSilence)
|
||||
} else playbackService?.mPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
|
||||
playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun setupLengthTextView() {
|
||||
showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
||||
txtvLength.setOnClickListener(View.OnClickListener {
|
||||
if (controller == null) return@OnClickListener
|
||||
showTimeLeft = !showTimeLeft
|
||||
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(position, duration))
|
||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(curMedia, curPosition, duration))
|
||||
})
|
||||
}
|
||||
|
||||
fun updatePlaybackSpeedButton(event: FlowEvent.SpeedChangedEvent) {
|
||||
val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble())
|
||||
txtvPlaybackSpeed.text = speedStr
|
||||
butPlaybackSpeed.setSpeed(event.newSpeed)
|
||||
binding.txtvPlaybackSpeed.text = speedStr
|
||||
binding.butPlaybackSpeed.setSpeed(event.newSpeed)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
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 currentPosition: Int = converter.convert(event.position)
|
||||
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")
|
||||
return
|
||||
}
|
||||
|
||||
txtvPosition.text = Converter.getDurationStringLong(currentPosition)
|
||||
txtvPosition.setContentDescription(getString(R.string.position,
|
||||
binding.txtvPosition.text = Converter.getDurationStringLong(currentPosition)
|
||||
binding.txtvPosition.setContentDescription(getString(R.string.position,
|
||||
Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong())))
|
||||
val showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
||||
if (showTimeLeft) {
|
||||
|
@ -670,7 +638,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
Converter.getDurationStringLocalized(requireContext(), duration.toLong())))
|
||||
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) {
|
||||
val progress: Float = (event.position.toFloat()) / event.duration
|
||||
|
@ -678,7 +646,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
sbPosition.progress = (progress * sbPosition.max).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
fun onPlaybackServiceChanged(event: FlowEvent.PlaybackServiceEvent) {
|
||||
when (event.action) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onStart() {
|
||||
Logd(TAG, "onStart() called")
|
||||
super.onStart()
|
||||
|
||||
txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
|
||||
txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
|
||||
if (UserPreferences.speedforwardSpeed > 0.1f) txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
|
||||
else txtvSkip.visibility = View.GONE
|
||||
binding.txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())
|
||||
binding.txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())
|
||||
if (UserPreferences.speedforwardSpeed > 0.1f) binding.txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed)
|
||||
else binding.txtvSkip.visibility = View.GONE
|
||||
val media = curMedia ?: return
|
||||
updatePlaybackSpeedButton(FlowEvent.SpeedChangedEvent(getCurrentPlaybackSpeed(media)))
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onPause() {
|
||||
Logd(TAG, "onPause() called")
|
||||
super.onPause()
|
||||
controller?.pause()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
if (isPlaybackServiceReady()) {
|
||||
if (playbackService?.isServiceReady() == true) {
|
||||
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
||||
seekTo((prog * duration).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun updateUi(media: Playable?) {
|
||||
Logd(TAG, "updateUi called $media")
|
||||
if (media == null) return
|
||||
|
||||
episodeTitle.text = media.getEpisodeTitle()
|
||||
binding.titleView.text = media.getEpisodeTitle()
|
||||
// (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()) {
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
|
||||
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
|
||||
|
@ -761,18 +719,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
|
||||
companion object {
|
||||
var controller: PlaybackController? = null
|
||||
fun newInstance(controller_: PlaybackController) : InternalPlayerFragment {
|
||||
fun newInstance(controller_: PlaybackController) : PlayerUIFragment {
|
||||
controller = controller_
|
||||
return InternalPlayerFragment()
|
||||
return PlayerUIFragment()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
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 {
|
||||
|
||||
val TAG = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
@JvmField
|
||||
|
@ -96,25 +95,19 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
setupLoadMoreScrollListener()
|
||||
recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||
|
||||
swipeActions = SwipeActions(this, getFragmentTag()).attachTo(recyclerView)
|
||||
swipeActions = SwipeActions(this, TAG).attachTo(recyclerView)
|
||||
lifecycle.addObserver(swipeActions)
|
||||
swipeActions.setFilter(getFilter())
|
||||
refreshSwipeTelltale()
|
||||
binding.leftActionIcon.setOnClickListener {
|
||||
swipeActions.showDialog()
|
||||
}
|
||||
binding.rightActionIcon.setOnClickListener {
|
||||
swipeActions.showDialog()
|
||||
}
|
||||
binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
|
||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||
|
||||
swipeRefreshLayout = binding.swipeRefresh
|
||||
swipeRefreshLayout.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
||||
swipeRefreshLayout.setOnRefreshListener {
|
||||
FeedUpdateManager.runOnceOrAsk(requireContext())
|
||||
}
|
||||
swipeRefreshLayout.setOnRefreshListener { FeedUpdateManager.runOnceOrAsk(requireContext()) }
|
||||
|
||||
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.
|
||||
!userVisibleHint || !isVisible || !isMenuVisible -> return false
|
||||
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)
|
||||
}
|
||||
listAdapter.onContextItemSelected(item) -> return true
|
||||
|
@ -328,7 +321,7 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
speedDialView.visibility = View.GONE
|
||||
}
|
||||
|
||||
fun onEventMainThread(event: FlowEvent.EpisodeEvent) {
|
||||
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||
for (item in event.episodes) {
|
||||
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}")
|
||||
if (currentPlaying != null && currentPlaying!!.isCurMedia)
|
||||
currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||
if (currentPlaying != null && event.media?.getIdentifier() == currentPlaying!!.episode?.media?.getIdentifier() && currentPlaying!!.isCurMedia) currentPlaying!!.notifyPlaybackPositionUpdated(event)
|
||||
else {
|
||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||
for (i in 0 until listAdapter.itemCount) {
|
||||
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
|
||||
holder.notifyPlaybackPositionUpdated(event)
|
||||
break
|
||||
|
@ -361,7 +353,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
|
||||
private fun onKeyUp(event: KeyEvent) {
|
||||
if (!isAdded || !isVisible || !isMenuVisible) return
|
||||
|
||||
when (event.keyCode) {
|
||||
KeyEvent.KEYCODE_T -> recyclerView.smoothScrollToPosition(0)
|
||||
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) {
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(episodes, downloadUrl)
|
||||
if (pos >= 0) listAdapter.notifyItemChangedCompat(pos)
|
||||
|
@ -393,9 +384,9 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||
is FlowEvent.FeedListUpdateEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||
is FlowEvent.PlaybackPositionEvent -> onEventMainThread(event)
|
||||
is FlowEvent.EpisodeEvent -> onEventMainThread(event)
|
||||
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||
is FlowEvent.PlaybackPositionEvent -> onPlaybackPositionEvent(event)
|
||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -404,8 +395,8 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
EventFlow.stickyEvents.collectLatest { event ->
|
||||
Logd(TAG, "Received sticky event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.EpisodeDownloadEvent -> onEventMainThread(event)
|
||||
is FlowEvent.FeedUpdateRunningEvent -> onEventMainThread(event)
|
||||
is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
|
||||
is FlowEvent.FeedUpdateRunningEvent -> onFeedUpdateRunningEvent(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -456,15 +447,15 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
|
||||
protected abstract fun loadTotalItemCount(): Int
|
||||
|
||||
protected abstract fun getFilter(): EpisodeFilter
|
||||
|
||||
protected abstract fun getFragmentTag(): String
|
||||
open fun getFilter(): EpisodeFilter {
|
||||
return EpisodeFilter.unfiltered()
|
||||
}
|
||||
|
||||
protected abstract fun getPrefName(): String
|
||||
|
||||
protected open fun updateToolbar() {}
|
||||
|
||||
fun onEventMainThread(event: FlowEvent.FeedUpdateRunningEvent) {
|
||||
private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdateRunningEvent) {
|
||||
swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning
|
||||
}
|
||||
|
||||
|
@ -475,6 +466,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
|
||||
companion object {
|
||||
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.loadChapters
|
||||
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.storage.model.Chapter
|
||||
import ac.mdiq.podcini.storage.utils.EmbeddedChapterImage
|
||||
|
@ -159,6 +159,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
|
||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||
if (event.media?.getIdentifier() != media?.getIdentifier()) return
|
||||
updateChapterSelection(getCurrentChapter(media), false)
|
||||
adapter.notifyTimeChanged(event.position.toLong())
|
||||
}
|
||||
|
@ -166,7 +167,7 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
|||
private fun getCurrentChapter(media: Playable?): Int {
|
||||
if (controller == null) return -1
|
||||
|
||||
return getCurrentChapterIndex(media, position)
|
||||
return getCurrentChapterIndex(media, curPosition)
|
||||
}
|
||||
|
||||
private fun loadMediaInfo(forceRefresh: Boolean) {
|
||||
|
@ -274,12 +275,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
|||
} else {
|
||||
if (media != null) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,8 +33,6 @@ import ac.mdiq.podcini.util.event.FlowEvent
|
|||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -63,13 +61,11 @@ import java.util.*
|
|||
private var runningDownloads: Set<String> = HashSet()
|
||||
private var items: MutableList<Episode> = mutableListOf()
|
||||
|
||||
private lateinit var infoBar: TextView
|
||||
private lateinit var adapter: DownloadsListAdapter
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var swipeActions: SwipeActions
|
||||
private lateinit var speedDialView: SpeedDialView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var emptyView: EmptyViewHandler
|
||||
|
||||
private var displayUpArrow = false
|
||||
|
@ -114,10 +110,7 @@ import java.util.*
|
|||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||
if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||
|
||||
progressBar = binding.progLoading
|
||||
progressBar.visibility = View.VISIBLE
|
||||
|
||||
infoBar = binding.infoBar
|
||||
binding.progLoading.visibility = View.VISIBLE
|
||||
|
||||
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
speedDialView = multiSelectDial.fabSD
|
||||
|
@ -243,7 +236,7 @@ import java.util.*
|
|||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
val selectedItem: Episode? = adapter.longPressedItem
|
||||
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)
|
||||
}
|
||||
if (adapter.onContextItemSelected(item)) return true
|
||||
|
@ -287,12 +280,12 @@ import java.util.*
|
|||
|
||||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||
// 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 {
|
||||
Logd(TAG, "onPlaybackPositionEvent ${event.TAG} search list")
|
||||
for (i in 0 until adapter.itemCount) {
|
||||
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
|
||||
holder.notifyPlaybackPositionUpdated(event)
|
||||
break
|
||||
|
@ -334,7 +327,7 @@ import java.util.*
|
|||
withContext(Dispatchers.Main) {
|
||||
items = result.toMutableList()
|
||||
// adapter.setDummyViews(0)
|
||||
progressBar.visibility = View.GONE
|
||||
binding.progLoading.visibility = View.GONE
|
||||
adapter.updateItems(result)
|
||||
refreshInfoBar()
|
||||
}
|
||||
|
@ -362,12 +355,10 @@ import java.util.*
|
|||
var info = String.format(Locale.getDefault(), "%d%s", items.size, getString(R.string.episodes_suffix))
|
||||
if (items.isNotEmpty()) {
|
||||
var sizeMB: Long = 0
|
||||
for (item in items) {
|
||||
sizeMB += item.media?.size?:0
|
||||
}
|
||||
for (item in items) sizeMB += item.media?.size ?: 0
|
||||
info += " • " + (sizeMB / 1000000) + " MB"
|
||||
}
|
||||
infoBar.text = info
|
||||
binding.infoBar.text = info
|
||||
}
|
||||
|
||||
override fun onStartSelectMode() {
|
||||
|
|
|
@ -35,8 +35,6 @@ class EpisodeHomeFragment : Fragment() {
|
|||
private var _binding: EpisodeHomeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
// private val ioScope = CoroutineScope(Dispatchers.IO) // IO dispatcher for initialization
|
||||
|
||||
private var startIndex = 0
|
||||
private var ttsSpeed = 1.0f
|
||||
|
||||
|
@ -181,6 +179,9 @@ class EpisodeHomeFragment : Fragment() {
|
|||
}
|
||||
menu.findItem(R.id.share_notes)?.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) {
|
||||
|
@ -191,12 +192,10 @@ class EpisodeHomeFragment : Fragment() {
|
|||
@OptIn(UnstableApi::class) override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.switch_home -> {
|
||||
Logd(TAG, "switch_home selected")
|
||||
switchMode()
|
||||
return true
|
||||
}
|
||||
R.id.switchJS -> {
|
||||
Logd(TAG, "switchJS selected")
|
||||
jsEnabled = !jsEnabled
|
||||
showWebContent()
|
||||
return true
|
||||
|
@ -287,11 +286,7 @@ class EpisodeHomeFragment : Fragment() {
|
|||
fun newInstance(item: Episode): EpisodeHomeFragment {
|
||||
val fragment = EpisodeHomeFragment()
|
||||
Logd(TAG, "item.itemIdentifier ${item.identifier}")
|
||||
if (item.identifier != currentItem?.identifier) {
|
||||
currentItem = item
|
||||
} else {
|
||||
// currentItem?.feed = item.feed
|
||||
}
|
||||
if (item.identifier != currentItem?.identifier) currentItem = item
|
||||
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.preferences.UsageStatistics
|
||||
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.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
|
@ -79,20 +80,11 @@ import kotlin.math.max
|
|||
|
||||
private lateinit var shownotesCleaner: ShownotesCleaner
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var root: ViewGroup
|
||||
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 progbarDownload: CircularProgressBar
|
||||
private lateinit var progbarLoading: ProgressBar
|
||||
|
||||
private lateinit var homeButtonAction: View
|
||||
private lateinit var butAction1: ImageView
|
||||
private lateinit var butAction2: ImageView
|
||||
private lateinit var noMediaLabel: View
|
||||
|
||||
private var actionButton1: EpisodeActionButton? = null
|
||||
private var actionButton2: EpisodeActionButton? = null
|
||||
|
@ -101,7 +93,7 @@ import kotlin.math.max
|
|||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
_binding = EpisodeInfoFragmentBinding.inflate(inflater, container, false)
|
||||
root = binding.root
|
||||
// root = binding.root
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
|
||||
toolbar = binding.toolbar
|
||||
|
@ -110,13 +102,9 @@ import kotlin.math.max
|
|||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
txtvPodcast = binding.txtvPodcast
|
||||
txtvPodcast.setOnClickListener { openPodcast() }
|
||||
txtvTitle = binding.txtvTitle
|
||||
txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||
txtvDuration = binding.txtvDuration
|
||||
txtvPublished = binding.txtvPublished
|
||||
txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
||||
binding.txtvPodcast.setOnClickListener { openPodcast() }
|
||||
binding.txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
||||
binding.txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
||||
webvDescription = binding.webvDescription
|
||||
webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
val cMedia = curMedia
|
||||
|
@ -127,14 +115,10 @@ import kotlin.math.max
|
|||
|
||||
imgvCover = binding.imgvCover
|
||||
imgvCover.setOnClickListener { openPodcast() }
|
||||
progbarDownload = binding.circularProgressBar
|
||||
progbarLoading = binding.progbarLoading
|
||||
homeButtonAction = binding.homeButton
|
||||
butAction1 = binding.butAction1
|
||||
butAction2 = binding.butAction2
|
||||
noMediaLabel = binding.noMediaLabel
|
||||
|
||||
homeButtonAction.setOnClickListener {
|
||||
binding.homeButton.setOnClickListener {
|
||||
if (!item?.link.isNullOrEmpty()) {
|
||||
homeFragment = EpisodeHomeFragment.newInstance(item!!)
|
||||
(activity as MainActivity).loadChildFragment(homeFragment!!)
|
||||
|
@ -242,7 +226,7 @@ import kotlin.math.max
|
|||
@UnstableApi override fun onResume() {
|
||||
super.onResume()
|
||||
if (itemLoaded) {
|
||||
progbarLoading.visibility = View.GONE
|
||||
binding.progbarLoading.visibility = View.GONE
|
||||
updateAppearance()
|
||||
}
|
||||
}
|
||||
|
@ -250,13 +234,14 @@ import kotlin.math.max
|
|||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
|
||||
root.removeView(webvDescription)
|
||||
|
||||
binding.root.removeView(webvDescription)
|
||||
webvDescription.clearHistory()
|
||||
webvDescription.clearCache(true)
|
||||
webvDescription.clearView()
|
||||
webvDescription.destroy()
|
||||
|
||||
_binding = null
|
||||
}
|
||||
|
||||
@UnstableApi private fun onFragmentLoaded() {
|
||||
|
@ -276,14 +261,14 @@ import kotlin.math.max
|
|||
// 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)
|
||||
|
||||
if (item!!.feed != null) txtvPodcast.text = item!!.feed!!.title
|
||||
txtvTitle.text = item!!.title
|
||||
if (item!!.feed != null) binding.txtvPodcast.text = item!!.feed!!.title
|
||||
binding.txtvTitle.text = item!!.title
|
||||
binding.itemLink.text = item!!.link
|
||||
|
||||
if (item?.pubDate != null) {
|
||||
val pubDateStr = DateFormatter.formatAbbrev(context, Date(item!!.pubDate))
|
||||
txtvPublished.text = pubDateStr
|
||||
txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(item!!.pubDate)))
|
||||
binding.txtvPublished.text = pubDateStr
|
||||
binding.txtvPublished.setContentDescription(DateFormatter.formatForAccessibility(Date(item!!.pubDate)))
|
||||
}
|
||||
|
||||
val media = item?.media
|
||||
|
@ -326,14 +311,14 @@ import kotlin.math.max
|
|||
}
|
||||
|
||||
@UnstableApi private fun updateButtons() {
|
||||
progbarDownload.visibility = View.GONE
|
||||
binding.circularProgressBar.visibility = View.GONE
|
||||
val dls = DownloadServiceInterface.get()
|
||||
if (item != null && item!!.media != null && item!!.media!!.downloadUrl != null) {
|
||||
val url = item!!.media!!.downloadUrl!!
|
||||
if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||
progbarDownload.visibility = View.VISIBLE
|
||||
progbarDownload.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), item)
|
||||
progbarDownload.setIndeterminate(dls.isEpisodeQueued(url))
|
||||
binding.circularProgressBar.visibility = View.VISIBLE
|
||||
binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), item)
|
||||
binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -344,12 +329,12 @@ import kotlin.math.max
|
|||
butAction1.visibility = View.INVISIBLE
|
||||
actionButton2 = VisitWebsiteActionButton(item!!)
|
||||
}
|
||||
noMediaLabel.visibility = View.VISIBLE
|
||||
binding.noMediaLabel.visibility = View.VISIBLE
|
||||
} else {
|
||||
noMediaLabel.visibility = View.GONE
|
||||
binding.noMediaLabel.visibility = View.GONE
|
||||
if (media.getDuration() > 0) {
|
||||
txtvDuration.text = Converter.getDurationStringLong(media.getDuration())
|
||||
txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||
binding.txtvDuration.text = Converter.getDurationStringLong(media.getDuration())
|
||||
binding.txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||
}
|
||||
if (item != null) {
|
||||
actionButton1 = when {
|
||||
|
@ -439,7 +424,7 @@ import kotlin.math.max
|
|||
}
|
||||
|
||||
@UnstableApi private fun load() {
|
||||
if (!itemLoaded) progbarLoading.visibility = View.VISIBLE
|
||||
if (!itemLoaded) binding.progbarLoading.visibility = View.VISIBLE
|
||||
|
||||
Logd(TAG, "load() called")
|
||||
lifecycleScope.launch {
|
||||
|
@ -453,7 +438,7 @@ import kotlin.math.max
|
|||
feedItem
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
progbarLoading.visibility = View.GONE
|
||||
binding.progbarLoading.visibility = View.GONE
|
||||
item = result
|
||||
onFragmentLoaded()
|
||||
itemLoaded = true
|
||||
|
@ -465,7 +450,7 @@ import kotlin.math.max
|
|||
}
|
||||
|
||||
fun setItem(item_: Episode) {
|
||||
item = item_
|
||||
item = unmanagedCopy(item_)
|
||||
}
|
||||
|
||||
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.RealmDB.realm
|
||||
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.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
|
@ -89,6 +90,7 @@ import java.util.concurrent.Semaphore
|
|||
private var enableFilter: Boolean = true
|
||||
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
private var onInit: Boolean = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -194,7 +196,6 @@ import java.util.concurrent.Semaphore
|
|||
super.onStart()
|
||||
procFlowEvents()
|
||||
loadItems()
|
||||
// realmFeedMonitor()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
|
@ -269,11 +270,14 @@ import java.util.concurrent.Semaphore
|
|||
R.id.refresh_item -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
|
||||
R.id.refresh_complete_item -> {
|
||||
Thread {
|
||||
feed!!.nextPageLink = feed!!.downloadUrl
|
||||
feed!!.pageNr = 0
|
||||
try {
|
||||
runBlocking { resetPagedFeedPage(feed).join() }
|
||||
FeedUpdateManager.runOnce(requireContext(), feed)
|
||||
if (feed != null) {
|
||||
val feed_ = unmanagedCopy(feed!!)
|
||||
feed_.nextPageLink = feed_.downloadUrl
|
||||
feed_.pageNr = 0
|
||||
upsertBlk(feed_) {}
|
||||
FeedUpdateManager.runOnce(requireContext(), feed_)
|
||||
}
|
||||
} catch (e: ExecutionException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (e: InterruptedException) {
|
||||
|
@ -302,19 +306,10 @@ import java.util.concurrent.Semaphore
|
|||
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 {
|
||||
val selectedItem: Episode? = adapter.longPressedItem
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
|
||||
if (feed == null || episodes.isEmpty()) return
|
||||
|
@ -367,19 +335,8 @@ import java.util.concurrent.Semaphore
|
|||
while (i < size) {
|
||||
val item = event.episodes[i]
|
||||
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)
|
||||
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.add(pos, item)
|
||||
adapter.notifyItemChangedCompat(pos)
|
||||
|
@ -449,12 +406,12 @@ import java.util.concurrent.Semaphore
|
|||
|
||||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||
// 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 {
|
||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||
for (i in 0 until adapter.itemCount) {
|
||||
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
|
||||
holder.notifyPlaybackPositionUpdated(event)
|
||||
break
|
||||
|
@ -483,12 +440,11 @@ import java.util.concurrent.Semaphore
|
|||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
||||
is FlowEvent.PlayEvent -> onEvenStartPlay(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.EpisodesFilterOrSortEvent -> onEpisodesFilterSortEvent(event)
|
||||
is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||
is FlowEvent.EpisodePlayedEvent -> onEpisodePlayedEvent(event)
|
||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
||||
is FlowEvent.FeedListEvent -> if (feed != null && event.contains(feed!!)) loadItems()
|
||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||
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() {
|
||||
swipeActions.detach()
|
||||
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)
|
||||
}
|
||||
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) {
|
||||
if (feed != null && event.contains(feed!!)) {
|
||||
Logd(TAG, "onFeedListChanged called")
|
||||
loadItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFeedUpdateRunningEvent(event: FlowEvent.FeedUpdateRunningEvent) {
|
||||
nextPageLoader.setLoadingState(event.isFeedUpdateRunning)
|
||||
if (!event.isFeedUpdateRunning) nextPageLoader.root.visibility = View.GONE
|
||||
|
@ -612,13 +551,22 @@ import java.util.concurrent.Semaphore
|
|||
binding.header.butFilter.setOnLongClickListener {
|
||||
if (feed != null) {
|
||||
enableFilter = !enableFilter
|
||||
if (enableFilter) binding.header.butFilter.setColorFilter(Color.WHITE)
|
||||
else binding.header.butFilter.setColorFilter(Color.RED)
|
||||
onEpisodesFilterSortEvent(FlowEvent.EpisodesFilterOrSortEvent(FlowEvent.EpisodesFilterOrSortEvent.Action.FILTER_CHANGED, feed!!))
|
||||
episodes.clear()
|
||||
if (enableFilter) {
|
||||
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
|
||||
}
|
||||
|
||||
binding.header.txtvFailure.setOnClickListener { showErrorDetails() }
|
||||
binding.header.counts.text = adapter.itemCount.toString()
|
||||
headerCreated = true
|
||||
|
@ -659,31 +607,34 @@ import java.util.concurrent.Semaphore
|
|||
lifecycleScope.launch {
|
||||
try {
|
||||
feed = withContext(Dispatchers.IO) {
|
||||
val feed_ = getFeed(feedID, true)
|
||||
val feed_ = getFeed(feedID)
|
||||
if (feed_ != null) {
|
||||
episodes.clear()
|
||||
if (!feed_.preferences?.filterString.isNullOrEmpty()) {
|
||||
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {
|
||||
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)
|
||||
var hasNonMediaItems = false
|
||||
for (item in episodes) {
|
||||
// TODO: perhaps shouldn't set for all items, do it in the adaptor?
|
||||
item.feed = feed_
|
||||
if (item.media == null) hasNonMediaItems = true
|
||||
// Logd(TAG, "loadItems ${item.media?.downloaded} ${item.title}")
|
||||
}
|
||||
if (hasNonMediaItems) {
|
||||
ioScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!ttsReady) {
|
||||
initializeTTS(requireContext())
|
||||
semaphore.acquire()
|
||||
if (onInit) {
|
||||
var hasNonMediaItems = false
|
||||
for (item in episodes) {
|
||||
if (item.media == null) {
|
||||
hasNonMediaItems = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (hasNonMediaItems) {
|
||||
ioScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!ttsReady) {
|
||||
initializeTTS(requireContext())
|
||||
semaphore.acquire()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onInit = false
|
||||
}
|
||||
}
|
||||
feed_
|
||||
|
@ -695,7 +646,7 @@ import java.util.concurrent.Semaphore
|
|||
binding.progressBar.visibility = View.GONE
|
||||
adapter.setDummyViews(0)
|
||||
if (feed != null && episodes.isNotEmpty()) {
|
||||
adapter.updateItems(episodes)
|
||||
adapter.updateItems(episodes, feed)
|
||||
binding.header.counts.text = episodes.size.toString()
|
||||
}
|
||||
updateToolbar()
|
||||
|
@ -737,14 +688,14 @@ import java.util.concurrent.Semaphore
|
|||
if (feed != null) {
|
||||
Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
|
||||
runOnIOScope {
|
||||
feed.preferences?.filterString = newFilterValues.joinToString()
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
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
|
||||
else feed.sortOrder
|
||||
}
|
||||
|
||||
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
|
||||
|| ascending == SortOrder.DURATION_SHORT_LONG || ascending == SortOrder.RANDOM
|
||||
|
@ -765,24 +715,19 @@ import java.util.concurrent.Semaphore
|
|||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
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")
|
||||
runOnIOScope {
|
||||
feed.sortOrder = sortOrder
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
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.databinding.EditTextDialogBinding
|
||||
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.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.utils.HtmlToPlainText
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeedDownloadURL
|
||||
|
@ -32,7 +32,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
|
@ -66,15 +65,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
private lateinit var feed: Feed
|
||||
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 infoContainer: View
|
||||
private lateinit var header: View
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private val addLocalFolderLauncher = registerForActivityResult<Uri?, Uri>(AddLocalFolder()) {
|
||||
|
@ -115,18 +106,10 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
appBar.addOnOffsetChangedListener(iconTintManager)
|
||||
|
||||
imgvCover = binding.header.imgvCover
|
||||
txtvTitle = binding.header.txtvTitle
|
||||
txtvAuthorHeader = binding.header.txtvAuthor
|
||||
imgvBackground = binding.imgvBackground
|
||||
infoContainer = binding.infoContainer
|
||||
header = binding.header.root
|
||||
// https://github.com/bumptech/glide/issues/529
|
||||
// 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.setOnClickListener {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
|
@ -134,13 +117,12 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
txtvUrl.setOnClickListener(copyUrlToClipboard)
|
||||
binding.txtvUrl.setOnClickListener(copyUrlToClipboard)
|
||||
|
||||
// val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
val feedId = feed.id
|
||||
parentFragmentManager.beginTransaction().replace(R.id.statisticsFragmentContainer,
|
||||
FeedStatisticsFragment.newInstance(feedId, false), "feed_statistics_fragment")
|
||||
|
@ -158,8 +140,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
val horizontalSpacing = resources.getDimension(R.dimen.additional_horizontal_spacing).toInt()
|
||||
header.setPadding(horizontalSpacing, header.paddingTop, horizontalSpacing, header.paddingBottom)
|
||||
infoContainer.setPadding(horizontalSpacing, infoContainer.paddingTop, horizontalSpacing, infoContainer.paddingBottom)
|
||||
binding.header.root.setPadding(horizontalSpacing, binding.header.root.paddingTop, horizontalSpacing, binding.header.root.paddingBottom)
|
||||
binding.infoContainer.setPadding(horizontalSpacing, binding.infoContainer.paddingTop, horizontalSpacing, binding.infoContainer.paddingBottom)
|
||||
}
|
||||
|
||||
private fun showFeed() {
|
||||
|
@ -173,22 +155,22 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
error(R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
txtvTitle.text = feed.title
|
||||
txtvTitle.setMaxLines(6)
|
||||
binding.header.txtvTitle.text = feed.title
|
||||
binding.header.txtvTitle.setMaxLines(6)
|
||||
|
||||
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
|
||||
txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||
binding.txtvUrl.text = feed.downloadUrl
|
||||
binding.txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||
|
||||
if (feed.paymentLinks.isEmpty()) {
|
||||
lblSupport.visibility = View.GONE
|
||||
txtvFundingUrl.visibility = View.GONE
|
||||
binding.lblSupport.visibility = View.GONE
|
||||
binding.txtvFundingUrl.visibility = View.GONE
|
||||
} else {
|
||||
lblSupport.visibility = View.VISIBLE
|
||||
binding.lblSupport.visibility = View.VISIBLE
|
||||
val fundingList: ArrayList<FeedFunding> = feed.paymentLinks
|
||||
|
||||
// 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 = StringBuilder(StringUtils.trim(str.toString()))
|
||||
txtvFundingUrl.text = str.toString()
|
||||
binding.txtvFundingUrl.text = str.toString()
|
||||
}
|
||||
refreshToolbarState()
|
||||
}
|
||||
|
@ -260,8 +242,8 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
object : EditUrlSettingsDialog(activity as Activity, feed) {
|
||||
override fun setUrl(url: String?) {
|
||||
feed.downloadUrl = url
|
||||
txtvUrl.text = feed.downloadUrl
|
||||
txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||
binding.txtvUrl.text = feed.downloadUrl
|
||||
binding.txtvUrl.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||
}
|
||||
}.show()
|
||||
}
|
||||
|
|
|
@ -275,8 +275,8 @@ class FeedSettingsFragment : Fragment() {
|
|||
// EventFlow.postEvent(FlowEvent.FeedListUpdateEvent(feedPrefs!!.feedID))
|
||||
}
|
||||
updateVolumeAdaptationValue()
|
||||
if (feed != null && feedPrefs!!.volumeAdaptionSetting != null)
|
||||
EventFlow.postEvent(FlowEvent.VolumeAdaptionChangedEvent(feedPrefs!!.volumeAdaptionSetting!!, feed!!.id))
|
||||
// if (feed != null && feedPrefs!!.volumeAdaptionSetting != null)
|
||||
// EventFlow.postEvent(FlowEvent.VolumeAdaptionChangedEvent(feedPrefs!!.volumeAdaptionSetting!!, feed!!.id))
|
||||
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() {
|
||||
if (feedPrefs == null) return
|
||||
val pref = findPreference<SwitchPreferenceCompat>("keepUpdated")
|
||||
|
|
|
@ -37,9 +37,7 @@ import kotlin.math.min
|
|||
private var startDate : Long = 0L
|
||||
private var endDate : Long = Date().time
|
||||
|
||||
override fun getFragmentTag(): String {
|
||||
return TAG
|
||||
}
|
||||
var allHistory: List<Episode> = listOf()
|
||||
|
||||
override fun getPrefName(): String {
|
||||
return TAG
|
||||
|
@ -89,10 +87,6 @@ import kotlin.math.min
|
|||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
override fun getFilter(): EpisodeFilter {
|
||||
return EpisodeFilter.unfiltered()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (super.onOptionsItemSelected(item)) return true
|
||||
when (item.itemId) {
|
||||
|
@ -157,15 +151,15 @@ import kotlin.math.min
|
|||
}
|
||||
|
||||
override fun loadData(): List<Episode> {
|
||||
val hList = getHistory(0, page * EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList()
|
||||
// FeedItemPermutors.getPermutor(sortOrder).reorder(hList)
|
||||
return hList
|
||||
allHistory = getHistory(0, Int.MAX_VALUE, startDate, endDate, sortOrder).toMutableList()
|
||||
return allHistory.subList(0, min(allHistory.size-1, page * EPISODES_PER_PAGE))
|
||||
}
|
||||
|
||||
override fun loadMoreData(page: Int): List<Episode> {
|
||||
val hList = getHistory((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, startDate, endDate, sortOrder).toMutableList()
|
||||
// FeedItemPermutors.getPermutor(sortOrder).reorder(hList)
|
||||
return hList
|
||||
val offset = (page - 1) * EPISODES_PER_PAGE
|
||||
if (offset >= allHistory.size) return listOf()
|
||||
val toIndex = offset + EPISODES_PER_PAGE
|
||||
return allHistory.subList(offset, min(allHistory.size, toIndex))
|
||||
}
|
||||
|
||||
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,
|
||||
sortOrder: SortOrder = SortOrder.PLAYED_DATE_NEW_OLD): List<Episode> {
|
||||
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()
|
||||
for (m in medias) {
|
||||
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.episodeCacheSize
|
||||
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.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.model.DatasetStats
|
||||
|
@ -413,7 +413,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
// queueSize = queue?.episodeIds?.size ?: 0
|
||||
// }
|
||||
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 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 {
|
||||
_binding = OnlineFeedviewFragmentBinding.inflate(layoutInflater)
|
||||
binding.closeButton.visibility = View.INVISIBLE
|
||||
|
@ -170,9 +166,6 @@ import kotlin.concurrent.Volatile
|
|||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
_binding = null
|
||||
// updater?.dispose()
|
||||
// download?.dispose()
|
||||
// parser?.dispose()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
@ -332,7 +325,7 @@ import kotlin.concurrent.Volatile
|
|||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
||||
is FlowEvent.FeedListEvent -> onFeedListChanged(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -348,7 +341,7 @@ import kotlin.concurrent.Volatile
|
|||
}
|
||||
}
|
||||
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) {
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListEvent) {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val feeds = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -56,7 +56,7 @@ class OnlineSearchFragment : Fragment() {
|
|||
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 {
|
||||
|
|
|
@ -7,7 +7,7 @@ import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
|||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||
import ac.mdiq.podcini.playback.PlaybackController.Companion.curSpeedMultiplier
|
||||
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.storage.database.Episodes.persistEpisode
|
||||
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.
|
||||
*/
|
||||
@UnstableApi
|
||||
class PlayerDetailsFragment : Fragment() {
|
||||
class PlayerDetailsFragment : Fragment() {
|
||||
private lateinit var shownoteView: ShownotesWebView
|
||||
private var shownotesCleaner: ShownotesCleaner? = null
|
||||
|
||||
|
@ -67,8 +67,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
private val binding get() = _binding!!
|
||||
|
||||
private var prevItem: Episode? = null
|
||||
private var media: Playable? = null
|
||||
private var item: Episode? = null
|
||||
private var playable: Playable? = null
|
||||
private var currentItem: Episode? = null
|
||||
private var displayedChapterIndex = -1
|
||||
|
||||
private var cleanedNotes: String? = null
|
||||
|
@ -80,8 +80,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
private val currentChapter: Chapter?
|
||||
get() {
|
||||
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||
return media!!.getChapters()[displayedChapterIndex]
|
||||
if (playable == null || playable!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||
return playable!!.getChapters()[displayedChapterIndex]
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -115,11 +115,13 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
override fun onStart() {
|
||||
Logd(TAG, "onStart()")
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Logd(TAG, "onStop()")
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
@ -136,34 +138,34 @@ class PlayerDetailsFragment : Fragment() {
|
|||
return shownoteView.onContextItemSelected(item)
|
||||
}
|
||||
|
||||
internal fun load() {
|
||||
internal fun updateInfo() {
|
||||
// if (isLoading) return
|
||||
lifecycleScope.launch {
|
||||
Logd(TAG, "in load()")
|
||||
Logd(TAG, "in updateInfo")
|
||||
isLoading = true
|
||||
withContext(Dispatchers.IO) {
|
||||
if (item == null) {
|
||||
media = curMedia
|
||||
if (media != null && media is EpisodeMedia) {
|
||||
val episodeMedia = media as EpisodeMedia
|
||||
item = episodeMedia.episode
|
||||
if (currentItem == null) {
|
||||
playable = curMedia
|
||||
if (playable != null && playable is EpisodeMedia) {
|
||||
val episodeMedia = playable as EpisodeMedia
|
||||
currentItem = episodeMedia.episode
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
}
|
||||
if (item != null) {
|
||||
media = item!!.media
|
||||
if (prevItem?.identifier != item!!.identifier) cleanedNotes = null
|
||||
if (currentItem != null) {
|
||||
playable = currentItem!!.media
|
||||
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
||||
if (cleanedNotes == null) {
|
||||
Logd(TAG, "calling load description ${item!!.description==null} ${item!!.title}")
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(item?.description ?: "", media?.getDuration()?:0)
|
||||
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration()?:0)
|
||||
}
|
||||
prevItem = item
|
||||
prevItem = currentItem
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "subscribe: ${media?.getEpisodeTitle()}")
|
||||
displayMediaInfo(media!!)
|
||||
Logd(TAG, "subscribe: ${playable?.getEpisodeTitle()}")
|
||||
displayMediaInfo(playable!!)
|
||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||
Logd(TAG, "Webview loaded")
|
||||
}
|
||||
|
@ -177,17 +179,17 @@ class PlayerDetailsFragment : Fragment() {
|
|||
showHomeText = !showHomeText
|
||||
runOnIOScope {
|
||||
if (showHomeText) {
|
||||
homeText = item!!.transcript
|
||||
if (homeText == null && item?.link != null) {
|
||||
val url = item!!.link!!
|
||||
homeText = currentItem!!.transcript
|
||||
if (homeText == null && currentItem?.link != null) {
|
||||
val url = currentItem!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val readability4J = Readability4J(item!!.link!!, htmlSource)
|
||||
val readability4J = Readability4J(currentItem!!.link!!, htmlSource)
|
||||
val article = readability4J.parse()
|
||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
if (!readerhtml.isNullOrEmpty()) {
|
||||
item!!.setTranscriptIfLonger(readerhtml)
|
||||
homeText = item!!.transcript
|
||||
persistEpisode(item)
|
||||
currentItem!!.setTranscriptIfLonger(readerhtml)
|
||||
homeText = currentItem!!.transcript
|
||||
persistEpisode(currentItem)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
// val shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(item?.description ?: "", media?.getDuration() ?: 0)
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", playable?.getDuration() ?: 0)
|
||||
if (!cleanedNotes.isNullOrEmpty()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
shownoteView.loadDataWithBaseURL("https://127.0.0.1",
|
||||
|
@ -218,12 +220,12 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
@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())
|
||||
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||
if (media is EpisodeMedia) {
|
||||
if (item?.feedId != null) {
|
||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), item!!.feedId!!)
|
||||
if (currentItem?.feedId != null) {
|
||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), currentItem!!.feedId!!)
|
||||
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
||||
}
|
||||
} else {
|
||||
|
@ -231,8 +233,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
binding.txtvPodcastTitle.setOnLongClickListener { copyText(media.getFeedTitle()) }
|
||||
binding.episodeDate.text = StringUtils.stripToEmpty(pubDateStr)
|
||||
binding.txtvEpisodeTitle.text = item?.title
|
||||
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(item?.title?:"") }
|
||||
binding.txtvEpisodeTitle.text = currentItem?.title
|
||||
binding.txtvEpisodeTitle.setOnLongClickListener { copyText(currentItem?.title?:"") }
|
||||
binding.txtvEpisodeTitle.setOnClickListener {
|
||||
val lines = binding.txtvEpisodeTitle.lineCount
|
||||
val animUnit = 1500
|
||||
|
@ -262,9 +264,9 @@ class PlayerDetailsFragment : Fragment() {
|
|||
private fun updateChapterControlVisibility() {
|
||||
var chapterControlVisible = false
|
||||
when {
|
||||
media?.getChapters() != null -> chapterControlVisible = media!!.getChapters().isNotEmpty()
|
||||
media is EpisodeMedia -> {
|
||||
val fm: EpisodeMedia? = (media as EpisodeMedia?)
|
||||
playable?.getChapters() != null -> chapterControlVisible = playable!!.getChapters().isNotEmpty()
|
||||
playable is EpisodeMedia -> {
|
||||
val fm: EpisodeMedia? = (playable as EpisodeMedia?)
|
||||
// If an item has chapters but they are not loaded yet, still display the button.
|
||||
chapterControlVisible = fm?.episode != null && fm.episode!!.chapters.isNotEmpty()
|
||||
}
|
||||
|
@ -278,9 +280,9 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun refreshChapterData(chapterIndex: Int) {
|
||||
if (media != null && chapterIndex > -1) {
|
||||
if (media!!.getPosition() > media!!.getDuration() || chapterIndex >= media!!.getChapters().size - 1) {
|
||||
displayedChapterIndex = media!!.getChapters().size - 1
|
||||
if (playable != null && chapterIndex > -1) {
|
||||
if (playable!!.getPosition() > playable!!.getDuration() || chapterIndex >= playable!!.getChapters().size - 1) {
|
||||
displayedChapterIndex = playable!!.getChapters().size - 1
|
||||
binding.butNextChapter.visibility = View.INVISIBLE
|
||||
} else {
|
||||
displayedChapterIndex = chapterIndex
|
||||
|
@ -291,17 +293,17 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun displayCoverImage() {
|
||||
if (media == null) return
|
||||
if (displayedChapterIndex == -1 || media!!.getChapters().isEmpty() || media!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
||||
if (playable == null) return
|
||||
if (displayedChapterIndex == -1 || playable!!.getChapters().isEmpty() || playable!!.getChapters()[displayedChapterIndex].imageUrl.isNullOrEmpty()) {
|
||||
val imageLoader = binding.imgvCover.context.imageLoader
|
||||
val imageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(media!!.getImageLocation())
|
||||
.data(playable!!.getImageLocation())
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.placeholder(R.color.light_gray)
|
||||
.listener(object : ImageRequest.Listener {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.error(R.mipmap.ic_launcher)
|
||||
.target(binding.imgvCover)
|
||||
|
@ -314,7 +316,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
imageLoader.enqueue(imageRequest)
|
||||
|
||||
} else {
|
||||
val imgLoc = EmbeddedChapterImage.getModelFor(media!!, displayedChapterIndex)
|
||||
val imgLoc = EmbeddedChapterImage.getModelFor(playable!!, displayedChapterIndex)
|
||||
val imageLoader = binding.imgvCover.context.imageLoader
|
||||
val imageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(imgLoc)
|
||||
|
@ -323,7 +325,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
.listener(object : ImageRequest.Listener {
|
||||
override fun onError(request: ImageRequest, result: ErrorResult) {
|
||||
val fallbackImageRequest = ImageRequest.Builder(requireContext())
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
|
||||
.data(ImageResourceUtils.getFallbackImageLocation(playable!!))
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.error(R.mipmap.ic_launcher)
|
||||
.target(binding.imgvCover)
|
||||
|
@ -343,19 +345,19 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
when {
|
||||
displayedChapterIndex < 1 -> seekTo(0)
|
||||
(position - 10000 * curSpeedMultiplier) < curr.start -> {
|
||||
(curPosition - 10000 * curSpeedMultiplier) < curr.start -> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
seekTo(playable!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
}
|
||||
|
||||
|
||||
|
@ -425,17 +427,18 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(media, event.position)
|
||||
if (newChapterIndex > -1 && newChapterIndex != displayedChapterIndex) {
|
||||
private fun onEventMainThread(event: FlowEvent.PlaybackPositionEvent) {
|
||||
if (playable?.getIdentifier() != event.media?.getIdentifier()) return
|
||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(playable, event.position)
|
||||
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) {
|
||||
refreshChapterData(newChapterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
fun setItem(item_: Episode) {
|
||||
Logd(TAG, "setItem ${item_.title}")
|
||||
if (item?.identifier != item_.identifier) {
|
||||
item = item_
|
||||
if (currentItem?.identifier != item_.identifier) {
|
||||
currentItem = item_
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
|
|
|
@ -44,8 +44,6 @@ import android.os.Bundle
|
|||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.widget.CheckBox
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -74,15 +72,13 @@ import java.util.*
|
|||
private var _binding: QueueFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var infoBar: TextView
|
||||
private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var emptyView: EmptyViewHandler
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||
private lateinit var swipeActions: SwipeActions
|
||||
private lateinit var speedDialView: SpeedDialView
|
||||
private lateinit var progressBar: ProgressBar
|
||||
|
||||
|
||||
private var displayUpArrow = false
|
||||
private var queueItems: MutableList<Episode> = mutableListOf()
|
||||
|
||||
|
@ -112,10 +108,8 @@ import java.util.*
|
|||
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
|
||||
toolbar.inflateMenu(R.menu.queue)
|
||||
refreshToolbarState()
|
||||
progressBar = binding.progressBar
|
||||
progressBar.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
infoBar = binding.infoBar
|
||||
recyclerView = binding.recyclerView
|
||||
val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||
if (animator != null && animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||
|
@ -335,12 +329,12 @@ import java.util.*
|
|||
private fun onPlaybackPositionEvent(event: FlowEvent.PlaybackPositionEvent) {
|
||||
// Logd(TAG, "onEventMainThread() called with ${event.TAG}")
|
||||
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 {
|
||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||
Logd(TAG, "onPlaybackPositionEvent() ${event.TAG} search list")
|
||||
for (i in 0 until adapter!!.itemCount) {
|
||||
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
|
||||
holder.notifyPlaybackPositionUpdated(event)
|
||||
break
|
||||
|
@ -370,7 +364,7 @@ import java.util.*
|
|||
private fun onFeedPrefsChanged(event: FlowEvent.FeedPrefsChangeEvent) {
|
||||
Log.d(TAG,"speedPresetChanged called")
|
||||
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
|
||||
if (selectedItem == null) {
|
||||
Log.i(TAG, "Selected item was null, ignoring selection")
|
||||
Logd(TAG, "Selected item was null, ignoring selection")
|
||||
return super.onContextItemSelected(item)
|
||||
}
|
||||
if (adapter!!.onContextItemSelected(item)) return true
|
||||
|
||||
val pos: Int = EpisodeUtil.indexOfItemWithId(queueItems.toList(), selectedItem.id)
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -536,7 +530,7 @@ import java.util.*
|
|||
info += " • "
|
||||
info += Converter.getDurationStringLocalized(requireActivity(), timeLeft)
|
||||
}
|
||||
infoBar.text = info
|
||||
binding.infoBar.text = info
|
||||
toolbar.title = "${getString(R.string.queue_label)}: ${curQueue.name}"
|
||||
}
|
||||
|
||||
|
@ -551,7 +545,7 @@ import java.util.*
|
|||
|
||||
queueItems.clear()
|
||||
queueItems.addAll(curQueue.episodes)
|
||||
progressBar.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
adapter?.setDummyViews(0)
|
||||
adapter?.updateItems(queueItems)
|
||||
if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||
|
@ -562,13 +556,13 @@ import java.util.*
|
|||
swipeActions.detach()
|
||||
speedDialView.visibility = View.VISIBLE
|
||||
refreshToolbarState()
|
||||
infoBar.visibility = View.GONE
|
||||
binding.infoBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onEndSelectMode() {
|
||||
speedDialView.close()
|
||||
speedDialView.visibility = View.GONE
|
||||
infoBar.visibility = View.VISIBLE
|
||||
binding.infoBar.visibility = View.VISIBLE
|
||||
swipeActions.attachTo(recyclerView)
|
||||
}
|
||||
|
||||
|
@ -603,7 +597,7 @@ import java.util.*
|
|||
private fun reorderQueue(sortOrder: SortOrder?, broadcastUpdate: Boolean) : Job {
|
||||
Logd(TAG, "reorderQueue called")
|
||||
if (sortOrder == null) {
|
||||
Log.w(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
|
||||
Logd(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
|
||||
return Job()
|
||||
}
|
||||
val permutor = getPermutor(sortOrder)
|
||||
|
|
|
@ -215,14 +215,6 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
val podcast: PodcastSearchResult? = getItem(position)
|
||||
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) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
|
|
|
@ -23,7 +23,6 @@ import kotlin.math.min
|
|||
* Shows all episodes (possibly filtered by user).
|
||||
*/
|
||||
@UnstableApi class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
||||
// val TAG = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
private val episodeList: MutableList<Episode> = mutableListOf()
|
||||
|
||||
|
@ -31,17 +30,10 @@ import kotlin.math.min
|
|||
val root = super.onCreateView(inflater, container, savedInstanceState)
|
||||
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.setTitle(R.string.episodes_label)
|
||||
updateToolbar()
|
||||
listAdapter.setOnSelectModeListener(null)
|
||||
// updateFilterUi()
|
||||
// txtvInformation.setOnClickListener {
|
||||
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
||||
// }
|
||||
return root
|
||||
}
|
||||
|
||||
|
@ -66,21 +58,16 @@ import kotlin.math.min
|
|||
}
|
||||
|
||||
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 {
|
||||
return episodeList.size
|
||||
}
|
||||
|
||||
override fun getFilter(): EpisodeFilter {
|
||||
return EpisodeFilter.unfiltered()
|
||||
}
|
||||
|
||||
override fun getFragmentTag(): String {
|
||||
return TAG
|
||||
}
|
||||
|
||||
override fun getPrefName(): String {
|
||||
return PREF_NAME
|
||||
}
|
||||
|
@ -97,14 +84,6 @@ import kotlin.math.min
|
|||
if (super.onOptionsItemSelected(item)) return true
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
const val PREF_NAME: String = "EpisodesListFragment"
|
||||
const val EXTRA_EPISODES: String = "episodes_list"
|
||||
|
||||
fun newInstance(episodes: MutableList<Episode>): RemoteEpisodesFragment {
|
||||
val i = RemoteEpisodesFragment()
|
||||
|
|
|
@ -253,7 +253,7 @@ import java.lang.ref.WeakReference
|
|||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
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.PlaybackPositionEvent -> onEventMainThread(event)
|
||||
else -> {}
|
||||
|
@ -295,13 +295,13 @@ import java.lang.ref.WeakReference
|
|||
}
|
||||
|
||||
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)
|
||||
else {
|
||||
Logd(TAG, "onEventMainThread() ${event.TAG} search list")
|
||||
for (i in 0 until adapter.itemCount) {
|
||||
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
|
||||
holder.notifyPlaybackPositionUpdated(event)
|
||||
break
|
||||
|
@ -509,13 +509,6 @@ import java.lang.ref.WeakReference
|
|||
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) {
|
||||
placeholder(R.color.light_gray)
|
||||
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.preferences.UserPreferences
|
||||
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.getTags
|
||||
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.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
|
@ -38,14 +38,12 @@ import android.widget.*
|
|||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
|
@ -72,14 +70,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var subscriptionRecycler: RecyclerView
|
||||
private lateinit var listAdapter: ListAdapter
|
||||
private lateinit var listAdapter: SubscriptionsAdapter<*>
|
||||
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 swipeRefreshLayout: SwipeRefreshLayout
|
||||
private lateinit var progressBar: ProgressBar
|
||||
private lateinit var speedDialView: SpeedDialView
|
||||
|
||||
private var tagFilterIndex = 1
|
||||
|
@ -89,6 +82,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
private var feedList: MutableList<Feed> = mutableListOf()
|
||||
private var feedListFiltered: List<Feed> = mutableListOf()
|
||||
|
||||
private var useGrid: Boolean? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
|
@ -120,20 +115,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
subscriptionRecycler.addItemDecoration(GridDividerItemDecorator())
|
||||
registerForContextMenu(subscriptionRecycler)
|
||||
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)
|
||||
subscriptionRecycler.adapter = listAdapter
|
||||
initAdapter()
|
||||
setupEmptyView()
|
||||
|
||||
resetTags()
|
||||
|
@ -163,31 +146,19 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
} else false
|
||||
}
|
||||
|
||||
progressBar = binding.progressBar
|
||||
progressBar.visibility = View.VISIBLE
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
val subscriptionAddButton: FloatingActionButton = binding.subscriptionsAdd
|
||||
subscriptionAddButton.setOnClickListener {
|
||||
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(AddFeedFragment())
|
||||
}
|
||||
|
||||
feedsInfoMsg = binding.feedsInfoMessage
|
||||
// feedsInfoMsg.setOnClickListener {
|
||||
// SubscriptionsFilterDialog().show(
|
||||
// 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 {
|
||||
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
binding.swipeRefresh.setDistanceToTriggerSync(resources.getInteger(R.integer.swipe_refresh_distance))
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
FeedUpdateManager.runOnceOrAsk(requireContext())
|
||||
}
|
||||
|
||||
val speedDialBinding = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
|
||||
speedDialView = speedDialBinding.fabSD
|
||||
speedDialView.overlayLayout = speedDialBinding.fabSDOverlay
|
||||
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)
|
||||
true
|
||||
}
|
||||
|
||||
loadSubscriptions()
|
||||
|
||||
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() {
|
||||
super.onStart()
|
||||
initAdapter()
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -259,7 +244,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.FeedListUpdateEvent -> onFeedListChanged(event)
|
||||
is FlowEvent.FeedListEvent -> onFeedListChanged(event)
|
||||
is FlowEvent.EpisodePlayedEvent, is FlowEvent.FeedsSortedEvent -> loadSubscriptions()
|
||||
is FlowEvent.FeedTagsChangedEvent -> resetTags()
|
||||
else -> {}
|
||||
|
@ -270,7 +255,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
EventFlow.stickyEvents.collectLatest { event ->
|
||||
Logd(TAG, "Received sticky event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.FeedUpdateRunningEvent -> swipeRefreshLayout.isRefreshing = event.isFeedUpdateRunning
|
||||
is FlowEvent.FeedUpdateRunningEvent -> binding.swipeRefresh.isRefreshing = event.isFeedUpdateRunning
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -311,9 +296,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
if ( feedListFiltered.size > result.size) listAdapter.endSelectMode()
|
||||
feedList = result.toMutableList()
|
||||
filterOnTag()
|
||||
progressBar.visibility = View.GONE
|
||||
binding.progressBar.visibility = View.GONE
|
||||
listAdapter.setItems(feedListFiltered)
|
||||
feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
binding.count.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
emptyView.updateVisibility()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
|
@ -416,7 +401,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
val feed: Feed = listAdapter.getSelectedItem() ?: return false
|
||||
val feed: Feed = listAdapter.selectedItem ?: return false
|
||||
val itemId = item.itemId
|
||||
if (itemId == R.id.multi_select) {
|
||||
speedDialView.visibility = View.VISIBLE
|
||||
|
@ -426,8 +411,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
return FeedMenuHandler.onMenuItemClicked(this, item.itemId, feed) { this.loadSubscriptions() }
|
||||
}
|
||||
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent?) {
|
||||
updateFeedMap()
|
||||
private fun onFeedListChanged(event: FlowEvent.FeedListEvent) {
|
||||
// val feeds_ = realm.query(Feed::class,"id IN $0", event.feedIds).find()
|
||||
// updateFeedMap(feeds_)
|
||||
loadSubscriptions()
|
||||
}
|
||||
|
||||
|
@ -535,12 +521,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
private inner class ListAdapter
|
||||
: SelectableAdapter<ListAdapter.ViewHolder?>(activity as MainActivity), View.OnCreateContextMenuListener {
|
||||
private abstract inner class SubscriptionsAdapter<T : RecyclerView.ViewHolder?> : SelectableAdapter<T>(activity as MainActivity), View.OnCreateContextMenuListener {
|
||||
|
||||
private var feedList: List<Feed>
|
||||
private var selectedItem: Feed? = null
|
||||
private var longPressedPosition: Int = 0 // used to init actionMode
|
||||
protected var feedList: List<Feed>
|
||||
var selectedItem: Feed? = null
|
||||
protected var longPressedPosition: Int = 0 // used to init actionMode
|
||||
val selectedItems: List<Any>
|
||||
get() {
|
||||
val items = ArrayList<Feed>()
|
||||
|
@ -560,14 +545,49 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener, Selec
|
|||
fun getItem(position: Int): Any {
|
||||
return feedList[position]
|
||||
}
|
||||
fun getSelectedItem(): Feed? {
|
||||
return selectedItem
|
||||
override fun getItemCount(): Int {
|
||||
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)
|
||||
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]
|
||||
holder.bind(feed)
|
||||
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 {
|
||||
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
|
||||
@UnstableApi override fun onBindViewHolder(holder: ViewHolderBrief, position: Int) {
|
||||
val feed: Feed = feedList[position]
|
||||
holder.bind(feed)
|
||||
if (inActionMode()) {
|
||||
// inflater.inflate(R.menu.multi_select_context_popup, menu)
|
||||
// menu.findItem(R.id.multi_select).setVisible(true)
|
||||
holder.selectCheckbox.visibility = View.VISIBLE
|
||||
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 {
|
||||
inflater.inflate(R.menu.nav_feed_context, menu)
|
||||
// menu.findItem(R.id.multi_select).setVisible(true)
|
||||
menu.setHeaderTitle(selectedItem?.title)
|
||||
holder.selectView.visibility = View.GONE
|
||||
holder.coverImage.alpha = 1.0f
|
||||
}
|
||||
MenuItemUtils.setOnClickListeners(menu) { item: MenuItem ->
|
||||
this@SubscriptionsFragment.onContextItemSelected(item)
|
||||
holder.coverImage.setOnClickListener {
|
||||
if (inActionMode()) holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||
else {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.multi_select) {
|
||||
holder.coverImage.setOnLongClickListener {
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
selectedItem = feed
|
||||
startSelectMode(longPressedPosition)
|
||||
return true
|
||||
true
|
||||
}
|
||||
return false
|
||||
}
|
||||
fun setItems(listItems: List<Feed>) {
|
||||
this.feedList = listItems
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
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.setOnTouchListener { _: View?, e: MotionEvent ->
|
||||
if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) {
|
||||
if (!inActionMode()) {
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
selectedItem = feed
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
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.PlaybackController.Companion.isPlayingVideoLocally
|
||||
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.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
|
@ -79,17 +79,40 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
var controller: PlaybackController? = null
|
||||
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 {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
root = binding.root
|
||||
|
||||
controller = newPlaybackController()
|
||||
controller!!.init()
|
||||
// loadMediaInfo()
|
||||
|
||||
setupView()
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
|
@ -104,15 +127,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
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() {
|
||||
this@VideoEpisodeFragment.loadMediaInfo()
|
||||
}
|
||||
|
||||
override fun onPlaybackEnd() {
|
||||
activity?.finish()
|
||||
}
|
||||
|
@ -131,7 +153,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
|
||||
// Controller released; we will not receive buffering updates
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
@ -151,7 +172,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
_binding = null
|
||||
controller?.release()
|
||||
controller = null // prevent leak
|
||||
// scope.cancel()
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
|
@ -204,7 +224,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
@OptIn(UnstableApi::class) private fun loadMediaInfo() {
|
||||
Logd(TAG, "loadMediaInfo called")
|
||||
if (curMedia == null) return
|
||||
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||
Logd(TAG, "Closing, no longer video")
|
||||
destroyingDueToReload = true
|
||||
|
@ -245,7 +264,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun loadInBackground(): Episode? {
|
||||
|
@ -266,11 +284,9 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
private fun setupView() {
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
||||
|
||||
binding.durationLabel.setOnClickListener {
|
||||
showTimeLeft = !showTimeLeft
|
||||
val media = curMedia ?: return@setOnClickListener
|
||||
|
||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||
val length: String
|
||||
if (showTimeLeft) {
|
||||
|
@ -281,7 +297,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
length = getDurationStringLong(duration)
|
||||
}
|
||||
binding.durationLabel.text = length
|
||||
|
||||
setShowRemainTimeSetting(showTimeLeft)
|
||||
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||
}
|
||||
|
@ -304,15 +319,12 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
|
||||
setupVideoControlsToggler()
|
||||
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
|
||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
binding.videoView.setAvailableSize(binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
||||
}
|
||||
|
||||
webvDescription = binding.webvDescription
|
||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
// val cMedia = getMedia
|
||||
|
@ -325,45 +337,11 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
// }
|
||||
// registerForContextMenu(webvDescription)
|
||||
// webvDescription.visibility = View.GONE
|
||||
|
||||
binding.toggleViews.setOnClickListener {
|
||||
(activity as? VideoplayerActivity)?.toggleViews()
|
||||
}
|
||||
binding.toggleViews.setOnClickListener { (activity as? VideoplayerActivity)?.toggleViews() }
|
||||
binding.audioOnly.setOnClickListener {
|
||||
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
||||
(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() {
|
||||
|
@ -393,17 +371,14 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||
binding.skipAnimationImage.layoutParams = params
|
||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.skipAnimationImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
}
|
||||
|
@ -417,7 +392,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videoview holder created")
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -431,27 +407,24 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
|
||||
fun notifyVideoSurfaceAbandoned() {
|
||||
// playbackService?.notifyVideoSurfaceAbandoned()
|
||||
playbackService?.mediaPlayer?.pause(abandonFocus = true, reinit = false)
|
||||
playbackService?.mediaPlayer?.resetVideoSurface()
|
||||
playbackService?.mPlayer?.pause(abandonFocus = true, reinit = false)
|
||||
playbackService?.mPlayer?.resetVideoSurface()
|
||||
}
|
||||
|
||||
fun setVideoSurface(holder: SurfaceHolder?) {
|
||||
playbackService?.mediaPlayer?.setVideoSurface(holder)
|
||||
}
|
||||
// fun setVideoSurface(holder: SurfaceHolder?) {
|
||||
// playbackService?.mPlayer?.setVideoSurface(holder)
|
||||
// }
|
||||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
if (controller == null) return
|
||||
|
||||
val curr = position
|
||||
seekTo(curr - rewindSecs * 1000)
|
||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onPlayPause() {
|
||||
if (controller == null) return
|
||||
|
||||
controller!!.playPause()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
@ -459,9 +432,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
if (controller == null) return
|
||||
|
||||
val curr = position
|
||||
seekTo(curr + fastForwardSecs * 1000)
|
||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
|
@ -512,11 +483,10 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
|
||||
private fun onPositionObserverUpdate() {
|
||||
if (controller == null) return
|
||||
|
||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||
val currentPosition = converter.convert(position)
|
||||
val currentPosition = converter.convert(curPosition)
|
||||
val duration_ = converter.convert(duration)
|
||||
val remainingTime = converter.convert(duration - position)
|
||||
val remainingTime = converter.convert(duration - curPosition)
|
||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.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) {
|
||||
if (controller == null) return
|
||||
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(curSpeedMultiplier)
|
||||
|
@ -559,7 +528,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
seekTo((prog * duration).toInt())
|
||||
|
||||
binding.seekCardView.scaleX = 1f
|
||||
binding.seekCardView.scaleY = 1f
|
||||
binding.seekCardView.animate()
|
||||
|
@ -574,6 +542,6 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
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 {
|
||||
val holder = h as StatisticsHolder
|
||||
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) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
|
|
|
@ -75,7 +75,7 @@ class CoverLoader(private val activity: MainActivity) {
|
|||
.data(uri)
|
||||
.setHeader("User-Agent", "Mozilla/5.0")
|
||||
.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")
|
||||
val fallbackImageRequest = ImageRequest.Builder(activity)
|
||||
.data(fallbackUri)
|
||||
|
@ -99,13 +99,13 @@ class CoverLoader(private val activity: MainActivity) {
|
|||
override fun onStart(placeholder: Drawable?) {
|
||||
|
||||
}
|
||||
override fun onError(errorDrawable: Drawable?) {
|
||||
override fun onError(error: Drawable?) {
|
||||
setTitleVisibility(fallbackTitle.get(), true)
|
||||
}
|
||||
|
||||
override fun onSuccess(resource: Drawable) {
|
||||
override fun onSuccess(result: Drawable) {
|
||||
val ivCover = cover.get()
|
||||
ivCover!!.setImageDrawable(resource)
|
||||
ivCover!!.setImageDrawable(result)
|
||||
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.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.util.Converter.getDurationStringLong
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
|
@ -45,11 +46,12 @@ object WidgetUpdater {
|
|||
* Update the widgets with the given parameters. Must be called in a background thread.
|
||||
*/
|
||||
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)
|
||||
VideoPlayerActivityStarter(context).pendingIntent
|
||||
else MainActivityStarter(context).withOpenPlayer().pendingIntent
|
||||
val startMediaPlayer =
|
||||
if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO) VideoPlayerActivityStarter(context).pendingIntent
|
||||
else MainActivityStarter(context).withOpenPlayer().pendingIntent
|
||||
|
||||
val startPlaybackSpeedDialog = PlaybackSpeedActivityStarter(context).pendingIntent
|
||||
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.butPlaybackSpeed, startPlaybackSpeedDialog)
|
||||
|
||||
val radius = context.resources.getDimensionPixelSize(R.dimen.widget_inner_radius)
|
||||
// val options = RequestOptions()
|
||||
// .dontAnimate()
|
||||
// .transform(FitCenter(), RoundedCorners(radius))
|
||||
|
||||
try {
|
||||
val imgLoc = widgetState.media.getImageLocation()
|
||||
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 {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(imgLoc)
|
||||
|
@ -162,6 +147,7 @@ object WidgetUpdater {
|
|||
val widgetIds = manager.getAppWidgetIds(playerWidget)
|
||||
|
||||
for (id in widgetIds) {
|
||||
Logd(TAG, "updating widget $id")
|
||||
val options = manager.getAppWidgetOptions(id)
|
||||
// val prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
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.Episode
|
||||
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.VolumeAdaptionSetting
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.util.Consumer
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -26,7 +28,7 @@ import kotlin.math.max
|
|||
sealed class 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() {
|
||||
enum class Action { SERVICE_STARTED, SERVICE_SHUT_DOWN, }
|
||||
|
@ -124,34 +126,25 @@ sealed class FlowEvent {
|
|||
}
|
||||
}
|
||||
|
||||
data class FeedListUpdateEvent(val feedIds: List<Long> = emptyList()) : FlowEvent() {
|
||||
constructor(feed: Feed) : this(listOf(feed.id))
|
||||
constructor(feedId: Long) : this(listOf(feedId))
|
||||
constructor(feeds: List<Feed>, junk: String = "") : this(feeds.map { it.id })
|
||||
data class FeedListEvent(val action: Action, val feedIds: List<Long> = emptyList()) : FlowEvent() {
|
||||
enum class Action { ADDED, REMOVED, ERROR, UNKNOWN }
|
||||
|
||||
constructor(action: Action, feedId: Long) : this(action, listOf(feedId))
|
||||
|
||||
fun contains(feed: Feed): Boolean {
|
||||
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 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 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()
|
||||
|
||||
|
@ -160,8 +153,6 @@ sealed class FlowEvent {
|
|||
get() = map.keys
|
||||
}
|
||||
|
||||
// data class NewEpisodeDownloadEvent(val url: String) : FlowEvent() {}
|
||||
|
||||
// TODO: need better handling at receving end
|
||||
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 FeedUpdateRunningEvent(val isFeedUpdateRunning: Boolean) : FlowEvent()
|
||||
|
@ -195,12 +184,9 @@ sealed class FlowEvent {
|
|||
data class SyncServiceEvent(val messageResId: Int, val message: String = "") : FlowEvent()
|
||||
|
||||
data class DiscoveryDefaultUpdateEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||
|
||||
data class DiscoveryCompletedEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||
}
|
||||
|
||||
object EventFlow {
|
||||
val collectorCount = MutableStateFlow(0)
|
||||
val events: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 0)
|
||||
val stickyEvents: MutableSharedFlow<FlowEvent> = MutableSharedFlow(replay = 1)
|
||||
val keyEvents: MutableSharedFlow<KeyEvent> = MutableSharedFlow(replay = 0)
|
||||
|
@ -211,7 +197,7 @@ object EventFlow {
|
|||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted: $event")
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
events.emit(event)
|
||||
}
|
||||
}
|
||||
|
@ -222,7 +208,7 @@ object EventFlow {
|
|||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted sticky: $event")
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
stickyEvents.emit(event)
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +219,7 @@ object EventFlow {
|
|||
val caller = if (stackTrace.size > 3) stackTrace[3] else null
|
||||
Logd("EventFlow", "${caller?.className}.${caller?.methodName} posted key: $event")
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Default) {
|
||||
CoroutineScope(Dispatchers.Default).launch {
|
||||
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
|
||||
|
||||
when (sortOrder) {
|
||||
SortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
||||
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.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
||||
pubDate(f2).compareTo(pubDate(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.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> 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.DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> pubDate(f2).compareTo(pubDate(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? ->
|
||||
feedTitle(f1).compareTo(feedTitle(f2))
|
||||
}
|
||||
SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? ->
|
||||
feedTitle(f2).compareTo(feedTitle(f1))
|
||||
}
|
||||
SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
|
||||
SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
|
||||
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> {
|
||||
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> {
|
||||
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))
|
||||
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)) }
|
||||
}
|
||||
if (comparator != null) {
|
||||
val comparator2: Comparator<Episode> = comparator
|
||||
|
@ -87,7 +61,6 @@ object EpisodesPermutors {
|
|||
return permutor!!
|
||||
}
|
||||
|
||||
// Null-safe accessors
|
||||
private fun pubDate(item: Episode?): Date {
|
||||
return if (item == null) Date() else Date(item.pubDate)
|
||||
}
|
||||
|
@ -154,9 +127,9 @@ object EpisodesPermutors {
|
|||
}
|
||||
|
||||
// Sort each individual list by PubDate (ascending/descending)
|
||||
val itemComparator: Comparator<Episode> = 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 }
|
||||
val itemComparator: Comparator<Episode> =
|
||||
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 }
|
||||
|
||||
val feeds: MutableList<List<Episode>> = ArrayList()
|
||||
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