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"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020238
versionName "6.5.4"
versionCode 3020239
versionName "6.5.5"
applicationId "ac.mdiq.podcini.R"
def commit = ""
@ -172,36 +172,37 @@ android {
dependencies {
/** 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
androidTestImplementation composeBom
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
// 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.ui:ui-tooling-preview:1.6.8'
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.8'
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
// Optional - Add full set of material icons
// implementation 'androidx.compose.material:material-icons-extended'
// Optional - Add window size utils
// 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 '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.appcompat:appcompat:1.7.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.media:media:1.7.0"
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.service.Downloader
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.util.Logd
import androidx.test.filters.LargeTest

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.net.download.service
import android.util.Log
import android.webkit.URLUtil
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
class DefaultDownloaderFactory : DownloaderFactory {
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.storage.model.Feed
@ -7,7 +7,6 @@ import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
class DownloadRequest private constructor(
@JvmField val destination: String?,
@JvmField val source: String?,

View File

@ -1,6 +1,5 @@
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.Feed
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 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.net.download.DownloadError
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.queue.SynchronizationQueueSink
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch

View File

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

View File

@ -1,7 +1,5 @@
package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
interface DownloaderFactory {
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.download.DownloadError
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.EpisodeMedia
import ac.mdiq.podcini.util.Logd

View File

@ -1,7 +1,6 @@
package ac.mdiq.podcini.net.download.service
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.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.model.Episode
@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit
import javax.net.ssl.*
import kotlin.concurrent.Volatile
/**
* 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.service.DefaultDownloaderFactory
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.FeedHandlerResult
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh

View File

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

View File

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

View File

@ -20,19 +20,6 @@ import javax.xml.parsers.ParserConfigurationException
import javax.xml.parsers.SAXParserFactory
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)
fun parseFeed(feed: Feed): FeedHandlerResult {
// val tg = TypeGetter()
@ -157,6 +144,19 @@ class FeedHandler {
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 */
class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
@JvmField

View File

@ -5,4 +5,7 @@ import ac.mdiq.podcini.storage.model.Feed
/**
* 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
/** 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

View File

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

View File

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

View File

@ -1,3 +1,6 @@
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
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 {
return "Header [id=$id, size=$size]"
}

View File

@ -1,6 +1,11 @@
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 {
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.util.concurrent.TimeUnit
class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(input!!) {
class VorbisCommentChapterReader(input: InputStream) : VorbisCommentReader(input) {
private val chapters: MutableList<Chapter> = ArrayList()
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()
if (parts.size >= 3) {
try {
val hours = TimeUnit.MILLISECONDS.convert(
parts[0].toLong(), TimeUnit.HOURS)
val minutes = TimeUnit.MILLISECONDS.convert(
parts[1].toLong(), TimeUnit.MINUTES)
val hours = TimeUnit.MILLISECONDS.convert(parts[0].toLong(), TimeUnit.HOURS)
val minutes = TimeUnit.MILLISECONDS.convert(parts[1].toLong(), TimeUnit.MINUTES)
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
return hours + minutes + seconds
} catch (e: NumberFormatException) {
throw VorbisCommentReaderException(e)
}
} else {
throw VorbisCommentReaderException("Invalid time string")
}
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
} else throw VorbisCommentReaderException("Invalid time string")
}
/**
@ -86,9 +80,7 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
try {
val strId = key.substring(8, 10)
return strId.toInt()
} catch (e: NumberFormatException) {
throw VorbisCommentReaderException(e)
}
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
}
throw VorbisCommentReaderException("key is too short ($key)")
}
@ -99,7 +91,6 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
*/
private fun getAttributeTypeFromKey(key: String?): String? {
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
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
class VorbisCommentMetadataReader(input: InputStream?) : VorbisCommentReader(input!!) {
class VorbisCommentMetadataReader(input: InputStream) : VorbisCommentReader(input!!) {
var description: String? = null
private set

View File

@ -171,6 +171,15 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
@Throws(VorbisCommentReaderException::class)
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 {
private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous"
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
// b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
when {
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type ->
state.feed.link = href
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> state.feed.link = href
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
// treat as podlove alternate feed
var title: String? = attributes.getValue(LINK_TITLE)
@ -71,9 +70,8 @@ class Atom : Namespace() {
if (title.isNullOrEmpty()) 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
}
LINK_TYPE_HTML, LINK_TYPE_XHTML -> {}
}
}
LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))

View File

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

View File

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

View File

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

View File

@ -6,8 +6,7 @@ import ac.mdiq.podcini.net.feed.parser.element.SyndElement
import org.xml.sax.Attributes
class PodcastIndex : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState,
attributes: Attributes): SyndElement {
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
when (localName) {
FUNDING -> {
val href: String? = attributes.getValue(URL)
@ -25,7 +24,6 @@ class PodcastIndex : Namespace() {
override fun handleElementEnd(localName: String, state: HandlerState) {
if (state.contentBuf == null) return
val content = state.contentBuf.toString()
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 chapter = Chapter(start, title, link, imageUrl)
currentItem.chapters?.add(chapter)
} catch (e: NumberFormatException) {
Log.e(TAG, "Unable to read chapter", e)
}
} catch (e: NumberFormatException) { Log.e(TAG, "Unable to read chapter", e) }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -14,14 +14,10 @@ import java.util.regex.Pattern
* 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
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
* scrape.
*
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a scrape.
*
* 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:
*
* `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]`
@ -40,7 +36,6 @@ class HtmlToPlainText {
val formatter = FormattingVisitor()
// walk the DOM, and call .head() and .tail() for each node
NodeTraversor.traverse(formatter, element)
return formatter.toString()
}
@ -72,7 +67,6 @@ class HtmlToPlainText {
private fun append(text: String) {
if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n")))
return // don't accumulate long runs of empty spaces
accum.append(text)
}

View File

@ -15,11 +15,7 @@ object URIUtil {
@JvmStatic
fun getURIFromRequestUrl(source: String): URI {
// try without encoding the URI
try {
return URI(source)
} catch (e: URISyntaxException) {
Logd(TAG, "Source is not encoded, encoding now")
}
try { return URI(source) } catch (e: URISyntaxException) { Logd(TAG, "Source is not encoded, encoding now") }
try {
val url = URL(source)
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 (media is EpisodeMedia) {
curMedia = media
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
curEpisode = media.episodeOrFetch()
// curMedia = curEpisode?.media
} else curMedia = media
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
/**
* 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 {
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")
}
/**
* 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) {}
fun skip() {
@ -276,13 +282,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
callback.statusChanged(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)
}
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
class MediaPlayerInfo(
@JvmField val oldPlayerStatus: PlayerStatus?,
@JvmField var playerStatus: PlayerStatus,
@JvmField var playable: Playable?)
companion object {
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
@ -308,8 +311,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
val audioPlaybackSpeed: Float
get() {
try {
return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
try { return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
} catch (e: NumberFormatException) {
Log.e(TAG, Log.getStackTraceString(e))
setPlaybackSpeed(1.0f)

View File

@ -495,7 +495,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
override fun getPosition(): Int {
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()
return retVal
}

View File

@ -1,6 +1,6 @@
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.isNetworkRestricted
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
@ -18,7 +18,6 @@ class ConnectivityActionReceiver : BroadcastReceiver() {
Log.d(TAG, "onReceive called with action: ${intent.action}")
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
Logd(TAG, "Received intent")
ClientConfigurator.initialize(context)
networkChangedDetected(context)
}

View File

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

View File

@ -22,7 +22,6 @@ class MediaButtonReceiver : BroadcastReceiver() {
val extras = intent.extras
Log.d(TAG, "onReceive Extras: $extras")
if (extras == null) return
Log.d(TAG, "onReceive Extras: ${extras.keySet()}")
for (key in extras.keySet()) {
Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}")
@ -40,11 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode)
serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source)
serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0)
try {
ContextCompat.startForegroundService(context, serviceIntent)
} catch (e: Exception) {
e.printStackTrace()
}
try { 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()}]")
getSharedPrefs(context)
WidgetUpdaterWorker.enqueueWork(context)
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
scheduleWorkaround(context)
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
@ -75,9 +74,7 @@ class PlayerWidget : AppWidgetProvider() {
companion object {
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
private const val PREFS_NAME: String = "PlayerWidgetPrefs"
const val DEFAULT_COLOR: Int = -0xd9d3cf
var prefs: SharedPreferences? = null
fun getSharedPrefs(context: Context) {

View File

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

View File

@ -1,6 +1,6 @@
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.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.storage.database
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.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink

View File

@ -1,6 +1,6 @@
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.curQueue
import ac.mdiq.podcini.preferences.UserPreferences

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.ui.actions.actionbutton
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.storage.database.RealmDB.upsertBlk
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.storage.model.Episode
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) {
override val visibility: Int

View File

@ -1,12 +1,11 @@
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.storage.model.Episode
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.view.View
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.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.storage.database.Episodes
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.databinding.MainActivityBinding
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.restartUpdateAlarm
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
@ -91,7 +91,6 @@ class MainActivity : CastEnabledActivity() {
private lateinit var navDrawerFragment: NavDrawerFragment
private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerView: View
// private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var navDrawer: View
private lateinit var dummyView : View
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
@ -104,6 +103,53 @@ class MainActivity : CastEnabledActivity() {
private var lastTheme = 0
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?) {
lastTheme = getNoTitleTheme(this)
setTheme(lastTheme)
@ -131,7 +177,7 @@ class MainActivity : CastEnabledActivity() {
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
PlayerWidget.getSharedPrefs(this@MainActivity)
StatisticsFragment.getSharedPrefs(this@MainActivity)
OnlineFeedViewFragment.getSharedPrefs(this@MainActivity)
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
ItunesTopListLoader.getSharedPrefs(this@MainActivity)
}
@ -213,9 +259,7 @@ class MainActivity : CastEnabledActivity() {
when (workInfo.state) {
WorkInfo.State.RUNNING -> isRefreshingFeeds = true
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
else -> {
// Log.d(TAG, "workInfo.state ${workInfo.state}")
}
else -> {}
}
}
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds))
@ -225,9 +269,7 @@ class MainActivity : CastEnabledActivity() {
private fun observeDownloads() {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
WorkManager.getInstance(this@MainActivity).pruneWork().result.get()
}
withContext(Dispatchers.IO) { WorkManager.getInstance(this@MainActivity).pruneWork().result.get() }
WorkManager.getInstance(this@MainActivity)
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
.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)
// }
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() {
super.onAttachedToWindow()
updateInsets()
@ -303,33 +335,6 @@ class MainActivity : CastEnabledActivity() {
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) {
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
// 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() {
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
@ -507,13 +509,6 @@ class MainActivity : CastEnabledActivity() {
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) {
super.onRestoreInstanceState(savedInstanceState)
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
@ -523,21 +518,10 @@ class MainActivity : CastEnabledActivity() {
super.onStart()
procFlowEvents()
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() {
super.onStop()
// MediaController.releaseFuture(controllerFuture)
cancelFlowEvents()
}
@ -634,7 +618,7 @@ class MainActivity : CastEnabledActivity() {
}
intent.hasExtra(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) -> {
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 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?) {
setTheme(getTheme(this))
super.onCreate(savedInstanceState)
@ -135,17 +157,6 @@ class OpmlImportActivity : AppCompatActivity() {
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 {
super.onCreateOptionsMenu(menu)
val inflater = menuInflater
@ -186,18 +197,6 @@ class OpmlImportActivity : AppCompatActivity() {
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. */
private fun startImport() {
binding.progressBar.visibility = View.VISIBLE

View File

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

View File

@ -2,36 +2,51 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.ServiceStatusHandler
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
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.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.isPlayingVideoLocally
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.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.storage.database.Episodes.setFavorite
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.dialog.MediaPlayerErrorDialog
import ac.mdiq.podcini.ui.dialog.ShareDialog
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.dialog.*
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.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.PixelFormat
import android.graphics.drawable.ColorDrawable
import android.media.AudioManager
@ -39,17 +54,33 @@ import android.os.Build
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.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.animation.*
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.AppCompatActivity
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.window.layout.WindowMetricsCalculator
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
/**
* Activity for playing video files.
@ -57,18 +88,9 @@ import kotlinx.coroutines.launch
@UnstableApi
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 val binding get() = _binding!!
private lateinit var videoEpisodeFragment: VideoEpisodeFragment
var switchToAudioOnly = false
override fun onCreate(savedInstanceState: Bundle?) {
@ -97,11 +119,12 @@ class VideoplayerActivity : CastEnabledActivity() {
val fm = supportFragmentManager
val transaction = fm.beginTransaction()
videoEpisodeFragment = VideoEpisodeFragment()
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
transaction.replace(R.id.main_view, videoEpisodeFragment, "VideoEpisodeFragment")
transaction.commit()
}
private fun setForVideoMode() {
Logd(TAG, "setForVideoMode videoMode: $videoMode")
when (videoMode) {
VideoMode.FULL_SCREEN_VIEW -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
@ -114,7 +137,8 @@ class VideoplayerActivity : CastEnabledActivity() {
VideoMode.WINDOW_VIEW -> {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
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
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
window.setFormat(PixelFormat.TRANSPARENT)
@ -127,6 +151,7 @@ class VideoplayerActivity : CastEnabledActivity() {
@UnstableApi
override fun onResume() {
super.onResume()
setForVideoMode()
switchToAudioOnly = false
if (isCasting) {
val intent = getPlayerActivityIntent(this)
@ -189,7 +214,6 @@ class VideoplayerActivity : CastEnabledActivity() {
}
private fun onEventMainThread(event: FlowEvent.MessageEvent) {
// Logd(TAG, "onEvent($event)")
val errorDialog = MaterialAlertDialogBuilder(this)
errorDialog.setMessage(event.message)
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
@ -369,6 +393,13 @@ class VideoplayerActivity : CastEnabledActivity() {
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() {
private lateinit var dialog: AlertDialog
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 {
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
const val VIDEO_MODE = "Video_Mode"

View File

@ -14,6 +14,9 @@ import android.os.Bundle
class MainActivityStarter(private val context: Context) {
private val intent: Intent = Intent(INTENT)
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 {
intent.setPackage(context.packageName)
@ -24,10 +27,6 @@ class MainActivityStarter(private val context: Context) {
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() {
context.startActivity(getIntent())
}
@ -64,7 +63,6 @@ class MainActivityStarter(private val context: Context) {
fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter {
if (fragmentArgs == null) fragmentArgs = Bundle()
fragmentArgs!!.putBoolean(name, value)
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) {
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 {
intent.setPackage(context.packageName)
@ -23,10 +26,6 @@ import androidx.media3.common.util.UnstableApi
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() {
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.EpisodeInfoFragmentBinding
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.isEpisodeHeadDownloadAllowed
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.setCustomTitle1(newTitle)
feed = upsertBlk(feed) {}
// feed = upsertBlk(feed) {
// it.setCustomTitle1(newTitle)
// }
}
.setNeutralButton(R.string.reset, 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.dateSelectionContainer.alpha = if (checked) 0.5f else 1f
}
if (showMarkPlayed) {
binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
} else {
if (showMarkPlayed) binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
else {
binding.includeMarkedCheckbox.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) {
appPrefs.edit()
.putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected)
.apply()
appPrefs.edit()
.putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir)
.apply()
appPrefs.edit().putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected).apply()
appPrefs.edit().putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir).apply()
}
}

View File

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

View File

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

View File

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

View File

@ -46,10 +46,7 @@ class SwitchQueueDialog(activity: Activity) {
val items = mutableListOf<Episode>()
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))
}
}

View File

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

View File

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

View File

@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
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.appPrefs
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.Downloader
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.discovery.CombinedSearcher
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
* or an Exception will be thrown.
*
*
* 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.
*/
@OptIn(UnstableApi::class)
class OnlineFeedViewFragment : Fragment() {
class OnlineFeedFragment : Fragment() {
private var _binding: OnlineFeedviewFragmentBinding? = null
private val binding get() = _binding!!
@ -147,17 +146,6 @@ class OnlineFeedViewFragment : Fragment() {
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.
*/
@ -392,7 +380,7 @@ class OnlineFeedViewFragment : Fragment() {
try {
val feeds = withContext(Dispatchers.IO) { getFeedList() }
withContext(Dispatchers.Main) {
this@OnlineFeedViewFragment.feeds = feeds
this@OnlineFeedFragment.feeds = feeds
handleUpdatedFeedStatus()
}
} 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)
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,
FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
if (isEnableAutodownload) {
if (feedSource != "VistaGuide" && isEnableAutodownload) {
val autoDownload = binding.autoDownloadCheckBox.isChecked
feed1.preferences!!.autoDownload = autoDownload
val editor = prefs!!.edit()
@ -679,11 +667,22 @@ class OnlineFeedViewFragment : Fragment() {
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) :
AuthenticationDialog(context, titleRes, true, username, password) {
override fun onConfirmed(username: String, password: String) {
this@OnlineFeedViewFragment.username = username
this@OnlineFeedViewFragment.password = password
this@OnlineFeedFragment.username = username
this@OnlineFeedFragment.password = password
startFeedBuilding(feedUrl)
}
}
@ -836,7 +835,7 @@ class OnlineFeedViewFragment : Fragment() {
const val ARG_FEEDURL: String = "arg.feedurl"
const val ARG_WAS_MANUAL_URL: String = "manual_url"
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 PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
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)
}
fun newInstance(feedUrl: String): OnlineFeedViewFragment {
val fragment = OnlineFeedViewFragment()
fun newInstance(feedUrl: String): OnlineFeedFragment {
val fragment = OnlineFeedFragment()
val b = Bundle()
b.putString(ARG_FEEDURL, feedUrl)
fragment.arguments = b

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@ class SearchResultsFragment : Fragment() {
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
val podcast = searchResults[position]
if (podcast.feedUrl != null) {
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
val fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
fragment.feedSource = podcast.source
(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.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.curQueue
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.PodciniHttpClient.setCacheDirectory
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.sync.SyncService
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink

View File

@ -7,13 +7,22 @@
android:background="@color/black"
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
android:id="@+id/videoPlayerContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/topBar"
android:background="@color/black">
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
<VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"

View File

@ -239,8 +239,8 @@
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
<item name="windowActionBarOverlay">true</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="windowActionBar">true</item>
<item name="windowNoTitle">false</item>
</style>
<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.getImageUrl
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.preferences.UserPreferences
import ac.mdiq.podcini.storage.database.Feeds.getFeedList

View File

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

View File

@ -1,5 +1,6 @@
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.EpisodeMedia
import android.content.Context

View File

@ -1,6 +1,6 @@
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.playback.base.InTheatre.curQueue
import ac.mdiq.podcini.preferences.UserPreferences

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.storage
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.storage.database.Queues
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation

View File

@ -1,6 +1,6 @@
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 org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
@ -19,13 +19,13 @@ import java.nio.charset.StandardCharsets
*/
@RunWith(RobolectricTestRunner::class)
class FeedDiscovererTest {
private var fd: OnlineFeedViewFragment.FeedDiscoverer? = null
private var fd: OnlineFeedFragment.FeedDiscoverer? = null
private var testDir: File? = null
@Before
fun setUp() {
fd = OnlineFeedViewFragment.FeedDiscoverer()
fd = OnlineFeedFragment.FeedDiscoverer()
testDir = File(InstrumentationRegistry
.getInstrumentation().targetContext.filesDir, "FeedDiscovererTest")
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
* 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