diff --git a/app/build.gradle b/app/build.gradle index cf636007..27a7c33b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/androidTest/kotlin/ac/test/podcini/service/download/HttpDownloaderTest.kt b/app/src/androidTest/kotlin/ac/test/podcini/service/download/HttpDownloaderTest.kt index 7ec37769..5781607d 100644 --- a/app/src/androidTest/kotlin/ac/test/podcini/service/download/HttpDownloaderTest.kt +++ b/app/src/androidTest/kotlin/ac/test/podcini/service/download/HttpDownloaderTest.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DefaultDownloaderFactory.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DefaultDownloaderFactory.kt index 2dfcc23b..e63ce079 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DefaultDownloaderFactory.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DefaultDownloaderFactory.kt @@ -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? { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadRequest.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt similarity index 99% rename from app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadRequest.kt rename to app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt index 43202a64..88d613ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadRequest.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequest.kt @@ -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?, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt index ffc17bb1..f6227ede 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadRequestCreator.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadServiceInterface.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterface.kt similarity index 97% rename from app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadServiceInterface.kt rename to app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterface.kt index fdaaf6d0..b30db448 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/serviceinterface/DownloadServiceInterface.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterface.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index 60a5cdbb..9029cb5a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/Downloader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/Downloader.kt index dec67418..a785df35 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/Downloader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/Downloader.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloaderFactory.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloaderFactory.kt index 67cbd5be..7ae802b1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloaderFactory.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloaderFactory.kt @@ -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? } \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt index 61356546..5017349d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/HttpDownloader.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/PodciniHttpClient.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/PodciniHttpClient.kt index c4e694c6..6fd470f7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/PodciniHttpClient.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/PodciniHttpClient.kt @@ -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. */ diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index daaa34d8..e4744c69 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt index eafd450a..ef93b6af 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/LocalFeedUpdater.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt index 5cc49207..269d3ad6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/discovery/ItunesTopListLoader.kt @@ -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 = ArrayList() for (i in 0 until entries.length()) { @@ -90,9 +87,8 @@ class ItunesTopListLoader(private val context: Context) { private fun removeSubscribed(suggestedPodcasts: List, subscribedFeeds: List, limit: Int): List { val subscribedPodcastsSet: MutableSet = 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 = ArrayList() for (suggested in suggestedPodcasts) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt index 9a20ccc0..16b5765e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandler.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandlerResult.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandlerResult.kt index b86f4930..3b7bc68e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandlerResult.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/FeedHandlerResult.kt @@ -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, val redirectUrl: String) +class FeedHandlerResult( + @JvmField val feed: Feed, + @JvmField val alternateFeedUrls: Map, + val redirectUrl: String) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/element/AtomText.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/element/AtomText.kt index f199f829..0a1aaa98 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/element/AtomText.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/element/AtomText.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ChapterReader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ChapterReader.kt index 4231c69a..1ff8f624 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ChapterReader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ChapterReader.kt @@ -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 = 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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ID3Reader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ID3Reader.kt index 1b6e7c98..7384f71b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ID3Reader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/ID3Reader.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/Id3MetadataReader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/Id3MetadataReader.kt index 3595156d..ce4aa067 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/Id3MetadataReader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/Id3MetadataReader.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/FrameHeader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/FrameHeader.kt index f3104a61..944c5817 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/FrameHeader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/FrameHeader.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/Header.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/Header.kt index 107e3d0c..05417521 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/Header.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/Header.kt @@ -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]" } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/TagHeader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/TagHeader.kt index 3242909a..c81abf5d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/TagHeader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/id3/model/TagHeader.kt @@ -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 + "]") } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentChapterReader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentChapterReader.kt index 6a81f1ec..96026ea9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentChapterReader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentChapterReader.kt @@ -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 = 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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentHeader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentHeader.kt deleted file mode 100644 index 3e3ea4a7..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentHeader.kt +++ /dev/null @@ -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 + "]") - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentMetadataReader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentMetadataReader.kt index 98080916..81d73599 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentMetadataReader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentMetadataReader.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentReader.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentReader.kt index d071710f..5ad01bed 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentReader.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/media/vorbis/VorbisCommentReader.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Atom.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Atom.kt index a0a5a34e..b551e3d3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Atom.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Atom.kt @@ -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 - } + //A Link such as to a directory such as iTunes + LINK_TYPE_HTML, LINK_TYPE_XHTML -> {} } } LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, "")) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Content.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Content.kt index 1411869a..6b2b6aff 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Content.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Content.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Itunes.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Itunes.kt index 7345350c..25e2fd57 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Itunes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Itunes.kt @@ -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 - } + // this is the feed image + // prefer to all other images + 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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Media.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Media.kt index 7e94c408..3d2bc019 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Media.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/Media.kt @@ -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 -> { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/PodcastIndex.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/PodcastIndex.kt index 4b8d257c..e5b449cd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/PodcastIndex.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/PodcastIndex.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/SimpleChapters.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/SimpleChapters.kt index 7d758624..a2c09bcf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/SimpleChapters.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/SimpleChapters.kt @@ -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) } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/YouTube.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/YouTube.kt index ec803e54..0e3ba9e6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/YouTube.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/namespace/YouTube.kt @@ -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 { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DateUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DateUtils.kt index df8500be..44282f80 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DateUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DateUtils.kt @@ -8,7 +8,6 @@ import java.text.ParsePosition import java.text.SimpleDateFormat import java.util.* - /** * Parses several date formats. */ diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DurationParser.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DurationParser.kt index 2ffef3f5..9a59199f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DurationParser.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/DurationParser.kt @@ -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()) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/MimeTypeUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/MimeTypeUtils.kt index 11e44b16..559d871a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/MimeTypeUtils.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/parser/utils/MimeTypeUtils.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/HtmlToPlainText.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/HtmlToPlainText.kt index c66c2c0b..2db659f0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/HtmlToPlainText.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/HtmlToPlainText.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/URIUtil.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/URIUtil.kt index f42c30d2..5d54d921 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/utils/URIUtil.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/utils/URIUtil.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt index 7a92f62c..53f13ac4 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/PlaybackServiceStarter.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index e7712d1a..283573a7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index c55c01c0..7bc40868 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/ConnectivityActionReceiver.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/ConnectivityActionReceiver.kt index a41e17d7..52a8cd9b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/ConnectivityActionReceiver.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/ConnectivityActionReceiver.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/FeedUpdateReceiver.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/FeedUpdateReceiver.kt index 4740252d..9f4fe514 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/FeedUpdateReceiver.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/FeedUpdateReceiver.kt @@ -16,7 +16,6 @@ class FeedUpdateReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { Logd(TAG, "Received intent") ClientConfigurator.initialize(context) - FeedUpdateManager.runOnce(context) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt index 6961110d..dc5acebf 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/MediaButtonReceiver.kt @@ -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() } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt index bd51a13a..1f91ae40 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PlayerWidget.kt @@ -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) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PowerConnectionReceiver.kt b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PowerConnectionReceiver.kt index 4719a3c6..c19f4eee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/receiver/PowerConnectionReceiver.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/receiver/PowerConnectionReceiver.kt @@ -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") diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index b72c3f4f..9129ae9a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt index 986f77e9..928aa03b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Episodes.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt index 48426d44..a5e54441 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Queues.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/CancelDownloadActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/CancelDownloadActionButton.kt index 1f671045..727c2369 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/CancelDownloadActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/CancelDownloadActionButton.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt index e77acd08..b65ad2c9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/DownloadActionButton.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt index 723a0507..3b2e7f13 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMultiSelectHandler.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMultiSelectHandler.kt index 8b8714c7..3809e7af 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMultiSelectHandler.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/handler/EpisodeMultiSelectHandler.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt index 339f8484..7376e185 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -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 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) } @@ -155,7 +201,7 @@ class MainActivity : CastEnabledActivity() { if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { // Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show() // requestPostNotificationPermission() - requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) } // Consume navigation bar insets - we apply them in setPlayerVisible() @@ -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 -> @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt index 23ccf177..7a89f32a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/OpmlImportActivity.kt @@ -54,6 +54,28 @@ class OpmlImportActivity : AppCompatActivity() { private var listAdapter: ArrayAdapter? = null private var readElements: ArrayList? = null + private val titleList: List + get() { + val result: MutableList = 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 - get() { - val result: MutableList = 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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt index caa11d1b..07875693 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/SplashActivity.kt @@ -39,6 +39,5 @@ class SplashActivity : Activity() { finish() } } - } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 0bfffbbb..7b9b1fb1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -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? + get() = playbackService?.mPlayer?.getVideoSize() + } + } + companion object { private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous" const val VIDEO_MODE = "Video_Mode" diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/MainActivityStarter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/MainActivityStarter.kt index 7a29c887..b4ee91d5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/MainActivityStarter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/MainActivityStarter.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt index 067b421b..560cf162 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt index 79061dac..aeb63521 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/adapter/EpisodesAdapter.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt index f013b8c4..098032d6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/CustomFeedNameDialog.kt @@ -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) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DatesFilterDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DatesFilterDialog.kt index 2ba1dd45..d8fb260b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DatesFilterDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/DatesFilterDialog.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt index 8e12d1ff..08c64540 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/FeedSortDialog.kt @@ -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() } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt index 5f288b2e..873eeed5 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/ShareDialog.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt index e54e2dbf..24e5a7ab 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SleepTimerDialog.kt @@ -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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwipeActionsDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwipeActionsDialog.kt index 70a66aab..869a9db0 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwipeActionsDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwipeActionsDialog.kt @@ -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() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt index 4e6f12fa..068b316d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/SwitchQueueDialog.kt @@ -46,10 +46,7 @@ class SwitchQueueDialog(activity: Activity) { val items = mutableListOf() 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)) } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt index 6b199124..c16e005b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/TagSettingsDialog.kt @@ -25,9 +25,7 @@ class TagSettingsDialog : DialogFragment() { private var _binding: EditTagsDialogBinding? = null private val binding get() = _binding!! - private var feedList: MutableList = mutableListOf() - private lateinit var displayedTags: MutableList 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() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index b64ed2b2..89d06478 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -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 - private lateinit var settingCode: BooleanArray + private val selectedSpeeds: MutableList 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 } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt index a2feba71..de9efd0e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/DownloadsFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt similarity index 98% rename from app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt rename to app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt index 753fde48..9cdf9fcd 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedViewFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineFeedFragment.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt index 44aef33e..c36f0017 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/OnlineSearchFragment.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt index 96c12001..92053ae7 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/QuickDiscoveryFragment.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 3fe15397..df7f3f06 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -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 } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt index 0585572c..84e966ee 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchResultsFragment.kt @@ -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) } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt deleted file mode 100644 index 004f53d7..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt +++ /dev/null @@ -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? - get() = playbackService?.mPlayer?.getVideoSize() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt deleted file mode 100644 index 49e37714..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/AspectRatioVideoView.kt +++ /dev/null @@ -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" - } -} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/EpisodeViewHolder.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/EpisodeViewHolder.kt index 64f8a79c..f45ac122 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/EpisodeViewHolder.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/EpisodeViewHolder.kt @@ -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 diff --git a/app/src/main/kotlin/ac/mdiq/podcini/util/config/ClientConfigurator.kt b/app/src/main/kotlin/ac/mdiq/podcini/util/config/ClientConfigurator.kt index d0465be2..4b011c5a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/util/config/ClientConfigurator.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/util/config/ClientConfigurator.kt @@ -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 diff --git a/app/src/main/res/layout/video_episode_fragment.xml b/app/src/main/res/layout/video_episode_fragment.xml index 2d0943b2..fdd78b6f 100644 --- a/app/src/main/res/layout/video_episode_fragment.xml +++ b/app/src/main/res/layout/video_episode_fragment.xml @@ -7,13 +7,22 @@ android:background="@color/black" android:id="@+id/videoEpisodeContainer"> + + - true - false - true + true + false