6.5.5 commit

This commit is contained in:
Xilin Jia 2024-09-05 22:46:39 +01:00
parent 782c582db6
commit c2977301f6
90 changed files with 814 additions and 1105 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests" testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020238 versionCode 3020239
versionName "6.5.4" versionName "6.5.5"
applicationId "ac.mdiq.podcini.R" applicationId "ac.mdiq.podcini.R"
def commit = "" def commit = ""
@ -172,36 +172,37 @@ android {
dependencies { dependencies {
/** Desugaring for using VistaGuide **/ /** Desugaring for using VistaGuide **/
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
def composeBom = platform('androidx.compose:compose-bom:2024.08.00') def composeBom = platform('androidx.compose:compose-bom:2024.09.00')
implementation composeBom implementation composeBom
androidTestImplementation composeBom androidTestImplementation composeBom
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6' implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
// implementation 'androidx.compose.material3:material3:1.2.0' // implementation 'androidx.compose.material3:material3:1.2.0'
implementation 'androidx.compose.material:material:1.6.8' implementation 'androidx.compose.material:material:1.7.0'
// implementation 'androidx.compose.foundation:foundation:1.6.2' // implementation 'androidx.compose.foundation:foundation:1.6.2'
implementation 'androidx.compose.ui:ui-tooling-preview:1.6.8' implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.8' debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
// Optional - Add full set of material icons // Optional - Add full set of material icons
// implementation 'androidx.compose.material:material-icons-extended' // implementation 'androidx.compose.material:material-icons-extended'
// Optional - Add window size utils // Optional - Add window size utils
// implementation 'androidx.compose.material3:material3-window-size-class' // implementation 'androidx.compose.material3:material3-window-size-class'
implementation 'androidx.activity:activity-compose:1.9.1' implementation 'androidx.activity:activity-compose:1.9.2'
implementation 'androidx.window:window:1.3.0'
implementation "androidx.core:core-ktx:1.13.1" implementation "androidx.core:core-ktx:1.13.1"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4" implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5"
implementation "androidx.annotation:annotation:1.8.2" implementation "androidx.annotation:annotation:1.8.2"
implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation "androidx.fragment:fragment-ktx:1.8.2" implementation "androidx.fragment:fragment-ktx:1.8.3"
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
// implementation "androidx.media:media:1.7.0" // implementation "androidx.media:media:1.7.0"
implementation "androidx.media3:media3-exoplayer:1.4.1" implementation "androidx.media3:media3-exoplayer:1.4.1"

View File

@ -3,7 +3,7 @@ package de.test.podcini.service.download
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.Downloader import ac.mdiq.podcini.net.download.service.Downloader
import ac.mdiq.podcini.net.download.service.HttpDownloader import ac.mdiq.podcini.net.download.service.HttpDownloader
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.download.service.DownloadRequest
import ac.mdiq.podcini.preferences.UserPreferences.init import ac.mdiq.podcini.preferences.UserPreferences.init
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.net.download.service
import android.util.Log import android.util.Log
import android.webkit.URLUtil import android.webkit.URLUtil
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
class DefaultDownloaderFactory : DownloaderFactory { class DefaultDownloaderFactory : DownloaderFactory {
override fun create(request: DownloadRequest): Downloader? { override fun create(request: DownloadRequest): Downloader? {

View File

@ -1,4 +1,4 @@
package ac.mdiq.podcini.net.download.serviceinterface package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
@ -7,7 +7,6 @@ import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
class DownloadRequest private constructor( class DownloadRequest private constructor(
@JvmField val destination: String?, @JvmField val destination: String?,
@JvmField val source: String?, @JvmField val source: String?,

View File

@ -1,6 +1,5 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath

View File

@ -1,4 +1,4 @@
package ac.mdiq.podcini.net.download.serviceinterface package ac.mdiq.podcini.net.download.service
import android.content.Context import android.content.Context
import ac.mdiq.podcini.net.download.DownloadStatus import ac.mdiq.podcini.net.download.DownloadStatus

View File

@ -3,8 +3,6 @@ package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.storage.model.DownloadResult import ac.mdiq.podcini.storage.model.DownloadResult
import ac.mdiq.podcini.util.config.ClientConfig import ac.mdiq.podcini.util.config.ClientConfig
import android.content.Context import android.content.Context

View File

@ -1,7 +1,5 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
interface DownloaderFactory { interface DownloaderFactory {
fun create(request: DownloadRequest): Downloader? fun create(request: DownloadRequest): Downloader?
} }

View File

@ -4,7 +4,6 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils.parse import ac.mdiq.podcini.net.feed.parser.utils.DateUtils.parse
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.storage.model.DownloadResult import ac.mdiq.podcini.storage.model.DownloadResult
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.net.download.service package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder.encode import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder.encode
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.utils.URIUtil import ac.mdiq.podcini.net.utils.URIUtil
import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit
import javax.net.ssl.* import javax.net.ssl.*
import kotlin.concurrent.Volatile import kotlin.concurrent.Volatile
/** /**
* Provides access to a HttpClient singleton. * Provides access to a HttpClient singleton.
*/ */

View File

@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.DownloadError import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest import ac.mdiq.podcini.net.download.service.DownloadRequest
import ac.mdiq.podcini.net.feed.parser.FeedHandler import ac.mdiq.podcini.net.feed.parser.FeedHandler
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh

View File

@ -85,20 +85,16 @@ object LocalFeedUpdater {
oldItem?.updateFromOther(newItem) ?: newItems.add(newItem) oldItem?.updateFromOther(newItem) ?: newItems.add(newItem)
updaterProgressListener?.onLocalFileScanned(i, mediaFiles.size) updaterProgressListener?.onLocalFileScanned(i, mediaFiles.size)
} }
// remove feed items without corresponding file // remove feed items without corresponding file
val it = newItems.iterator() val it = newItems.iterator()
while (it.hasNext()) { while (it.hasNext()) {
val feedItem = it.next() val feedItem = it.next()
if (!mediaFileNames.contains(feedItem.link)) it.remove() if (!mediaFileNames.contains(feedItem.link)) it.remove()
} }
if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri) if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri)
if (feed.preferences != null) feed.preferences!!.autoDownload = false if (feed.preferences != null) feed.preferences!!.autoDownload = false
feed.description = context.getString(R.string.local_feed_description) feed.description = context.getString(R.string.local_feed_description)
feed.author = context.getString(R.string.local_folder) feed.author = context.getString(R.string.local_folder)
Feeds.updateFeed(context, feed, true) Feeds.updateFeed(context, feed, true)
} }
@ -112,13 +108,11 @@ object LocalFeedUpdater {
if (iconLocation == file.name) return file.uri.toString() if (iconLocation == file.name) return file.uri.toString()
} }
} }
// use the first image in the folder if existing // use the first image in the folder if existing
for (file in files) { for (file in files) {
val mime = file.type val mime = file.type
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString() if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString()
} }
// use default icon as fallback // use default icon as fallback
return Feed.PREFIX_GENERATIVE_COVER + folderUri return Feed.PREFIX_GENERATIVE_COVER + folderUri
} }
@ -134,12 +128,10 @@ object LocalFeedUpdater {
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode { private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed) val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
item.disableAutoDownload() item.disableAutoDownload()
val size = file.length val size = file.length
val media = EpisodeMedia(0, item, 0, 0, size, file.type, val media = EpisodeMedia(0, item, 0, 0, size, file.type,
file.uri.toString(), file.uri.toString(), false, null, 0, 0) file.uri.toString(), file.uri.toString(), false, null, 0, 0)
item.media = media item.media = media
for (existingItem in feed.episodes) { for (existingItem in feed.episodes) {
if (existingItem.media != null && existingItem.media!!.downloadUrl == file.uri.toString() if (existingItem.media != null && existingItem.media!!.downloadUrl == file.uri.toString()
&& file.length == existingItem.media!!.size) { && file.length == existingItem.media!!.size) {
@ -148,13 +140,8 @@ object LocalFeedUpdater {
return item return item
} }
} }
// Did not find existing item. Scan metadata. // Did not find existing item. Scan metadata.
try { try { loadMetadata(item, file, context) } catch (e: Exception) { item.setDescriptionIfLonger(e.message) }
loadMetadata(item, file, context)
} catch (e: Exception) {
item.setDescriptionIfLonger(e.message)
}
return item return item
} }
@ -165,19 +152,16 @@ object LocalFeedUpdater {
if (!dateStr.isNullOrEmpty() && "19040101T000000.000Z" != dateStr) { if (!dateStr.isNullOrEmpty() && "19040101T000000.000Z" != dateStr) {
try { try {
val simpleDateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()) val simpleDateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault())
item.pubDate = simpleDateFormat.parse(dateStr).time item.pubDate = simpleDateFormat.parse(dateStr)?.time ?: 0L
} catch (parseException: ParseException) { } catch (parseException: ParseException) {
val date = DateUtils.parse(dateStr) val date = DateUtils.parse(dateStr)
if (date != null) item.pubDate = date.time if (date != null) item.pubDate = date.time
} }
} }
val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
if (!title.isNullOrEmpty()) item.title = title if (!title.isNullOrEmpty()) item.title = title
val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
item.media!!.setDuration(durationStr!!.toLong().toInt()) item.media!!.setDuration(durationStr!!.toLong().toInt())
item.media!!.hasEmbeddedPicture = (mediaMetadataRetriever.embeddedPicture != null) item.media!!.hasEmbeddedPicture = (mediaMetadataRetriever.embeddedPicture != null)
try { try {
context.contentResolver.openInputStream(file.uri).use { inputStream -> context.contentResolver.openInputStream(file.uri).use { inputStream ->
@ -188,30 +172,23 @@ object LocalFeedUpdater {
} catch (e: IOException) { } catch (e: IOException) {
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message) Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
try { try {
context.contentResolver.openInputStream(file.uri).use { inputStream -> context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
val reader = VorbisCommentMetadataReader(inputStream) val reader = VorbisCommentMetadataReader(inputStream)
reader.readInputStream() reader.readInputStream()
item.setDescriptionIfLonger(reader.description) item.setDescriptionIfLonger(reader.description)
} }
} catch (e2: IOException) { } catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) } catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
} catch (e2: VorbisCommentReaderException) {
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
}
} catch (e: ID3ReaderException) { } catch (e: ID3ReaderException) {
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message) Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
try { try {
context.contentResolver.openInputStream(file.uri).use { inputStream -> context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
val reader = VorbisCommentMetadataReader(inputStream) val reader = VorbisCommentMetadataReader(inputStream)
reader.readInputStream() reader.readInputStream()
item.setDescriptionIfLonger(reader.description) item.setDescriptionIfLonger(reader.description)
} }
} catch (e2: IOException) { } catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) } catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
} catch (e2: VorbisCommentReaderException) {
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
}
} }
} }
} }
@ -236,16 +213,12 @@ object LocalFeedUpdater {
*/ */
private fun mustReportDownloadSuccessful(feed: Feed): Boolean { private fun mustReportDownloadSuccessful(feed: Feed): Boolean {
val downloadResults = LogsAndStats.getFeedDownloadLog(feed.id).toMutableList() val downloadResults = LogsAndStats.getFeedDownloadLog(feed.id).toMutableList()
// report success if never reported before // report success if never reported before
if (downloadResults.isEmpty()) return true if (downloadResults.isEmpty()) return true
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult -> downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate()) downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())
} }
val lastDownloadResult = downloadResults[downloadResults.size - 1] val lastDownloadResult = downloadResults[downloadResults.size - 1]
// report success if the last update was not successful // report success if the last update was not successful
// (avoid logging success again if the last update was ok) // (avoid logging success again if the last update was ok)
return !lastDownloadResult.isSuccessful return !lastDownloadResult.isSuccessful

View File

@ -26,8 +26,7 @@ class ItunesTopListLoader(private val context: Context) {
var loadCountry = country var loadCountry = country
if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country
feedString = try { feedString = try { getTopListFeed(client, loadCountry)
getTopListFeed(client, loadCountry)
} catch (e: IOException) { } catch (e: IOException) {
if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US") if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US")
else throw e else throw e
@ -60,9 +59,7 @@ class ItunesTopListLoader(private val context: Context) {
try { try {
feed = result.getJSONObject("feed") feed = result.getJSONObject("feed")
entries = feed.getJSONArray("entry") entries = feed.getJSONArray("entry")
} catch (e: JSONException) { } catch (e: JSONException) { return ArrayList() }
return ArrayList()
}
val results: MutableList<PodcastSearchResult> = ArrayList() val results: MutableList<PodcastSearchResult> = ArrayList()
for (i in 0 until entries.length()) { for (i in 0 until entries.length()) {
@ -90,9 +87,8 @@ class ItunesTopListLoader(private val context: Context) {
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> { private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
val subscribedPodcastsSet: MutableSet<String> = HashSet() val subscribedPodcastsSet: MutableSet<String> = HashSet()
for (subscribedFeed in subscribedFeeds) { for (subscribedFeed in subscribedFeeds) {
if (subscribedFeed.title != null && subscribedFeed.author != null) { if (subscribedFeed.title != null && subscribedFeed.author != null)
subscribedPodcastsSet.add(subscribedFeed.title!!.trim { it <= ' ' } + " - " + subscribedFeed.author!!.trim { it <= ' ' }) subscribedPodcastsSet.add(subscribedFeed.title!!.trim { it <= ' ' } + " - " + subscribedFeed.author!!.trim { it <= ' ' })
}
} }
val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList() val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList()
for (suggested in suggestedPodcasts) { for (suggested in suggestedPodcasts) {

View File

@ -20,19 +20,6 @@ import javax.xml.parsers.ParserConfigurationException
import javax.xml.parsers.SAXParserFactory import javax.xml.parsers.SAXParserFactory
class FeedHandler { class FeedHandler {
enum class Type {
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
companion object {
fun fromName(name: String): Type {
for (t in entries) {
if (t.name == name) return t
}
return INVALID
}
}
}
@Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class) @Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class)
fun parseFeed(feed: Feed): FeedHandlerResult { fun parseFeed(feed: Feed): FeedHandlerResult {
// val tg = TypeGetter() // val tg = TypeGetter()
@ -157,6 +144,19 @@ class FeedHandler {
return reader return reader
} }
enum class Type {
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
companion object {
fun fromName(name: String): Type {
for (t in entries) {
if (t.name == name) return t
}
return INVALID
}
}
}
/** Superclass for all SAX Handlers which process Syndication formats */ /** Superclass for all SAX Handlers which process Syndication formats */
class SyndHandler(feed: Feed, type: Type) : DefaultHandler() { class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
@JvmField @JvmField

View File

@ -5,4 +5,7 @@ import ac.mdiq.podcini.storage.model.Feed
/** /**
* Container for results returned by the Feed parser * Container for results returned by the Feed parser
*/ */
class FeedHandlerResult(@JvmField val feed: Feed, @JvmField val alternateFeedUrls: Map<String, String>, val redirectUrl: String) class FeedHandlerResult(
@JvmField val feed: Feed,
@JvmField val alternateFeedUrls: Map<String, String>,
val redirectUrl: String)

View File

@ -4,7 +4,10 @@ import androidx.core.text.HtmlCompat
import ac.mdiq.podcini.net.feed.parser.namespace.Namespace import ac.mdiq.podcini.net.feed.parser.namespace.Namespace
/** Represents Atom Element which contains text (content, title, summary). */ /** Represents Atom Element which contains text (content, title, summary). */
class AtomText(name: String?, namespace: Namespace?, private val type: String?) : SyndElement(name!!, namespace!!) { class AtomText(
name: String,
namespace: Namespace,
private val type: String?) : SyndElement(name, namespace) {
private var content: String? = null private var content: String? = null

View File

@ -13,7 +13,7 @@ import java.net.URLDecoder
* Reads ID3 chapters. * Reads ID3 chapters.
* See https://id3.org/id3v2-chapters-1.0 * See https://id3.org/id3v2-chapters-1.0
*/ */
class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) { class ChapterReader(input: CountingInputStream) : ID3Reader(input) {
private val chapters: MutableList<Chapter> = ArrayList() private val chapters: MutableList<Chapter> = ArrayList()
@Throws(IOException::class, ID3ReaderException::class) @Throws(IOException::class, ID3ReaderException::class)
@ -23,9 +23,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
val chapter = readChapter(frameHeader) val chapter = readChapter(frameHeader)
Logd(TAG, "Chapter done: $chapter") Logd(TAG, "Chapter done: $chapter")
chapters.add(chapter) chapters.add(chapter)
} else { } else super.readFrame(frameHeader)
super.readFrame(frameHeader)
}
} }
@Throws(IOException::class, ID3ReaderException::class) @Throws(IOException::class, ID3ReaderException::class)
@ -63,9 +61,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
val decodedLink = URLDecoder.decode(url, "ISO-8859-1") val decodedLink = URLDecoder.decode(url, "ISO-8859-1")
chapter.link = decodedLink chapter.link = decodedLink
Logd(TAG, "Found link: " + chapter.link) Logd(TAG, "Found link: " + chapter.link)
} catch (iae: IllegalArgumentException) { } catch (iae: IllegalArgumentException) { Log.w(TAG, "Bad URL found in ID3 data") }
Log.w(TAG, "Bad URL found in ID3 data")
}
} }
FRAME_ID_PICTURE -> { FRAME_ID_PICTURE -> {
val encoding = readByte() val encoding = readByte()

View File

@ -17,6 +17,8 @@ import java.nio.charset.MalformedInputException
*/ */
open class ID3Reader(private val inputStream: CountingInputStream) { open class ID3Reader(private val inputStream: CountingInputStream) {
private var tagHeader: TagHeader? = null private var tagHeader: TagHeader? = null
val position: Int
get() = inputStream.count
@Throws(IOException::class, ID3ReaderException::class) @Throws(IOException::class, ID3ReaderException::class)
fun readInputStream() { fun readInputStream() {
@ -38,16 +40,12 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
skipBytes(frameHeader.size) skipBytes(frameHeader.size)
} }
val position: Int
get() = inputStream.count
/** /**
* Skip a certain number of bytes on the given input stream. * Skip a certain number of bytes on the given input stream.
*/ */
@Throws(IOException::class, ID3ReaderException::class) @Throws(IOException::class, ID3ReaderException::class)
fun skipBytes(number: Int) { fun skipBytes(number: Int) {
if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes") if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes")
IOUtils.skipFully(inputStream, number.toLong()) IOUtils.skipFully(inputStream, number.toLong())
} }
@ -98,7 +96,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
val id = readPlainBytesToString(FRAME_ID_LENGTH) val id = readPlainBytesToString(FRAME_ID_LENGTH)
var size = readInt() var size = readInt()
if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size) if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size)
val flags = readShort() val flags = readShort()
return FrameHeader(id, size, flags) return FrameHeader(id, size, flags)
} }
@ -106,13 +103,11 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
private fun unsynchsafe(inVal: Int): Int { private fun unsynchsafe(inVal: Int): Int {
var out = 0 var out = 0
var mask = 0x7F000000 var mask = 0x7F000000
while (mask != 0) { while (mask != 0) {
out = out shr 1 out = out shr 1
out = out or (inVal and mask) out = out or (inVal and mask)
mask = mask shr 8 mask = mask shr 8
} }
return out return out
} }
@ -161,7 +156,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
val c = readByte() val c = readByte()
bytesRead++ bytesRead++
if (c.toInt() == 0) break if (c.toInt() == 0) break
bytes.write(c.toInt()) bytes.write(c.toInt())
} }
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString() return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
@ -191,11 +185,7 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
val c = readByte() val c = readByte()
if (c.toInt() != 0) bytes.write(c.toInt()) if (c.toInt() != 0) bytes.write(c.toInt())
} }
return try { return try { charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString() } catch (e: MalformedInputException) { "" }
charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
} catch (e: MalformedInputException) {
""
}
} }
companion object { companion object {

View File

@ -7,7 +7,7 @@ import java.io.IOException
/** /**
* Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support. * Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support.
*/ */
class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) { class Id3MetadataReader(input: CountingInputStream) : ID3Reader(input) {
var comment: String? = null var comment: String? = null
private set private set
@ -20,9 +20,7 @@ class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
val shortDescription = readEncodedString(encoding, frameHeader.size - 4) val shortDescription = readEncodedString(encoding, frameHeader.size - 4)
val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt()) val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt())
comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription
} else { } else super.readFrame(frameHeader)
super.readFrame(frameHeader)
}
} }
companion object { companion object {

View File

@ -1,3 +1,6 @@
package ac.mdiq.podcini.net.feed.parser.media.id3.model package ac.mdiq.podcini.net.feed.parser.media.id3.model
class FrameHeader(id: String?, size: Int, flags: Short) : Header(id!!, size) class FrameHeader(
id: String,
size: Int,
flags: Short) : Header(id, size)

View File

@ -1,6 +1,9 @@
package ac.mdiq.podcini.net.feed.parser.media.id3.model package ac.mdiq.podcini.net.feed.parser.media.id3.model
abstract class Header internal constructor(@JvmField val id: String, @JvmField val size: Int) { abstract class Header internal constructor(
@JvmField val id: String,
@JvmField val size: Int) {
override fun toString(): String { override fun toString(): String {
return "Header [id=$id, size=$size]" return "Header [id=$id, size=$size]"
} }

View File

@ -1,6 +1,11 @@
package ac.mdiq.podcini.net.feed.parser.media.id3.model package ac.mdiq.podcini.net.feed.parser.media.id3.model
class TagHeader(id: String?, size: Int, @JvmField val version: Short, private val flags: Byte) : Header(id!!, size) { class TagHeader(
id: String,
size: Int,
@JvmField val version: Short,
private val flags: Byte) : Header(id, size) {
override fun toString(): String { override fun toString(): String {
return ("TagHeader [version=" + version + ", flags=" + flags + ", id=" + id + ", size=" + size + "]") return ("TagHeader [version=" + version + ", flags=" + flags + ", id=" + id + ", size=" + size + "]")
} }

View File

@ -5,7 +5,7 @@ import ac.mdiq.podcini.util.Logd
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(input!!) { class VorbisCommentChapterReader(input: InputStream) : VorbisCommentReader(input) {
private val chapters: MutableList<Chapter> = ArrayList() private val chapters: MutableList<Chapter> = ArrayList()
public override fun handles(key: String?): Boolean { public override fun handles(key: String?): Boolean {
@ -59,19 +59,13 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size >= 3) { if (parts.size >= 3) {
try { try {
val hours = TimeUnit.MILLISECONDS.convert( val hours = TimeUnit.MILLISECONDS.convert(parts[0].toLong(), TimeUnit.HOURS)
parts[0].toLong(), TimeUnit.HOURS) val minutes = TimeUnit.MILLISECONDS.convert(parts[1].toLong(), TimeUnit.MINUTES)
val minutes = TimeUnit.MILLISECONDS.convert(
parts[1].toLong(), TimeUnit.MINUTES)
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->")) if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS) val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
return hours + minutes + seconds return hours + minutes + seconds
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
throw VorbisCommentReaderException(e) } else throw VorbisCommentReaderException("Invalid time string")
}
} else {
throw VorbisCommentReaderException("Invalid time string")
}
} }
/** /**
@ -86,9 +80,7 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
try { try {
val strId = key.substring(8, 10) val strId = key.substring(8, 10)
return strId.toInt() return strId.toInt()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
throw VorbisCommentReaderException(e)
}
} }
throw VorbisCommentReaderException("key is too short ($key)") throw VorbisCommentReaderException("key is too short ($key)")
} }
@ -99,7 +91,6 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
*/ */
private fun getAttributeTypeFromKey(key: String?): String? { private fun getAttributeTypeFromKey(key: String?): String? {
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH) if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
return null return null
} }
} }

View File

@ -1,7 +0,0 @@
package ac.mdiq.podcini.net.feed.parser.media.vorbis
internal class VorbisCommentHeader(val vendorString: String, val userCommentLength: Long) {
override fun toString(): String {
return ("VorbisCommentHeader [vendorString=" + vendorString + ", userCommentLength=" + userCommentLength + "]")
}
}

View File

@ -2,7 +2,7 @@ package ac.mdiq.podcini.net.feed.parser.media.vorbis
import java.io.InputStream import java.io.InputStream
class VorbisCommentMetadataReader(input: InputStream?) : VorbisCommentReader(input!!) { class VorbisCommentMetadataReader(input: InputStream) : VorbisCommentReader(input!!) {
var description: String? = null var description: String? = null
private set private set

View File

@ -171,6 +171,15 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
@Throws(VorbisCommentReaderException::class) @Throws(VorbisCommentReaderException::class)
protected abstract fun onContentVectorValue(key: String?, value: String?) protected abstract fun onContentVectorValue(key: String?, value: String?)
internal class VorbisCommentHeader(
val vendorString: String,
val userCommentLength: Long) {
override fun toString(): String {
return ("VorbisCommentHeader [vendorString=" + vendorString + ", userCommentLength=" + userCommentLength + "]")
}
}
companion object { companion object {
private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous" private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous"
private const val FIRST_OGG_PAGE_LENGTH = 58 private const val FIRST_OGG_PAGE_LENGTH = 58

View File

@ -53,8 +53,7 @@ class Atom : Namespace() {
// a) no type-attribute is given and feed-object has no link yet // a) no type-attribute is given and feed-object has no link yet
// b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML // b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
when { when {
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> state.feed.link = href
state.feed.link = href
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> { LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
// treat as podlove alternate feed // treat as podlove alternate feed
var title: String? = attributes.getValue(LINK_TITLE) var title: String? = attributes.getValue(LINK_TITLE)
@ -71,9 +70,8 @@ class Atom : Namespace() {
if (title.isNullOrEmpty()) title = href?:"" if (title.isNullOrEmpty()) title = href?:""
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href) if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
} }
LINK_TYPE_HTML, LINK_TYPE_XHTML -> { //A Link such as to a directory such as iTunes
//A Link such as to a directory such as iTunes LINK_TYPE_HTML, LINK_TYPE_XHTML -> {}
}
} }
} }
LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, "")) LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))

View File

@ -10,9 +10,8 @@ class Content : Namespace() {
} }
override fun handleElementEnd(localName: String, state: HandlerState) { override fun handleElementEnd(localName: String, state: HandlerState) {
if (ENCODED == localName && state.currentItem != null && state.contentBuf != null) { if (ENCODED == localName && state.contentBuf != null)
state.currentItem!!.setDescriptionIfLonger(state.contentBuf.toString()) state.currentItem?.setDescriptionIfLonger(state.contentBuf.toString())
}
} }
companion object { companion object {

View File

@ -8,24 +8,19 @@ import ac.mdiq.podcini.net.feed.parser.utils.DurationParser.inMillis
import org.xml.sax.Attributes import org.xml.sax.Attributes
class Itunes : Namespace() { class Itunes : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
attributes: Attributes): SyndElement {
if (IMAGE == localName) { if (IMAGE == localName) {
val url: String? = attributes.getValue(IMAGE_HREF) val url: String? = attributes.getValue(IMAGE_HREF)
if (state.currentItem != null) state.currentItem!!.imageUrl = url if (state.currentItem != null) state.currentItem!!.imageUrl = url
else { // this is the feed image
// this is the feed image // prefer to all other images
// prefer to all other images else if (!url.isNullOrEmpty()) state.feed.imageUrl = url
if (!url.isNullOrEmpty()) state.feed.imageUrl = url
}
} }
return SyndElement(localName, this) return SyndElement(localName, this)
} }
override fun handleElementEnd(localName: String, state: HandlerState) { override fun handleElementEnd(localName: String, state: HandlerState) {
if (state.contentBuf == null) return if (state.contentBuf == null) return
val content = state.contentBuf.toString() val content = state.contentBuf.toString()
if (content.isEmpty()) return if (content.isEmpty()) return
@ -38,9 +33,7 @@ class Itunes : Namespace() {
try { try {
val durationMs = inMillis(content) val durationMs = inMillis(content)
state.tempObjects[DURATION] = durationMs.toInt() state.tempObjects[DURATION] = durationMs.toInt()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
}
} }
SUBTITLE == localName -> { SUBTITLE == localName -> {
when { when {

View File

@ -53,11 +53,7 @@ class Media : Namespace() {
var size: Long = 0 var size: Long = 0
val sizeStr: String? = attributes.getValue(SIZE) val sizeStr: String? = attributes.getValue(SIZE)
if (!sizeStr.isNullOrEmpty()) { if (!sizeStr.isNullOrEmpty()) {
try { try { size = sizeStr.toLong() } catch (e: NumberFormatException) { Log.e(TAG, "Size \"$sizeStr\" could not be parsed.") }
size = sizeStr.toLong()
} catch (e: NumberFormatException) {
Log.e(TAG, "Size \"$sizeStr\" could not be parsed.")
}
} }
var durationMs = 0 var durationMs = 0
val durationStr: String? = attributes.getValue(DURATION) val durationStr: String? = attributes.getValue(DURATION)
@ -65,19 +61,14 @@ class Media : Namespace() {
try { try {
val duration = durationStr.toLong() val duration = durationStr.toLong()
durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt() durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { Log.e(TAG, "Duration \"$durationStr\" could not be parsed") }
Log.e(TAG, "Duration \"$durationStr\" could not be parsed")
}
} }
Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType") Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
val media = EpisodeMedia(state.currentItem, url, size, mimeType) val media = EpisodeMedia(state.currentItem, url, size, mimeType)
if (durationMs > 0) media.setDuration( durationMs) if (durationMs > 0) media.setDuration( durationMs)
state.currentItem!!.media = media state.currentItem!!.media = media
} }
state.currentItem != null && url != null && validTypeImage -> { state.currentItem != null && url != null && validTypeImage -> state.currentItem!!.imageUrl = url
state.currentItem!!.imageUrl = url
}
} }
} }
IMAGE -> { IMAGE -> {

View File

@ -6,8 +6,7 @@ import ac.mdiq.podcini.net.feed.parser.element.SyndElement
import org.xml.sax.Attributes import org.xml.sax.Attributes
class PodcastIndex : Namespace() { class PodcastIndex : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
attributes: Attributes): SyndElement {
when (localName) { when (localName) {
FUNDING -> { FUNDING -> {
val href: String? = attributes.getValue(URL) val href: String? = attributes.getValue(URL)
@ -25,7 +24,6 @@ class PodcastIndex : Namespace() {
override fun handleElementEnd(localName: String, state: HandlerState) { override fun handleElementEnd(localName: String, state: HandlerState) {
if (state.contentBuf == null) return if (state.contentBuf == null) return
val content = state.contentBuf.toString() val content = state.contentBuf.toString()
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content) if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content)
} }

View File

@ -22,9 +22,7 @@ class SimpleChapters : Namespace() {
val imageUrl: String? = attributes.getValue(IMAGE) val imageUrl: String? = attributes.getValue(IMAGE)
val chapter = Chapter(start, title, link, imageUrl) val chapter = Chapter(start, title, link, imageUrl)
currentItem.chapters?.add(chapter) currentItem.chapters?.add(chapter)
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { Log.e(TAG, "Unable to read chapter", e) }
Log.e(TAG, "Unable to read chapter", e)
}
} }
} }
} }

View File

@ -36,9 +36,7 @@ class YouTube : Namespace() {
try { try {
val durationMs = inMillis(content) val durationMs = inMillis(content)
state.tempObjects[DURATION] = durationMs.toInt() state.tempObjects[DURATION] = durationMs.toInt()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
}
} }
SUBTITLE == localName -> { SUBTITLE == localName -> {
when { when {

View File

@ -8,7 +8,6 @@ import java.text.ParsePosition
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
/** /**
* Parses several date formats. * Parses several date formats.
*/ */

View File

@ -25,8 +25,6 @@ object DurationParser {
val value = seconds.toFloat() val value = seconds.toFloat()
val millis = value % 1 val millis = value % 1
return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong() return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong()
} else { } else return TimeUnit.SECONDS.toMillis(seconds.toLong())
return TimeUnit.SECONDS.toMillis(seconds.toLong())
}
} }
} }

View File

@ -22,10 +22,8 @@ object MimeTypeUtils {
@JvmStatic @JvmStatic
fun getMimeType(type: String?, filename: String?): String? { fun getMimeType(type: String?, filename: String?): String? {
if (isMediaFile(type) && OCTET_STREAM != type) return type if (isMediaFile(type) && OCTET_STREAM != type) return type
val filenameType = getMimeTypeFromUrl(filename) val filenameType = getMimeTypeFromUrl(filename)
if (isMediaFile(filenameType)) return filenameType if (isMediaFile(filenameType)) return filenameType
return type return type
} }

View File

@ -14,14 +14,10 @@ import java.util.regex.Pattern
* This class is based on `HtmlToPlainText` from jsoup's examples package. * This class is based on `HtmlToPlainText` from jsoup's examples package.
* *
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted * HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a * plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a scrape.
* scrape.
*
* *
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend. * Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
* *
*
*
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory: * To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:
* *
* `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]` * `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]`
@ -40,7 +36,6 @@ class HtmlToPlainText {
val formatter = FormattingVisitor() val formatter = FormattingVisitor()
// walk the DOM, and call .head() and .tail() for each node // walk the DOM, and call .head() and .tail() for each node
NodeTraversor.traverse(formatter, element) NodeTraversor.traverse(formatter, element)
return formatter.toString() return formatter.toString()
} }
@ -72,7 +67,6 @@ class HtmlToPlainText {
private fun append(text: String) { private fun append(text: String) {
if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n"))) if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n")))
return // don't accumulate long runs of empty spaces return // don't accumulate long runs of empty spaces
accum.append(text) accum.append(text)
} }

View File

@ -15,11 +15,7 @@ object URIUtil {
@JvmStatic @JvmStatic
fun getURIFromRequestUrl(source: String): URI { fun getURIFromRequestUrl(source: String): URI {
// try without encoding the URI // try without encoding the URI
try { try { return URI(source) } catch (e: URISyntaxException) { Logd(TAG, "Source is not encoded, encoding now") }
return URI(source)
} catch (e: URISyntaxException) {
Logd(TAG, "Source is not encoded, encoding now")
}
try { try {
val url = URL(source) val url = URL(source)
return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref) return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)

View File

@ -46,9 +46,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END)) if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
if (media is EpisodeMedia) { if (media is EpisodeMedia) {
curMedia = media curMedia = media
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
curEpisode = media.episodeOrFetch() curEpisode = media.episodeOrFetch()
// curMedia = curEpisode?.media
} else curMedia = media } else curMedia = media
if (PlaybackService.isRunning && !callEvenIfRunning) return if (PlaybackService.isRunning && !callEvenIfRunning) return

View File

@ -50,6 +50,20 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
*/ */
private var wifiLock: WifiLock? = null private var wifiLock: WifiLock? = null
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
* @return The PSMPInfo object.
*/
@get:Synchronized
val playerInfo: MediaPlayerInfo
get() = MediaPlayerInfo(oldStatus, status, curMedia)
val isAudioChannelInUse: Boolean
get() {
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
}
init { init {
status = PlayerStatus.STOPPED status = PlayerStatus.STOPPED
} }
@ -185,14 +199,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player") Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player")
} }
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
* @return The PSMPInfo object.
*/
@get:Synchronized
val playerInfo: MediaPlayerInfo
get() = MediaPlayerInfo(oldStatus, status, curMedia)
open fun setAudioTrack(track: Int) {} open fun setAudioTrack(track: Int) {}
fun skip() { fun skip() {
@ -276,13 +282,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia)) callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
} }
val isAudioChannelInUse: Boolean class MediaPlayerInfo(
get() { @JvmField val oldPlayerStatus: PlayerStatus?,
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager @JvmField var playerStatus: PlayerStatus,
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive) @JvmField var playable: Playable?)
}
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
companion object { companion object {
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous" private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
@ -308,8 +311,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
val audioPlaybackSpeed: Float val audioPlaybackSpeed: Float
get() { get() {
try { try { return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
} catch (e: NumberFormatException) { } catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e)) Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f) setPlaybackSpeed(1.0f)

View File

@ -495,7 +495,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
override fun getPosition(): Int { override fun getPosition(): Int {
var retVal = Playable.INVALID_TIME var retVal = Playable.INVALID_TIME
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt() if (exoPlayer != null && status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition() if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
return retVal return retVal
} }

View File

@ -1,6 +1,6 @@
package ac.mdiq.podcini.receiver package ac.mdiq.podcini.receiver
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
@ -18,7 +18,6 @@ class ConnectivityActionReceiver : BroadcastReceiver() {
Log.d(TAG, "onReceive called with action: ${intent.action}") Log.d(TAG, "onReceive called with action: ${intent.action}")
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) { if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
Logd(TAG, "Received intent") Logd(TAG, "Received intent")
ClientConfigurator.initialize(context) ClientConfigurator.initialize(context)
networkChangedDetected(context) networkChangedDetected(context)
} }

View File

@ -16,7 +16,6 @@ class FeedUpdateReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
Logd(TAG, "Received intent") Logd(TAG, "Received intent")
ClientConfigurator.initialize(context) ClientConfigurator.initialize(context)
FeedUpdateManager.runOnce(context) FeedUpdateManager.runOnce(context)
} }

View File

@ -22,7 +22,6 @@ class MediaButtonReceiver : BroadcastReceiver() {
val extras = intent.extras val extras = intent.extras
Log.d(TAG, "onReceive Extras: $extras") Log.d(TAG, "onReceive Extras: $extras")
if (extras == null) return if (extras == null) return
Log.d(TAG, "onReceive Extras: ${extras.keySet()}") Log.d(TAG, "onReceive Extras: ${extras.keySet()}")
for (key in extras.keySet()) { for (key in extras.keySet()) {
Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}") Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}")
@ -40,11 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode) serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode)
serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source) serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source)
serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0) serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0)
try { try { ContextCompat.startForegroundService(context, serviceIntent) } catch (e: Exception) { e.printStackTrace() }
ContextCompat.startForegroundService(context, serviceIntent)
} catch (e: Exception) {
e.printStackTrace()
}
} }
} }

View File

@ -26,7 +26,6 @@ class PlayerWidget : AppWidgetProvider() {
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]") Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
getSharedPrefs(context) getSharedPrefs(context)
WidgetUpdaterWorker.enqueueWork(context) WidgetUpdaterWorker.enqueueWork(context)
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) { if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
scheduleWorkaround(context) scheduleWorkaround(context)
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply() prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
@ -75,9 +74,7 @@ class PlayerWidget : AppWidgetProvider() {
companion object { companion object {
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous" private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
private const val PREFS_NAME: String = "PlayerWidgetPrefs" private const val PREFS_NAME: String = "PlayerWidgetPrefs"
const val DEFAULT_COLOR: Int = -0xd9d3cf const val DEFAULT_COLOR: Int = -0xd9d3cf
var prefs: SharedPreferences? = null var prefs: SharedPreferences? = null
fun getSharedPrefs(context: Context) { fun getSharedPrefs(context: Context) {

View File

@ -5,7 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.util.config.ClientConfigurator import ac.mdiq.podcini.util.config.ClientConfigurator
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -20,7 +20,6 @@ class PowerConnectionReceiver : BroadcastReceiver() {
@UnstableApi override fun onReceive(context: Context, intent: Intent) { @UnstableApi override fun onReceive(context: Context, intent: Intent) {
val action = intent.action val action = intent.action
Log.d(TAG, "onReceive charging intent: $action") Log.d(TAG, "onReceive charging intent: $action")
ClientConfigurator.initialize(context) ClientConfigurator.initialize(context)
if (Intent.ACTION_POWER_CONNECTED == action) { if (Intent.ACTION_POWER_CONNECTED == action) {
Logd(TAG, "charging, starting auto-download") Logd(TAG, "charging, starting auto-download")

View File

@ -1,6 +1,6 @@
package ac.mdiq.podcini.storage.algorithms package ac.mdiq.podcini.storage.algorithms
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.storage.database package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
import ac.mdiq.podcini.net.sync.model.EpisodeAction import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink

View File

@ -1,6 +1,6 @@
package ac.mdiq.podcini.storage.database package ac.mdiq.podcini.storage.database
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode

View File

@ -12,7 +12,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) { class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
override val visibility: Int override val visibility: Int

View File

@ -1,12 +1,11 @@
package ac.mdiq.podcini.ui.actions.actionbutton package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView

View File

@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.actions.handler
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.storage.database.Episodes import ac.mdiq.podcini.storage.database.Episodes
import ac.mdiq.podcini.storage.database.Episodes.setPlayState import ac.mdiq.podcini.storage.database.Episodes.setPlayState

View File

@ -4,7 +4,7 @@ import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MainActivityBinding import ac.mdiq.podcini.databinding.MainActivityBinding
import ac.mdiq.podcini.net.download.DownloadStatus import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.net.feed.FeedUpdateManager
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
@ -91,7 +91,6 @@ class MainActivity : CastEnabledActivity() {
private lateinit var navDrawerFragment: NavDrawerFragment private lateinit var navDrawerFragment: NavDrawerFragment
private lateinit var audioPlayerFragment: AudioPlayerFragment private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerView: View private lateinit var audioPlayerView: View
// private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var navDrawer: View private lateinit var navDrawer: View
private lateinit var dummyView : View private lateinit var dummyView : View
lateinit var bottomSheet: LockableBottomSheetBehavior<*> lateinit var bottomSheet: LockableBottomSheetBehavior<*>
@ -104,6 +103,53 @@ class MainActivity : CastEnabledActivity() {
private var lastTheme = 0 private var lastTheme = 0
private var navigationBarInsets = Insets.NONE private var navigationBarInsets = Insets.NONE
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) return@registerForActivityResult
MaterialAlertDialogBuilder(this)
.setMessage(R.string.notification_permission_text)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
.show()
}
private var prevState: Int = 0
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) {
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> {
audioPlayerFragment.onCollaped()
onSlide(view,0.0f)
prevState = state
}
BottomSheetBehavior.STATE_EXPANDED -> {
audioPlayerFragment.onExpanded()
onSlide(view, 1.0f)
prevState = state
}
else -> {}
}
}
override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToTop()
// }
audioPlayer.fadePlayerToToolbar(slideOffset)
}
}
private val isDrawerOpen: Boolean
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
private val screenWidth: Int
get() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
return displayMetrics.widthPixels
}
@UnstableApi public override fun onCreate(savedInstanceState: Bundle?) { @UnstableApi public override fun onCreate(savedInstanceState: Bundle?) {
lastTheme = getNoTitleTheme(this) lastTheme = getNoTitleTheme(this)
setTheme(lastTheme) setTheme(lastTheme)
@ -131,7 +177,7 @@ class MainActivity : CastEnabledActivity() {
PlayerDetailsFragment.getSharedPrefs(this@MainActivity) PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
PlayerWidget.getSharedPrefs(this@MainActivity) PlayerWidget.getSharedPrefs(this@MainActivity)
StatisticsFragment.getSharedPrefs(this@MainActivity) StatisticsFragment.getSharedPrefs(this@MainActivity)
OnlineFeedViewFragment.getSharedPrefs(this@MainActivity) OnlineFeedFragment.getSharedPrefs(this@MainActivity)
ItunesTopListLoader.getSharedPrefs(this@MainActivity) ItunesTopListLoader.getSharedPrefs(this@MainActivity)
} }
@ -155,7 +201,7 @@ class MainActivity : CastEnabledActivity() {
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
// Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show() // Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
// requestPostNotificationPermission() // requestPostNotificationPermission()
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} }
// Consume navigation bar insets - we apply them in setPlayerVisible() // Consume navigation bar insets - we apply them in setPlayerVisible()
@ -213,9 +259,7 @@ class MainActivity : CastEnabledActivity() {
when (workInfo.state) { when (workInfo.state) {
WorkInfo.State.RUNNING -> isRefreshingFeeds = true WorkInfo.State.RUNNING -> isRefreshingFeeds = true
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
else -> { else -> {}
// Log.d(TAG, "workInfo.state ${workInfo.state}")
}
} }
} }
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds)) EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds))
@ -225,9 +269,7 @@ class MainActivity : CastEnabledActivity() {
private fun observeDownloads() { private fun observeDownloads() {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) { WorkManager.getInstance(this@MainActivity).pruneWork().result.get() }
WorkManager.getInstance(this@MainActivity).pruneWork().result.get()
}
WorkManager.getInstance(this@MainActivity) WorkManager.getInstance(this@MainActivity)
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG) .getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
.observe(this@MainActivity) { workInfos: List<WorkInfo> -> .observe(this@MainActivity) { workInfos: List<WorkInfo> ->
@ -267,20 +309,10 @@ class MainActivity : CastEnabledActivity() {
} }
} }
} }
// fun requestPostNotificationPermission() { // fun requestPostNotificationPermission() {
// if (Build.VERSION.SDK_INT >= 33) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) // if (Build.VERSION.SDK_INT >= 33) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
// } // }
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) return@registerForActivityResult
MaterialAlertDialogBuilder(this)
.setMessage(R.string.notification_permission_text)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
.show()
}
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
updateInsets() updateInsets()
@ -303,33 +335,6 @@ class MainActivity : CastEnabledActivity() {
outState.putInt(Extras.generated_view_id.name, View.generateViewId()) outState.putInt(Extras.generated_view_id.name, View.generateViewId())
} }
private var prevState: Int = 0
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) {
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> {
audioPlayerFragment.onCollaped()
onSlide(view,0.0f)
prevState = state
}
BottomSheetBehavior.STATE_EXPANDED -> {
audioPlayerFragment.onExpanded()
onSlide(view, 1.0f)
prevState = state
}
else -> {}
}
}
override fun onSlide(view: View, slideOffset: Float) {
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
// audioPlayer.scrollToTop()
// }
audioPlayer.fadePlayerToToolbar(slideOffset)
}
}
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) { fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow") Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
// Tablet layout does not have a drawer // Tablet layout does not have a drawer
@ -375,9 +380,6 @@ class MainActivity : CastEnabledActivity() {
} }
} }
private val isDrawerOpen: Boolean
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
private fun updateInsets() { private fun updateInsets() {
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE) setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt() val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
@ -507,13 +509,6 @@ class MainActivity : CastEnabledActivity() {
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}") Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
} }
private val screenWidth: Int
get() {
val displayMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(displayMetrics)
return displayMetrics.widthPixels
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) { override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState) super.onRestoreInstanceState(savedInstanceState)
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f) if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
@ -523,21 +518,10 @@ class MainActivity : CastEnabledActivity() {
super.onStart() super.onStart()
procFlowEvents() procFlowEvents()
RatingDialog.init(this) RatingDialog.init(this)
// val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
// controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
// controllerFuture.addListener({
// // Call controllerFuture.get() to retrieve the MediaController.
// // MediaController implements the Player interface, so it can be
// // attached to the PlayerView UI component.
//// playerView.setPlayer(controllerFuture.get())
// val player = controllerFuture.get()
// }, MoreExecutors.directExecutor())
} }
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// MediaController.releaseFuture(controllerFuture)
cancelFlowEvents() cancelFlowEvents()
} }
@ -634,7 +618,7 @@ class MainActivity : CastEnabledActivity() {
} }
intent.hasExtra(Extras.fragment_feed_url.name) -> { intent.hasExtra(Extras.fragment_feed_url.name) -> {
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name) val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl)) if (feedurl != null) loadChildFragment(OnlineFeedFragment.newInstance(feedurl))
} }
intent.hasExtra(Extras.search_string.name) -> { intent.hasExtra(Extras.search_string.name) -> {
val query = intent.getStringExtra(Extras.search_string.name) val query = intent.getStringExtra(Extras.search_string.name)

View File

@ -54,6 +54,28 @@ class OpmlImportActivity : AppCompatActivity() {
private var listAdapter: ArrayAdapter<String>? = null private var listAdapter: ArrayAdapter<String>? = null
private var readElements: ArrayList<OpmlElement>? = null private var readElements: ArrayList<OpmlElement>? = null
private val titleList: List<String>
get() {
val result: MutableList<String> = ArrayList()
if (!readElements.isNullOrEmpty()) {
for (element in readElements!!) {
if (element.text != null) result.add(element.text!!)
}
}
return result
}
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) startImport()
else {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_ask_read_permission)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
.show()
}
}
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) { @UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTheme(this)) setTheme(getTheme(this))
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -135,17 +157,6 @@ class OpmlImportActivity : AppCompatActivity() {
startImport() startImport()
} }
private val titleList: List<String>
get() {
val result: MutableList<String> = ArrayList()
if (!readElements.isNullOrEmpty()) {
for (element in readElements!!) {
if (element.text != null) result.add(element.text!!)
}
}
return result
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
super.onCreateOptionsMenu(menu) super.onCreateOptionsMenu(menu)
val inflater = menuInflater val inflater = menuInflater
@ -186,18 +197,6 @@ class OpmlImportActivity : AppCompatActivity() {
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
} }
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) startImport()
else {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_ask_read_permission)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
.show()
}
}
/** Starts the import process. */ /** Starts the import process. */
private fun startImport() { private fun startImport() {
binding.progressBar.visibility = View.VISIBLE binding.progressBar.visibility = View.VISIBLE

View File

@ -39,6 +39,5 @@ class SplashActivity : Activity() {
finish() finish()
} }
} }
} }
} }

View File

@ -2,36 +2,51 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioControlsBinding import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.ServiceStatusHandler import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
import ac.mdiq.podcini.storage.database.Episodes.setFavorite import ac.mdiq.podcini.storage.database.Episodes.setFavorite
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.ui.dialog.ShareDialog import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog import ac.mdiq.podcini.ui.dialog.*
import ac.mdiq.podcini.ui.fragment.ChaptersFragment import ac.mdiq.podcini.ui.fragment.ChaptersFragment
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.media.AudioManager import android.media.AudioManager
@ -39,17 +54,33 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.util.Pair
import android.view.* import android.view.*
import android.view.MenuItem.SHOW_AS_ACTION_NEVER import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.animation.*
import android.widget.EditText import android.widget.EditText
import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
/** /**
* Activity for playing video files. * Activity for playing video files.
@ -57,18 +88,9 @@ import kotlinx.coroutines.launch
@UnstableApi @UnstableApi
class VideoplayerActivity : CastEnabledActivity() { class VideoplayerActivity : CastEnabledActivity() {
enum class VideoMode(val mode: Int) {
None(0),
WINDOW_VIEW(1),
FULL_SCREEN_VIEW(2),
AUDIO_ONLY(3)
}
private var _binding: VideoplayerActivityBinding? = null private var _binding: VideoplayerActivityBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var videoEpisodeFragment: VideoEpisodeFragment private lateinit var videoEpisodeFragment: VideoEpisodeFragment
var switchToAudioOnly = false var switchToAudioOnly = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -97,11 +119,12 @@ class VideoplayerActivity : CastEnabledActivity() {
val fm = supportFragmentManager val fm = supportFragmentManager
val transaction = fm.beginTransaction() val transaction = fm.beginTransaction()
videoEpisodeFragment = VideoEpisodeFragment() videoEpisodeFragment = VideoEpisodeFragment()
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG) transaction.replace(R.id.main_view, videoEpisodeFragment, "VideoEpisodeFragment")
transaction.commit() transaction.commit()
} }
private fun setForVideoMode() { private fun setForVideoMode() {
Logd(TAG, "setForVideoMode videoMode: $videoMode")
when (videoMode) { when (videoMode) {
VideoMode.FULL_SCREEN_VIEW -> { VideoMode.FULL_SCREEN_VIEW -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
@ -114,7 +137,8 @@ class VideoplayerActivity : CastEnabledActivity() {
VideoMode.WINDOW_VIEW -> { VideoMode.WINDOW_VIEW -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN) window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
setTheme(R.style.Theme_Podcini_VideoEpisode) // setTheme(R.style.Theme_Podcini_VideoEpisode)
setTheme(R.style.Theme_Podcini_VideoPlayer)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
window.setFormat(PixelFormat.TRANSPARENT) window.setFormat(PixelFormat.TRANSPARENT)
@ -127,6 +151,7 @@ class VideoplayerActivity : CastEnabledActivity() {
@UnstableApi @UnstableApi
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
setForVideoMode()
switchToAudioOnly = false switchToAudioOnly = false
if (isCasting) { if (isCasting) {
val intent = getPlayerActivityIntent(this) val intent = getPlayerActivityIntent(this)
@ -189,7 +214,6 @@ class VideoplayerActivity : CastEnabledActivity() {
} }
private fun onEventMainThread(event: FlowEvent.MessageEvent) { private fun onEventMainThread(event: FlowEvent.MessageEvent) {
// Logd(TAG, "onEvent($event)")
val errorDialog = MaterialAlertDialogBuilder(this) val errorDialog = MaterialAlertDialogBuilder(this)
errorDialog.setMessage(event.message) errorDialog.setMessage(event.message)
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int -> errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
@ -369,6 +393,13 @@ class VideoplayerActivity : CastEnabledActivity() {
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
enum class VideoMode(val mode: Int) {
None(0),
WINDOW_VIEW(1),
FULL_SCREEN_VIEW(2),
AUDIO_ONLY(3)
}
class PlaybackControlsDialog : DialogFragment() { class PlaybackControlsDialog : DialogFragment() {
private lateinit var dialog: AlertDialog private lateinit var dialog: AlertDialog
private var _binding: AudioControlsBinding? = null private var _binding: AudioControlsBinding? = null
@ -425,6 +456,472 @@ class VideoplayerActivity : CastEnabledActivity() {
} }
} }
@UnstableApi
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private var _binding: VideoEpisodeFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var root: ViewGroup
private var videoControlsVisible = true
private var videoSurfaceCreated = false
private var lastScreenTap: Long = 0
private val videoControlsHider = Handler(Looper.getMainLooper())
private var showTimeLeft = false
private var prog = 0f
private var itemsLoaded = false
private var episode: Episode? = null
private var webviewData: String? = null
private var webvDescription: ShownotesWebView? = null
var destroyingDueToReload = false
private var statusHandler: ServiceStatusHandler? = null
var isFavorite = false
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
Logd(TAG, "onVideoviewTouched ${event.action}")
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
videoControlsHider.removeCallbacks(hideVideoControls)
Logd(TAG, "onVideoviewTouched $videoControlsVisible ${System.currentTimeMillis() - lastScreenTap}")
if (System.currentTimeMillis() - lastScreenTap < 300) {
if (event.x > v.measuredWidth / 2.0f) {
onFastForward()
showSkipAnimation(true)
} else {
onRewind()
showSkipAnimation(false)
}
if (videoControlsVisible) {
hideVideoControls(false)
videoControlsVisible = false
}
return@OnTouchListener true
}
toggleVideoControlsVisibility()
if (videoControlsVisible) setupVideoControlsToggler()
lastScreenTap = System.currentTimeMillis()
true
}
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.setFixedSize(width, height)
}
@UnstableApi
override fun surfaceCreated(holder: SurfaceHolder) {
Logd(TAG, "Videoview holder created")
videoSurfaceCreated = true
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
setupVideoAspectRatio()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Logd(TAG, "Videosurface was destroyed")
videoSurfaceCreated = false
(activity as? VideoplayerActivity)?.finish()
}
}
private val hideVideoControls = Runnable {
if (videoControlsVisible) {
Logd(TAG, "Hiding video controls")
hideVideoControls(true)
videoControlsVisible = false
}
}
@OptIn(UnstableApi::class)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(activity))
root = binding.root
statusHandler = newStatusHandler()
statusHandler!!.init()
setupView()
return root
}
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
return object : ServiceStatusHandler(requireActivity()) {
override fun updatePlayButton(showPlay: Boolean) {
Logd(TAG, "updatePlayButtonShowsPlay called")
binding.playButton.setIsShowPlay(showPlay)
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else {
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setupVideoAspectRatio()
if (videoSurfaceCreated) {
Logd(TAG, "Videosurface already created, setting videosurface now")
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
}
}
}
override fun loadMediaInfo() {
this@VideoEpisodeFragment.loadMediaInfo()
}
override fun onPlaybackEnd() {
activity?.finish()
}
}
}
@UnstableApi
override fun onStart() {
super.onStart()
onPositionObserverUpdate()
procFlowEvents()
}
@UnstableApi
override fun onStop() {
super.onStop()
cancelFlowEvents()
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
// Controller released; we will not receive buffering updates
binding.progressBar.visibility = View.GONE
}
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
if (webvDescription != null) {
root.removeView(webvDescription!!)
webvDescription!!.destroy()
}
_binding = null
statusHandler?.release()
statusHandler = null // prevent leak
super.onDestroyView()
}
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink != null) return
eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
else -> {}
}
}
}
}
fun setForVideoMode() {
when (videoMode) {
VideoMode.FULL_SCREEN_VIEW -> {
Logd(TAG, "setForVideoMode setting for FULL_SCREEN_VIEW")
webvDescription?.visibility = View.GONE
val layoutParams = binding.videoPlayerContainer.layoutParams
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.videoPlayerContainer.layoutParams = layoutParams
binding.topBar.visibility = View.GONE
}
VideoMode.WINDOW_VIEW -> {
Logd(TAG, "setForVideoMode setting for WINDOW_VIEW")
webvDescription?.visibility = View.VISIBLE
val layoutParams = binding.videoPlayerContainer.layoutParams
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
binding.videoPlayerContainer.layoutParams = layoutParams
binding.topBar.visibility = View.VISIBLE
}
else -> {}
}
setupVideoAspectRatio()
}
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
when {
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
}
}
private fun setupVideoAspectRatio() {
if (videoSurfaceCreated) {
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity as Activity)
val videoWidth = if (videoMode == VideoMode.FULL_SCREEN_VIEW) max(windowMetrics.bounds.width(), windowMetrics.bounds.height())
else min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
var videoHeight = 0
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
Logd(TAG, "setupVideoAspectRatio Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
Logd(TAG, "setupVideoAspectRatio Width,height of video 1: $videoWidth, $videoHeight")
} else {
Log.e(TAG, "setupVideoAspectRatio Could not determine video size")
videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
Logd(TAG, "setupVideoAspectRatio Width,height of video 2: $videoWidth, $videoHeight")
}
val lp = binding.videoView.layoutParams
lp.width = videoWidth
lp.height = videoHeight
binding.videoView.layoutParams = lp
}
}
private var loadItemsRunning = false
@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
activity?.finish()
MainActivityStarter(requireContext()).withOpenPlayer().start()
return
}
showTimeLeft = shouldShowRemainingTime()
onPositionObserverUpdate()
if (!loadItemsRunning) {
loadItemsRunning = true
lifecycleScope.launch {
try {
episode = withContext(Dispatchers.IO) {
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
if (feedItem != null) {
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
}
feedItem
}
withContext(Dispatchers.Main) {
Logd(TAG, "load() item ${episode?.id}")
if (episode != null) {
val isFav = episode!!.isFavorite
if (isFavorite != isFav) {
isFavorite = isFav
invalidateOptionsMenu(activity)
}
}
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
"text/html", "utf-8", "about:blank")
itemsLoaded = true
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
} finally { loadItemsRunning = false }
}
}
val media = curMedia
if (media != null) {
(activity as AppCompatActivity).supportActionBar?.subtitle = media.getEpisodeTitle()
(activity as AppCompatActivity).supportActionBar?.title = media.getFeedTitle()
}
}
@UnstableApi
private fun setupView() {
showTimeLeft = shouldShowRemainingTime()
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft
val media = curMedia ?: return@setOnClickListener
val converter = TimeSpeedConverter(curSpeedFB)
val length: String
if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
length = "-" + getDurationStringLong(remainingTime)
} else {
val duration = converter.convert(media.getDuration())
length = getDurationStringLong(duration)
}
binding.durationLabel.text = length
setShowRemainTimeSetting(showTimeLeft)
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
}
binding.sbPosition.setOnSeekBarChangeListener(this)
binding.rewindButton.setOnClickListener { onRewind() }
binding.rewindButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
Logd(TAG, "setupView 1")
binding.playButton.setIsVideoScreen(true)
binding.playButton.setOnClickListener { onPlayPause() }
binding.fastForwardButton.setOnClickListener { onFastForward() }
binding.fastForwardButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
false
}
Logd(TAG, "setupView 2")
// To suppress touches directly below the slider
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
binding.videoView.holder.addCallback(surfaceHolderCallback)
setupVideoControlsToggler()
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
Logd(TAG, "setupView 2")
webvDescription = binding.webvDescription
// webvDescription.setTimecodeSelectedListener { time: Int? ->
// val cMedia = getMedia
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
// seekTo(time ?: 0)
// } else {
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
// Snackbar.LENGTH_LONG)
// }
// }
// registerForContextMenu(webvDescription)
// webvDescription.visibility = View.GONE
binding.toggleViews.setOnClickListener { (activity as VideoplayerActivity).toggleViews() }
binding.audioOnly.setOnClickListener {
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
(activity as? VideoplayerActivity)?.finish()
}
Logd(TAG, "setupView 4")
}
fun toggleVideoControlsVisibility() {
if (videoControlsVisible) hideVideoControls(true)
else showVideoControls()
videoControlsVisible = !videoControlsVisible
}
fun showSkipAnimation(isForward: Boolean) {
val skipAnimation = AnimationSet(true)
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
skipAnimation.fillAfter = false
skipAnimation.duration = 800
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
if (isForward) {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
} else {
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) {}
})
}
@UnstableApi
fun onRewind() {
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
setupVideoControlsToggler()
}
@UnstableApi
fun onPlayPause() {
playPause()
setupVideoControlsToggler()
}
@UnstableApi
fun onFastForward() {
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
setupVideoControlsToggler()
}
private fun setupVideoControlsToggler() {
videoControlsHider.removeCallbacks(hideVideoControls)
videoControlsHider.postDelayed(hideVideoControls, 2500)
}
private fun showVideoControls() {
Logd(TAG, "showVideoControls")
binding.bottomControlsContainer.visibility = View.VISIBLE
binding.controlsContainer.visibility = View.VISIBLE
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_in)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
(activity as AppCompatActivity).supportActionBar?.show()
}
fun hideVideoControls(showAnimation: Boolean) {
Logd(TAG, "hideVideoControls $showAnimation")
if (!isAdded) return
if (showAnimation) {
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_out)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
binding.bottomControlsContainer.visibility = View.GONE
binding.controlsContainer.visibility = View.GONE
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
}
private fun onPositionObserverUpdate() {
val converter = TimeSpeedConverter(curSpeedFB)
val currentPosition = converter.convert(curPositionFB)
val duration_ = converter.convert(curDurationFB)
val remainingTime = converter.convert(curDurationFB - curPositionFB)
// 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")
return
}
binding.positionLabel.text = getDurationStringLong(currentPosition)
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
else binding.durationLabel.text = getDurationStringLong(duration_)
updateProgressbarPosition(currentPosition, duration_)
}
private fun updateProgressbarPosition(position: Int, duration: Int) {
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
val progress = (position.toFloat()) / duration
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (fromUser) {
prog = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(curSpeedFB)
val position = converter.convert((prog * curDurationFB).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
binding.seekCardView.scaleX = .8f
binding.seekCardView.scaleY = .8f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start()
videoControlsHider.removeCallbacks(hideVideoControls)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
seekTo((prog * curDurationFB).toInt())
binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start()
setupVideoControlsToggler()
}
companion object {
val videoSize: Pair<Int, Int>?
get() = playbackService?.mPlayer?.getVideoSize()
}
}
companion object { companion object {
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous" private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
const val VIDEO_MODE = "Video_Mode" const val VIDEO_MODE = "Video_Mode"

View File

@ -14,6 +14,9 @@ import android.os.Bundle
class MainActivityStarter(private val context: Context) { class MainActivityStarter(private val context: Context) {
private val intent: Intent = Intent(INTENT) private val intent: Intent = Intent(INTENT)
private var fragmentArgs: Bundle? = null private var fragmentArgs: Bundle? = null
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
init { init {
intent.setPackage(context.packageName) intent.setPackage(context.packageName)
@ -24,10 +27,6 @@ class MainActivityStarter(private val context: Context) {
return intent return intent
} }
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
fun start() { fun start() {
context.startActivity(getIntent()) context.startActivity(getIntent())
} }
@ -64,7 +63,6 @@ class MainActivityStarter(private val context: Context) {
fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter { fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter {
if (fragmentArgs == null) fragmentArgs = Bundle() if (fragmentArgs == null) fragmentArgs = Bundle()
fragmentArgs!!.putBoolean(name, value) fragmentArgs!!.putBoolean(name, value)
return this return this
} }

View File

@ -16,6 +16,9 @@ import androidx.media3.common.util.UnstableApi
*/ */
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) { @OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
val intent: Intent = Intent(INTENT) val intent: Intent = Intent(INTENT)
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
init { init {
intent.setPackage(context.packageName) intent.setPackage(context.packageName)
@ -23,10 +26,6 @@ import androidx.media3.common.util.UnstableApi
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode) if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
} }
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
fun start() { fun start() {
context.startActivity(intent) context.startActivity(intent)
} }

View File

@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre

View File

@ -33,10 +33,6 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) {
feed = unmanaged(feed) feed = unmanaged(feed)
feed.setCustomTitle1(newTitle) feed.setCustomTitle1(newTitle)
feed = upsertBlk(feed) {} feed = upsertBlk(feed) {}
// feed = upsertBlk(feed) {
// it.setCustomTitle1(newTitle)
// }
} }
.setNeutralButton(R.string.reset, null) .setNeutralButton(R.string.reset, null)
.setNegativeButton(R.string.cancel_label, null) .setNegativeButton(R.string.cancel_label, null)

View File

@ -50,9 +50,8 @@ abstract class DatesFilterDialog(private val context: Context, oldestDate: Long)
binding.allTimeButton.isEnabled = !checked binding.allTimeButton.isEnabled = !checked
binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f
} }
if (showMarkPlayed) { if (showMarkPlayed) binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed else {
} else {
binding.includeMarkedCheckbox.visibility = View.GONE binding.includeMarkedCheckbox.visibility = View.GONE
binding.noticeMessage.visibility = View.GONE binding.noticeMessage.visibility = View.GONE
} }

View File

@ -136,11 +136,7 @@ open class FeedSortDialog : BottomSheetDialogFragment() {
} }
private fun setFeedOrder(selected: String, dir: Int) { private fun setFeedOrder(selected: String, dir: Int) {
appPrefs.edit() appPrefs.edit().putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected).apply()
.putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected) appPrefs.edit().putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir).apply()
.apply()
appPrefs.edit()
.putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir)
.apply()
} }
} }

View File

@ -25,17 +25,13 @@ class ShareDialog : BottomSheetDialogFragment() {
private var item: Episode? = null private var item: Episode? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
ctx = requireContext() ctx = requireContext()
prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
_binding = ShareEpisodeDialogBinding.inflate(inflater) _binding = ShareEpisodeDialogBinding.inflate(inflater)
binding.shareDialogRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int -> binding.shareDialogRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
binding.sharePositionCheckbox.isEnabled = checkedId == binding.shareSocialRadio.id binding.sharePositionCheckbox.isEnabled = checkedId == binding.shareSocialRadio.id
} }
setupOptions() setupOptions()
binding.shareButton.setOnClickListener { binding.shareButton.setOnClickListener {
val includePlaybackPosition = binding.sharePositionCheckbox.isChecked val includePlaybackPosition = binding.sharePositionCheckbox.isChecked
val position: Int val position: Int
@ -52,14 +48,9 @@ class ShareDialog : BottomSheetDialogFragment() {
shareFeedItemFile(ctx, item!!.media!!) shareFeedItemFile(ctx, item!!.media!!)
position = 3 position = 3
} }
else -> { else -> throw IllegalStateException("Unknown share method")
throw IllegalStateException("Unknown share method")
}
} }
prefs.edit() prefs.edit().putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition).putInt(PREF_SHARE_EPISODE_TYPE, position).apply()
.putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition)
.putInt(PREF_SHARE_EPISODE_TYPE, position)
.apply()
dismiss() dismiss()
} }
return binding.root return binding.root

View File

@ -85,15 +85,9 @@ class SleepTimerDialog : DialogFragment() {
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10) extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20) extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
extendSleepFiveMinutesButton.setOnClickListener { extendSleepFiveMinutesButton.setOnClickListener { extendSleepTimer((5 * 1000 * 60).toLong()) }
extendSleepTimer((5 * 1000 * 60).toLong()) extendSleepTenMinutesButton.setOnClickListener { extendSleepTimer((10 * 1000 * 60).toLong()) }
} extendSleepTwentyMinutesButton.setOnClickListener { extendSleepTimer((20 * 1000 * 60).toLong()) }
extendSleepTenMinutesButton.setOnClickListener {
extendSleepTimer((10 * 1000 * 60).toLong())
}
extendSleepTwentyMinutesButton.setOnClickListener {
extendSleepTimer((20 * 1000 * 60).toLong())
}
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) etxtTime.visibility = View.GONE if (isChecked) etxtTime.visibility = View.GONE
@ -115,9 +109,7 @@ class SleepTimerDialog : DialogFragment() {
changeTimesButton.isEnabled = chAutoEnable.isChecked changeTimesButton.isEnabled = chAutoEnable.isChecked
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setShakeToReset(isChecked) }
setShakeToReset(isChecked)
}
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) } binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setAutoEnable(isChecked) setAutoEnable(isChecked)
@ -132,9 +124,7 @@ class SleepTimerDialog : DialogFragment() {
showTimeRangeDialog(from, to) showTimeRangeDialog(from, to)
} }
binding.disableSleeptimerButton.setOnClickListener { binding.disableSleeptimerButton.setOnClickListener { playbackService?.taskManager?.disableSleepTimer() }
playbackService?.taskManager?.disableSleepTimer()
}
binding.setSleeptimerButton.setOnClickListener { binding.setSleeptimerButton.setOnClickListener {
if (!PlaybackService.isRunning) { if (!PlaybackService.isRunning) {
@ -237,18 +227,16 @@ class SleepTimerDialog : DialogFragment() {
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) { class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
private val view = TimeRangeView(context, from, to) private val view = TimeRangeView(context, from, to)
val from: Int
get() = view.from
val to: Int
get() = view.to
init { init {
setView(view) setView(view)
setPositiveButton(android.R.string.ok, null) setPositiveButton(android.R.string.ok, null)
} }
val from: Int
get() = view.from
val to: Int
get() = view.to
internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) { internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) {
private val paintDial = Paint() private val paintDial = Paint()
private val paintSelected = Paint() private val paintSelected = Paint()

View File

@ -135,7 +135,6 @@ class SwipeActionsDialog(private val context: Context, private val tag: String)
item.root.setOnClickListener { item.root.setOnClickListener {
if (direction == LEFT) leftAction = keys[i] if (direction == LEFT) leftAction = keys[i]
else rightAction = keys[i] else rightAction = keys[i]
setupSwipeDirectionView(view, direction) setupSwipeDirectionView(view, direction)
dialog.dismiss() dialog.dismiss()
} }

View File

@ -46,10 +46,7 @@ class SwitchQueueDialog(activity: Activity) {
val items = mutableListOf<Episode>() val items = mutableListOf<Episode>()
items.addAll(curQueue.episodes) items.addAll(curQueue.episodes)
items.addAll(curQueue_.episodes) items.addAll(curQueue_.episodes)
// unmanaged(curQueue_) curQueue = upsertBlk(curQueue_) { it.update() }
curQueue = upsertBlk(curQueue_) {
it.update()
}
EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items)) EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items))
} }
} }

View File

@ -25,9 +25,7 @@ class TagSettingsDialog : DialogFragment() {
private var _binding: EditTagsDialogBinding? = null private var _binding: EditTagsDialogBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var feedList: MutableList<Feed> = mutableListOf() private var feedList: MutableList<Feed> = mutableListOf()
private lateinit var displayedTags: MutableList<String> private lateinit var displayedTags: MutableList<String>
private lateinit var adapter: SimpleChipAdapter private lateinit var adapter: SimpleChipAdapter
@ -53,7 +51,6 @@ class TagSettingsDialog : DialogFragment() {
} }
} }
binding.tagsRecycler.adapter = adapter binding.tagsRecycler.adapter = adapter
binding.newTagTextInput.setEndIconOnClickListener { binding.newTagTextInput.setEndIconOnClickListener {
addTag(binding.newTagEditText.text.toString().trim { it <= ' ' }) addTag(binding.newTagEditText.text.toString().trim { it <= ' ' })
} }
@ -87,7 +84,6 @@ class TagSettingsDialog : DialogFragment() {
private fun addTag(name: String) { private fun addTag(name: String) {
if (name.isEmpty() || displayedTags.contains(name)) return if (name.isEmpty() || displayedTags.contains(name)) return
displayedTags.add(name) displayedTags.add(name)
binding.newTagEditText.setText("") binding.newTagEditText.setText("")
adapter.notifyDataSetChanged() adapter.notifyDataSetChanged()

View File

@ -47,17 +47,14 @@ import java.util.*
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
open class VariableSpeedDialog : BottomSheetDialogFragment() { open class VariableSpeedDialog : BottomSheetDialogFragment() {
private var _binding: SpeedSelectDialogBinding? = null
private val binding get() = _binding!!
private lateinit var adapter: SpeedSelectionAdapter private lateinit var adapter: SpeedSelectionAdapter
private lateinit var speedSeekBar: PlaybackSpeedSeekBar private lateinit var speedSeekBar: PlaybackSpeedSeekBar
private lateinit var addCurrentSpeedChip: Chip private lateinit var addCurrentSpeedChip: Chip
private var _binding: SpeedSelectDialogBinding? = null
private val binding get() = _binding!!
private val selectedSpeeds: MutableList<Float>
private lateinit var settingCode: BooleanArray private lateinit var settingCode: BooleanArray
private val selectedSpeeds: MutableList<Float>
init { init {
val format = DecimalFormatSymbols(Locale.US) val format = DecimalFormatSymbols(Locale.US)
@ -262,9 +259,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
if (codeArray[1]) { if (codeArray[1]) {
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
if (episode?.feed?.preferences != null) { if (episode?.feed?.preferences != null) {
upsertBlk(episode.feed!!) { upsertBlk(episode.feed!!) { it.preferences!!.playSpeed = speed }
it.preferences!!.playSpeed = speed
}
} }
} }
if (codeArray[0]) { if (codeArray[0]) {
@ -283,9 +278,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
} }
} }
private fun setCurTempSpeed(speed: Float) { private fun setCurTempSpeed(speed: Float) {
curState = upsertBlk(curState) { curState = upsertBlk(curState) { it.curTempSpeed = speed }
it.curTempSpeed = speed
}
} }
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip) inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
} }
@ -310,7 +303,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
args.putBooleanArray("settingCode", settingCode) args.putBooleanArray("settingCode", settingCode)
if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault) if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault)
dialog.arguments = args dialog.arguments = args
return dialog return dialog
} }
} }

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes import ac.mdiq.podcini.storage.database.Episodes.getEpisodes

View File

@ -7,7 +7,7 @@ import ac.mdiq.podcini.net.download.DownloadError
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.service.Downloader import ac.mdiq.podcini.net.download.service.Downloader
import ac.mdiq.podcini.net.download.service.HttpDownloader import ac.mdiq.podcini.net.download.service.HttpDownloader
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
@ -81,12 +81,11 @@ import kotlin.math.min
* feed object that was parsed. This activity MUST be started with a given URL * feed object that was parsed. This activity MUST be started with a given URL
* or an Exception will be thrown. * or an Exception will be thrown.
* *
*
* If the feed cannot be downloaded or parsed, an error dialog will be displayed * If the feed cannot be downloaded or parsed, an error dialog will be displayed
* and the activity will finish as soon as the error dialog is closed. * and the activity will finish as soon as the error dialog is closed.
*/ */
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
class OnlineFeedViewFragment : Fragment() { class OnlineFeedFragment : Fragment() {
private var _binding: OnlineFeedviewFragmentBinding? = null private var _binding: OnlineFeedviewFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -147,17 +146,6 @@ class OnlineFeedViewFragment : Fragment() {
return binding.root return binding.root
} }
private fun showNoPodcastFoundError() {
requireActivity().runOnUiThread {
MaterialAlertDialogBuilder(requireContext())
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
.setTitle(R.string.error_label)
.setMessage(R.string.null_value_podcast_error)
.setOnDismissListener {}
.show()
}
}
/** /**
* Displays a progress indicator. * Displays a progress indicator.
*/ */
@ -392,7 +380,7 @@ class OnlineFeedViewFragment : Fragment() {
try { try {
val feeds = withContext(Dispatchers.IO) { getFeedList() } val feeds = withContext(Dispatchers.IO) { getFeedList() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@OnlineFeedViewFragment.feeds = feeds this@OnlineFeedFragment.feeds = feeds
handleUpdatedFeedStatus() handleUpdatedFeedStatus()
} }
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
@ -492,7 +480,7 @@ class OnlineFeedViewFragment : Fragment() {
} }
} }
} }
if (feedSource == "VistaGuide" && isEnableAutodownload) if (feedSource != "VistaGuide" && isEnableAutodownload)
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true) binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
@ -563,7 +551,7 @@ class OnlineFeedViewFragment : Fragment() {
if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false, if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false,
FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "") FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
if (isEnableAutodownload) { if (feedSource != "VistaGuide" && isEnableAutodownload) {
val autoDownload = binding.autoDownloadCheckBox.isChecked val autoDownload = binding.autoDownloadCheckBox.isChecked
feed1.preferences!!.autoDownload = autoDownload feed1.preferences!!.autoDownload = autoDownload
val editor = prefs!!.edit() val editor = prefs!!.edit()
@ -679,11 +667,22 @@ class OnlineFeedViewFragment : Fragment() {
return true return true
} }
private fun showNoPodcastFoundError() {
requireActivity().runOnUiThread {
MaterialAlertDialogBuilder(requireContext())
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
.setTitle(R.string.error_label)
.setMessage(R.string.null_value_podcast_error)
.setOnDismissListener {}
.show()
}
}
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) : private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
AuthenticationDialog(context, titleRes, true, username, password) { AuthenticationDialog(context, titleRes, true, username, password) {
override fun onConfirmed(username: String, password: String) { override fun onConfirmed(username: String, password: String) {
this@OnlineFeedViewFragment.username = username this@OnlineFeedFragment.username = username
this@OnlineFeedViewFragment.password = password this@OnlineFeedFragment.password = password
startFeedBuilding(feedUrl) startFeedBuilding(feedUrl)
} }
} }
@ -836,7 +835,7 @@ class OnlineFeedViewFragment : Fragment() {
const val ARG_FEEDURL: String = "arg.feedurl" const val ARG_FEEDURL: String = "arg.feedurl"
const val ARG_WAS_MANUAL_URL: String = "manual_url" const val ARG_WAS_MANUAL_URL: String = "manual_url"
private const val RESULT_ERROR = 2 private const val RESULT_ERROR = 2
private val TAG: String = OnlineFeedViewFragment::class.simpleName ?: "Anonymous" private val TAG: String = OnlineFeedFragment::class.simpleName ?: "Anonymous"
private const val PREFS = "OnlineFeedViewFragmentPreferences" private const val PREFS = "OnlineFeedViewFragmentPreferences"
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload" private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
private const val KEY_UP_ARROW = "up_arrow" private const val KEY_UP_ARROW = "up_arrow"
@ -846,8 +845,8 @@ class OnlineFeedViewFragment : Fragment() {
if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
} }
fun newInstance(feedUrl: String): OnlineFeedViewFragment { fun newInstance(feedUrl: String): OnlineFeedFragment {
val fragment = OnlineFeedViewFragment() val fragment = OnlineFeedFragment()
val b = Bundle() val b = Bundle()
b.putString(ARG_FEEDURL, feedUrl) b.putString(ARG_FEEDURL, feedUrl)
fragment.arguments = b fragment.arguments = b

View File

@ -127,7 +127,7 @@ class OnlineSearchFragment : Fragment() {
} }
private fun addUrl(url: String) { private fun addUrl(url: String) {
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url) val fragment: Fragment = OnlineFeedFragment.newInstance(url)
(activity as MainActivity).loadChildFragment(fragment) (activity as MainActivity).loadChildFragment(fragment)
} }

View File

@ -183,7 +183,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val podcast: PodcastSearchResult? = adapter.getItem(position) val podcast: PodcastSearchResult? = adapter.getItem(position)
if (podcast?.feedUrl.isNullOrEmpty()) return if (podcast?.feedUrl.isNullOrEmpty()) return
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast!!.feedUrl!!) val fragment: Fragment = OnlineFeedFragment.newInstance(podcast!!.feedUrl!!)
(activity as MainActivity).loadChildFragment(fragment) (activity as MainActivity).loadChildFragment(fragment)
} }
@ -318,7 +318,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val podcast = searchResults!![position] val podcast = searchResults!![position]
if (podcast.feedUrl == null) return@OnItemClickListener if (podcast.feedUrl == null) return@OnItemClickListener
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl) val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
(activity as MainActivity).loadChildFragment(fragment) (activity as MainActivity).loadChildFragment(fragment)
} }

View File

@ -395,7 +395,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
inVal.hideSoftInputFromWindow(searchView.windowToken, 0) inVal.hideSoftInputFromWindow(searchView.windowToken, 0)
val query = searchView.query.toString() val query = searchView.query.toString()
if (query.matches("http[s]?://.*".toRegex())) { if (query.matches("http[s]?://.*".toRegex())) {
val fragment: Fragment = OnlineFeedViewFragment.newInstance(query) val fragment: Fragment = OnlineFeedFragment.newInstance(query)
(activity as MainActivity).loadChildFragment(fragment) (activity as MainActivity).loadChildFragment(fragment)
return return
} }

View File

@ -61,7 +61,7 @@ class SearchResultsFragment : Fragment() {
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long -> gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
val podcast = searchResults[position] val podcast = searchResults[position]
if (podcast.feedUrl != null) { if (podcast.feedUrl != null) {
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl) val fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
fragment.feedSource = podcast.source fragment.feedSource = podcast.source
(activity as MainActivity).loadChildFragment(fragment) (activity as MainActivity).loadChildFragment(fragment)
} }

View File

@ -1,559 +0,0 @@
package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
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.ui.activity.VideoplayerActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.Pair
import android.view.*
import android.view.animation.*
import android.widget.FrameLayout
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@UnstableApi
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
private var _binding: VideoEpisodeFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var root: ViewGroup
private var videoControlsVisible = true
private var videoSurfaceCreated = false
private var lastScreenTap: Long = 0
private val videoControlsHider = Handler(Looper.getMainLooper())
private var showTimeLeft = false
private var prog = 0f
private var itemsLoaded = false
private var episode: Episode? = null
private var webviewData: String? = null
private var webvDescription: ShownotesWebView? = null
var destroyingDueToReload = false
var statusHandler: ServiceStatusHandler? = 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 (videoControlsVisible) {
hideVideoControls(false)
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
videoControlsVisible = false
}
return@OnTouchListener true
}
toggleVideoControlsVisibility()
if (videoControlsVisible) setupVideoControlsToggler()
lastScreenTap = System.currentTimeMillis()
true
}
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
holder.setFixedSize(width, height)
}
@UnstableApi
override fun surfaceCreated(holder: SurfaceHolder) {
Logd(TAG, "Videoview holder created")
videoSurfaceCreated = true
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) setVideoSurface(holder)
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
setupVideoAspectRatio()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Logd(TAG, "Videosurface was destroyed")
videoSurfaceCreated = false
(activity as? VideoplayerActivity)?.finish()
// TODO: test
// if (controller != null && !destroyingDueToReload && !(activity as VideoplayerActivity).switchToAudioOnly)
// notifyVideoSurfaceAbandoned()
}
}
private val hideVideoControls = Runnable {
if (videoControlsVisible) {
Logd(TAG, "Hiding video controls")
hideVideoControls(true)
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as? AppCompatActivity)?.supportActionBar?.hide()
videoControlsVisible = false
}
}
@OptIn(UnstableApi::class)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
root = binding.root
statusHandler = newStatusHandler()
statusHandler!!.init()
// loadMediaInfo()
setupView()
return root
}
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
return object : ServiceStatusHandler(requireActivity()) {
override fun updatePlayButton(showPlay: Boolean) {
Logd(TAG, "updatePlayButtonShowsPlay called")
binding.playButton.setIsShowPlay(showPlay)
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
else {
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
setupVideoAspectRatio()
if (videoSurfaceCreated) {
Logd(TAG, "Videosurface already created, setting videosurface now")
// setVideoSurface(binding.videoView.holder)
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
}
}
}
override fun loadMediaInfo() {
this@VideoEpisodeFragment.loadMediaInfo()
}
override fun onPlaybackEnd() {
activity?.finish()
}
}
}
@UnstableApi
override fun onStart() {
super.onStart()
onPositionObserverUpdate()
procFlowEvents()
}
@UnstableApi
override fun onStop() {
super.onStop()
cancelFlowEvents()
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
// Controller released; we will not receive buffering updates
binding.progressBar.visibility = View.GONE
}
@UnstableApi
override fun onPause() {
// this does nothing
// if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
// }
super.onPause()
}
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
if (webvDescription != null) {
root.removeView(webvDescription!!)
webvDescription!!.destroy()
}
_binding = null
statusHandler?.release()
statusHandler = null // prevent leak
super.onDestroyView()
}
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink != null) return
eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
else -> {}
}
}
}
}
fun setForVideoMode() {
when (videoMode) {
VideoMode.FULL_SCREEN_VIEW ->{
webvDescription?.visibility = View.GONE
val layoutParams = binding.videoPlayerContainer.layoutParams
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
binding.videoPlayerContainer.layoutParams = layoutParams
}
VideoMode.WINDOW_VIEW -> {
webvDescription?.visibility = View.VISIBLE
val layoutParams = binding.videoPlayerContainer.layoutParams
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
binding.videoPlayerContainer.layoutParams = layoutParams
}
else -> {}
}
}
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
when {
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
}
}
private fun setupVideoAspectRatio() {
if (videoSurfaceCreated) {
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
val videoWidth = resources.displayMetrics.widthPixels
val videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
binding.videoView.setVideoSize(videoWidth, videoHeight)
// binding.videoView.setVideoSize(videoSize.first, videoSize.second)
// binding.videoView.setVideoSize(-1, -1)
} else {
Log.e(TAG, "Could not determine video size")
val videoWidth = resources.displayMetrics.widthPixels
val videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
binding.videoView.setVideoSize(videoWidth, videoHeight)
}
}
}
private var loadItemsRunning = false
@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
activity?.finish()
MainActivityStarter(requireContext()).withOpenPlayer().start()
return
}
showTimeLeft = shouldShowRemainingTime()
onPositionObserverUpdate()
if (!loadItemsRunning) {
loadItemsRunning = true
lifecycleScope.launch {
try {
episode = withContext(Dispatchers.IO) {
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
if (feedItem != null) {
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
}
feedItem
}
withContext(Dispatchers.Main) {
Logd(TAG, "load() item ${episode?.id}")
if (episode != null) {
val isFav = episode!!.isFavorite
if (isFavorite != isFav) {
isFavorite = isFav
invalidateOptionsMenu(requireActivity())
}
}
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
"text/html", "utf-8", "about:blank")
itemsLoaded = true
}
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
} finally { loadItemsRunning = false }
}
}
val media = curMedia
if (media != null) {
(activity as AppCompatActivity).supportActionBar!!.subtitle = media.getEpisodeTitle()
(activity as AppCompatActivity).supportActionBar!!.title = media.getFeedTitle()
}
}
@UnstableApi
private fun setupView() {
showTimeLeft = shouldShowRemainingTime()
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
binding.durationLabel.setOnClickListener {
showTimeLeft = !showTimeLeft
val media = curMedia ?: return@setOnClickListener
val converter = TimeSpeedConverter(curSpeedFB)
val length: String
if (showTimeLeft) {
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
length = "-" + getDurationStringLong(remainingTime)
} else {
val duration = converter.convert(media.getDuration())
length = getDurationStringLong(duration)
}
binding.durationLabel.text = length
setShowRemainTimeSetting(showTimeLeft)
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
}
binding.sbPosition.setOnSeekBarChangeListener(this)
binding.rewindButton.setOnClickListener { onRewind() }
binding.rewindButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
true
}
binding.playButton.setIsVideoScreen(true)
binding.playButton.setOnClickListener { onPlayPause() }
binding.fastForwardButton.setOnClickListener { onFastForward() }
binding.fastForwardButton.setOnLongClickListener {
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
false
}
// To suppress touches directly below the slider
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
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
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
// seekTo(time ?: 0)
// } else {
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
// Snackbar.LENGTH_LONG)
// }
// }
// registerForContextMenu(webvDescription)
// webvDescription.visibility = View.GONE
binding.toggleViews.setOnClickListener { (activity as? VideoplayerActivity)?.toggleViews() }
binding.audioOnly.setOnClickListener {
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
(activity as? VideoplayerActivity)?.finish()
}
}
fun toggleVideoControlsVisibility() {
if (videoControlsVisible) {
hideVideoControls(true)
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
} else {
showVideoControls()
(activity as AppCompatActivity).supportActionBar?.show()
}
videoControlsVisible = !videoControlsVisible
}
fun showSkipAnimation(isForward: Boolean) {
val skipAnimation = AnimationSet(true)
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
skipAnimation.fillAfter = false
skipAnimation.duration = 800
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
if (isForward) {
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
} else {
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) {}
})
}
fun notifyVideoSurfaceAbandoned() {
// playbackService?.notifyVideoSurfaceAbandoned()
playbackService?.mPlayer?.pause(abandonFocus = true, reinit = false)
playbackService?.mPlayer?.resetVideoSurface()
}
// fun setVideoSurface(holder: SurfaceHolder?) {
// playbackService?.mPlayer?.setVideoSurface(holder)
// }
@UnstableApi
fun onRewind() {
// if (statusHandler == null) return
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
setupVideoControlsToggler()
}
@UnstableApi
fun onPlayPause() {
playPause()
setupVideoControlsToggler()
}
@UnstableApi
fun onFastForward() {
// if (statusHandler == null) return
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
setupVideoControlsToggler()
}
private fun setupVideoControlsToggler() {
videoControlsHider.removeCallbacks(hideVideoControls)
videoControlsHider.postDelayed(hideVideoControls, 2500)
}
private fun showVideoControls() {
binding.bottomControlsContainer.visibility = View.VISIBLE
binding.controlsContainer.visibility = View.VISIBLE
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
// binding.bottomControlsContainer.fitsSystemWindows = true
}
fun hideVideoControls(showAnimation: Boolean) {
if (!isAdded) return
if (showAnimation) {
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out)
if (animation != null) {
binding.bottomControlsContainer.startAnimation(animation)
binding.controlsContainer.startAnimation(animation)
}
}
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
// (activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
// or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
// or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
// binding.bottomControlsContainer.fitsSystemWindows = true
binding.bottomControlsContainer.visibility = View.GONE
binding.controlsContainer.visibility = View.GONE
}
private fun onPositionObserverUpdate() {
// if (statusHandler == null) return
val converter = TimeSpeedConverter(curSpeedFB)
val currentPosition = converter.convert(curPositionFB)
val duration_ = converter.convert(curDurationFB)
val remainingTime = converter.convert(curDurationFB - curPositionFB)
// 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")
return
}
binding.positionLabel.text = getDurationStringLong(currentPosition)
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
else binding.durationLabel.text = getDurationStringLong(duration_)
updateProgressbarPosition(currentPosition, duration_)
}
private fun updateProgressbarPosition(position: Int, duration: Int) {
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
val progress = (position.toFloat()) / duration
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
// if (statusHandler == null) return
if (fromUser) {
prog = progress / (seekBar.max.toFloat())
val converter = TimeSpeedConverter(curSpeedFB)
val position = converter.convert((prog * curDurationFB).toInt())
binding.seekPositionLabel.text = getDurationStringLong(position)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
binding.seekCardView.scaleX = .8f
binding.seekCardView.scaleY = .8f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(1f).scaleX(1f).scaleY(1f)
.setDuration(200)
.start()
videoControlsHider.removeCallbacks(hideVideoControls)
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
seekTo((prog * curDurationFB).toInt())
binding.seekCardView.scaleX = 1f
binding.seekCardView.scaleY = 1f
binding.seekCardView.animate()
.setInterpolator(FastOutSlowInInterpolator())
.alpha(0f).scaleX(.8f).scaleY(.8f)
.setDuration(200)
.start()
setupVideoControlsToggler()
}
companion object {
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
val videoSize: Pair<Int, Int>?
get() = playbackService?.mPlayer?.getVideoSize()
}
}

View File

@ -1,86 +0,0 @@
package ac.mdiq.podcini.ui.view
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.util.AttributeSet
import android.widget.VideoView
import kotlin.math.ceil
class AspectRatioVideoView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
: VideoView(context, attrs, defStyle) {
private var mVideoWidth = 0
private var mVideoHeight = 0
private var mAvailableWidth = -1f
private var mAvailableHeight = -1f
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (mVideoWidth <= 0 || mVideoHeight <= 0) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
Logd(TAG, "onMeasure $mAvailableWidth $mAvailableHeight")
if (mAvailableWidth < 0 || mAvailableHeight < 0) {
mAvailableWidth = width.toFloat()
mAvailableHeight = height.toFloat()
}
val heightRatio = mVideoHeight.toFloat() / mAvailableHeight
val widthRatio = mVideoWidth.toFloat() / mAvailableWidth
val scaledHeight: Int
val scaledWidth: Int
if (heightRatio > widthRatio) {
scaledHeight = ceil((mVideoHeight.toFloat() / heightRatio).toDouble()).toInt()
scaledWidth = ceil((mVideoWidth.toFloat() / heightRatio).toDouble()).toInt()
} else {
scaledHeight = ceil((mVideoHeight.toFloat() / widthRatio).toDouble()).toInt()
scaledWidth = ceil((mVideoWidth.toFloat() / widthRatio).toDouble()).toInt()
}
setMeasuredDimension(scaledWidth, scaledHeight)
}
/**
* Source code originally from:
* http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625
*
* @param videoWidth
* @param videoHeight
*/
fun setVideoSize(videoWidth: Int, videoHeight: Int) {
// Set the new video size
mVideoWidth = videoWidth
mVideoHeight = videoHeight
Logd(TAG, "setVideoSize $mVideoWidth $mVideoHeight")
/*
* If this isn't set the video is stretched across the
* SurfaceHolders display surface (i.e. the SurfaceHolder
* as the same size and the video is drawn to fit this
* display area). We want the size to be the video size
* and allow the aspectratio to handle how the surface is shown
*/
holder.setFixedSize(videoWidth, videoHeight)
// requestLayout()
// invalidate()
}
/**
* Sets the maximum size that the view might expand to
* @param width
* @param height
*/
fun setAvailableSize(width: Float, height: Float) {
mAvailableWidth = width
mAvailableHeight = height
Logd(TAG, "setAvailableSize $mAvailableWidth $mAvailableHeight")
// requestLayout()
}
companion object {
private val TAG: String = AspectRatioVideoView::class.simpleName ?: "Anonymous"
}
}

View File

@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.view
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.base.InTheatre
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.util.config
import ac.mdiq.podcini.net.download.service.DownloadServiceInterfaceImpl import ac.mdiq.podcini.net.download.service.DownloadServiceInterfaceImpl
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setCacheDirectory import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setCacheDirectory
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.ssl.SslProviderInstaller import ac.mdiq.podcini.net.ssl.SslProviderInstaller
import ac.mdiq.podcini.net.sync.SyncService import ac.mdiq.podcini.net.sync.SyncService
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink

View File

@ -7,13 +7,22 @@
android:background="@color/black" android:background="@color/black"
android:id="@+id/videoEpisodeContainer"> android:id="@+id/videoEpisodeContainer">
<View
android:id="@+id/topBar"
android:layout_width="match_parent"
android:layout_height="100dp"
android:minHeight="?android:attr/actionBarSize"
android:layout_alignParentTop="true"
android:background="@color/black" />
<FrameLayout <FrameLayout
android:id="@+id/videoPlayerContainer" android:id="@+id/videoPlayerContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/topBar"
android:background="@color/black"> android:background="@color/black">
<ac.mdiq.podcini.ui.view.AspectRatioVideoView <VideoView
android:id="@+id/videoView" android:id="@+id/videoView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -239,8 +239,8 @@
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark"> <style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
<item name="windowActionBarOverlay">true</item> <item name="windowActionBarOverlay">true</item>
<item name="windowActionBar">false</item> <item name="windowActionBar">true</item>
<item name="windowNoTitle">true</item> <item name="windowNoTitle">false</item>
</style> </style>
<style name="Podcini.TextView.Heading" parent="@android:style/TextAppearance.Medium"> <style name="Podcini.TextView.Heading" parent="@android:style/TextAppearance.Medium">

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.feed
import ac.mdiq.podcini.net.feed.LocalFeedUpdater import ac.mdiq.podcini.net.feed.LocalFeedUpdater
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getFeedList

View File

@ -1,5 +1,6 @@
package ac.mdiq.podcini.net.download.serviceinterface package ac.mdiq.podcini.net.download.serviceinterface
import ac.mdiq.podcini.net.download.service.DownloadRequest
import android.os.Bundle import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia

View File

@ -1,5 +1,6 @@
package ac.mdiq.podcini.net.download.serviceinterface package ac.mdiq.podcini.net.download.serviceinterface
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeMedia import ac.mdiq.podcini.storage.model.EpisodeMedia
import android.content.Context import android.content.Context

View File

@ -1,6 +1,6 @@
package ac.mdiq.podcini.storage package ac.mdiq.podcini.storage
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.storage package ac.mdiq.podcini.storage
import ac.mdiq.podcini.feed.FeedMother.anyFeed import ac.mdiq.podcini.feed.FeedMother.anyFeed
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
import ac.mdiq.podcini.storage.database.Queues import ac.mdiq.podcini.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation

View File

@ -1,6 +1,6 @@
package ac.mdiq.podcini.util.syndication package ac.mdiq.podcini.util.syndication
import ac.mdiq.podcini.ui.fragment.OnlineFeedViewFragment import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import org.apache.commons.io.FileUtils import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils import org.apache.commons.io.IOUtils
@ -19,13 +19,13 @@ import java.nio.charset.StandardCharsets
*/ */
@RunWith(RobolectricTestRunner::class) @RunWith(RobolectricTestRunner::class)
class FeedDiscovererTest { class FeedDiscovererTest {
private var fd: OnlineFeedViewFragment.FeedDiscoverer? = null private var fd: OnlineFeedFragment.FeedDiscoverer? = null
private var testDir: File? = null private var testDir: File? = null
@Before @Before
fun setUp() { fun setUp() {
fd = OnlineFeedViewFragment.FeedDiscoverer() fd = OnlineFeedFragment.FeedDiscoverer()
testDir = File(InstrumentationRegistry testDir = File(InstrumentationRegistry
.getInstrumentation().targetContext.filesDir, "FeedDiscovererTest") .getInstrumentation().targetContext.filesDir, "FeedDiscovererTest")
testDir!!.mkdir() testDir!!.mkdir()

View File

@ -1,3 +1,10 @@
# 6.5.5
* corrected issue of Youtube channel being set for auto-download when subscribing
* fixed various issues on video sizing and further refined the video player
* some class restructuring and refactoring and nullalability adjustments
* updated various dependencies
# 6.5.4 # 6.5.4
* in the search bar of OnlineSearch view, search button is moved to the end of the bar * in the search bar of OnlineSearch view, search button is moved to the end of the bar

View File

@ -0,0 +1,6 @@
Version 6.5.5 brings several changes:
* corrected issue of Youtube channel being set for auto-download when subscribing
* fixed various issues on video sizing and further refined the video player
* some class restructuring and refactoring and nullalability adjustments
* updated various dependencies