6.5.5 commit
This commit is contained in:
parent
782c582db6
commit
c2977301f6
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020238
|
versionCode 3020239
|
||||||
versionName "6.5.4"
|
versionName "6.5.5"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
@ -172,36 +172,37 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
/** Desugaring for using VistaGuide **/
|
/** Desugaring for using VistaGuide **/
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.1'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
|
||||||
|
|
||||||
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
|
def composeBom = platform('androidx.compose:compose-bom:2024.09.00')
|
||||||
implementation composeBom
|
implementation composeBom
|
||||||
androidTestImplementation composeBom
|
androidTestImplementation composeBom
|
||||||
|
|
||||||
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
|
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
|
||||||
|
|
||||||
// implementation 'androidx.compose.material3:material3:1.2.0'
|
// implementation 'androidx.compose.material3:material3:1.2.0'
|
||||||
implementation 'androidx.compose.material:material:1.6.8'
|
implementation 'androidx.compose.material:material:1.7.0'
|
||||||
// implementation 'androidx.compose.foundation:foundation:1.6.2'
|
// implementation 'androidx.compose.foundation:foundation:1.6.2'
|
||||||
|
|
||||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.6.8'
|
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
|
||||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.8'
|
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
|
||||||
|
|
||||||
// Optional - Add full set of material icons
|
// Optional - Add full set of material icons
|
||||||
// implementation 'androidx.compose.material:material-icons-extended'
|
// implementation 'androidx.compose.material:material-icons-extended'
|
||||||
// Optional - Add window size utils
|
// Optional - Add window size utils
|
||||||
// implementation 'androidx.compose.material3:material3-window-size-class'
|
// implementation 'androidx.compose.material3:material3-window-size-class'
|
||||||
|
|
||||||
implementation 'androidx.activity:activity-compose:1.9.1'
|
implementation 'androidx.activity:activity-compose:1.9.2'
|
||||||
|
implementation 'androidx.window:window:1.3.0'
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.13.1"
|
implementation "androidx.core:core-ktx:1.13.1"
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||||
|
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5"
|
||||||
implementation "androidx.annotation:annotation:1.8.2"
|
implementation "androidx.annotation:annotation:1.8.2"
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
||||||
implementation "androidx.fragment:fragment-ktx:1.8.2"
|
implementation "androidx.fragment:fragment-ktx:1.8.3"
|
||||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||||
// implementation "androidx.media:media:1.7.0"
|
// implementation "androidx.media:media:1.7.0"
|
||||||
implementation "androidx.media3:media3-exoplayer:1.4.1"
|
implementation "androidx.media3:media3-exoplayer:1.4.1"
|
||||||
|
|
|
@ -3,7 +3,7 @@ package de.test.podcini.service.download
|
||||||
import ac.mdiq.podcini.net.download.DownloadError
|
import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.download.service.Downloader
|
import ac.mdiq.podcini.net.download.service.Downloader
|
||||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.init
|
import ac.mdiq.podcini.preferences.UserPreferences.init
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import androidx.test.filters.LargeTest
|
import androidx.test.filters.LargeTest
|
||||||
|
|
|
@ -2,7 +2,6 @@ package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.webkit.URLUtil
|
import android.webkit.URLUtil
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
|
|
||||||
class DefaultDownloaderFactory : DownloaderFactory {
|
class DefaultDownloaderFactory : DownloaderFactory {
|
||||||
override fun create(request: DownloadRequest): Downloader? {
|
override fun create(request: DownloadRequest): Downloader? {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package ac.mdiq.podcini.net.download.serviceinterface
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
|
@ -7,7 +7,6 @@ import android.os.Bundle
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
|
||||||
|
|
||||||
class DownloadRequest private constructor(
|
class DownloadRequest private constructor(
|
||||||
@JvmField val destination: String?,
|
@JvmField val destination: String?,
|
||||||
@JvmField val source: String?,
|
@JvmField val source: String?,
|
|
@ -1,6 +1,5 @@
|
||||||
package ac.mdiq.podcini.net.download.service
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
|
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package ac.mdiq.podcini.net.download.serviceinterface
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
import ac.mdiq.podcini.net.download.DownloadStatus
|
|
@ -3,8 +3,6 @@ package ac.mdiq.podcini.net.download.service
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.DownloadError
|
import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ac.mdiq.podcini.net.download.service
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||||
import ac.mdiq.podcini.util.config.ClientConfig
|
import ac.mdiq.podcini.util.config.ClientConfig
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package ac.mdiq.podcini.net.download.service
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
|
|
||||||
interface DownloaderFactory {
|
interface DownloaderFactory {
|
||||||
fun create(request: DownloadRequest): Downloader?
|
fun create(request: DownloadRequest): Downloader?
|
||||||
}
|
}
|
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils.parse
|
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils.parse
|
||||||
import ac.mdiq.podcini.net.download.DownloadError
|
import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package ac.mdiq.podcini.net.download.service
|
package ac.mdiq.podcini.net.download.service
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder.encode
|
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder.encode
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
|
||||||
import ac.mdiq.podcini.net.utils.URIUtil
|
import ac.mdiq.podcini.net.utils.URIUtil
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
|
@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit
|
||||||
import javax.net.ssl.*
|
import javax.net.ssl.*
|
||||||
import kotlin.concurrent.Volatile
|
import kotlin.concurrent.Volatile
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides access to a HttpClient singleton.
|
* Provides access to a HttpClient singleton.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.DownloadError
|
import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory
|
import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory
|
||||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||||
import ac.mdiq.podcini.net.feed.parser.FeedHandler
|
import ac.mdiq.podcini.net.feed.parser.FeedHandler
|
||||||
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
|
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh
|
||||||
|
|
|
@ -85,20 +85,16 @@ object LocalFeedUpdater {
|
||||||
oldItem?.updateFromOther(newItem) ?: newItems.add(newItem)
|
oldItem?.updateFromOther(newItem) ?: newItems.add(newItem)
|
||||||
updaterProgressListener?.onLocalFileScanned(i, mediaFiles.size)
|
updaterProgressListener?.onLocalFileScanned(i, mediaFiles.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove feed items without corresponding file
|
// remove feed items without corresponding file
|
||||||
val it = newItems.iterator()
|
val it = newItems.iterator()
|
||||||
while (it.hasNext()) {
|
while (it.hasNext()) {
|
||||||
val feedItem = it.next()
|
val feedItem = it.next()
|
||||||
if (!mediaFileNames.contains(feedItem.link)) it.remove()
|
if (!mediaFileNames.contains(feedItem.link)) it.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri)
|
if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri)
|
||||||
|
|
||||||
if (feed.preferences != null) feed.preferences!!.autoDownload = false
|
if (feed.preferences != null) feed.preferences!!.autoDownload = false
|
||||||
feed.description = context.getString(R.string.local_feed_description)
|
feed.description = context.getString(R.string.local_feed_description)
|
||||||
feed.author = context.getString(R.string.local_folder)
|
feed.author = context.getString(R.string.local_folder)
|
||||||
|
|
||||||
Feeds.updateFeed(context, feed, true)
|
Feeds.updateFeed(context, feed, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,13 +108,11 @@ object LocalFeedUpdater {
|
||||||
if (iconLocation == file.name) return file.uri.toString()
|
if (iconLocation == file.name) return file.uri.toString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the first image in the folder if existing
|
// use the first image in the folder if existing
|
||||||
for (file in files) {
|
for (file in files) {
|
||||||
val mime = file.type
|
val mime = file.type
|
||||||
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString()
|
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// use default icon as fallback
|
// use default icon as fallback
|
||||||
return Feed.PREFIX_GENERATIVE_COVER + folderUri
|
return Feed.PREFIX_GENERATIVE_COVER + folderUri
|
||||||
}
|
}
|
||||||
|
@ -134,12 +128,10 @@ object LocalFeedUpdater {
|
||||||
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
|
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
|
||||||
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
|
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
|
||||||
item.disableAutoDownload()
|
item.disableAutoDownload()
|
||||||
|
|
||||||
val size = file.length
|
val size = file.length
|
||||||
val media = EpisodeMedia(0, item, 0, 0, size, file.type,
|
val media = EpisodeMedia(0, item, 0, 0, size, file.type,
|
||||||
file.uri.toString(), file.uri.toString(), false, null, 0, 0)
|
file.uri.toString(), file.uri.toString(), false, null, 0, 0)
|
||||||
item.media = media
|
item.media = media
|
||||||
|
|
||||||
for (existingItem in feed.episodes) {
|
for (existingItem in feed.episodes) {
|
||||||
if (existingItem.media != null && existingItem.media!!.downloadUrl == file.uri.toString()
|
if (existingItem.media != null && existingItem.media!!.downloadUrl == file.uri.toString()
|
||||||
&& file.length == existingItem.media!!.size) {
|
&& file.length == existingItem.media!!.size) {
|
||||||
|
@ -148,13 +140,8 @@ object LocalFeedUpdater {
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Did not find existing item. Scan metadata.
|
// Did not find existing item. Scan metadata.
|
||||||
try {
|
try { loadMetadata(item, file, context) } catch (e: Exception) { item.setDescriptionIfLonger(e.message) }
|
||||||
loadMetadata(item, file, context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
item.setDescriptionIfLonger(e.message)
|
|
||||||
}
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,19 +152,16 @@ object LocalFeedUpdater {
|
||||||
if (!dateStr.isNullOrEmpty() && "19040101T000000.000Z" != dateStr) {
|
if (!dateStr.isNullOrEmpty() && "19040101T000000.000Z" != dateStr) {
|
||||||
try {
|
try {
|
||||||
val simpleDateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault())
|
val simpleDateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault())
|
||||||
item.pubDate = simpleDateFormat.parse(dateStr).time
|
item.pubDate = simpleDateFormat.parse(dateStr)?.time ?: 0L
|
||||||
} catch (parseException: ParseException) {
|
} catch (parseException: ParseException) {
|
||||||
val date = DateUtils.parse(dateStr)
|
val date = DateUtils.parse(dateStr)
|
||||||
if (date != null) item.pubDate = date.time
|
if (date != null) item.pubDate = date.time
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||||
if (!title.isNullOrEmpty()) item.title = title
|
if (!title.isNullOrEmpty()) item.title = title
|
||||||
|
|
||||||
val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||||
item.media!!.setDuration(durationStr!!.toLong().toInt())
|
item.media!!.setDuration(durationStr!!.toLong().toInt())
|
||||||
|
|
||||||
item.media!!.hasEmbeddedPicture = (mediaMetadataRetriever.embeddedPicture != null)
|
item.media!!.hasEmbeddedPicture = (mediaMetadataRetriever.embeddedPicture != null)
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||||
|
@ -188,30 +172,23 @@ object LocalFeedUpdater {
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||||
val reader = VorbisCommentMetadataReader(inputStream)
|
val reader = VorbisCommentMetadataReader(inputStream)
|
||||||
reader.readInputStream()
|
reader.readInputStream()
|
||||||
item.setDescriptionIfLonger(reader.description)
|
item.setDescriptionIfLonger(reader.description)
|
||||||
}
|
}
|
||||||
} catch (e2: IOException) {
|
} catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
} catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
|
||||||
} catch (e2: VorbisCommentReaderException) {
|
|
||||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
|
||||||
}
|
|
||||||
} catch (e: ID3ReaderException) {
|
} catch (e: ID3ReaderException) {
|
||||||
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||||
val reader = VorbisCommentMetadataReader(inputStream)
|
val reader = VorbisCommentMetadataReader(inputStream)
|
||||||
reader.readInputStream()
|
reader.readInputStream()
|
||||||
item.setDescriptionIfLonger(reader.description)
|
item.setDescriptionIfLonger(reader.description)
|
||||||
}
|
}
|
||||||
} catch (e2: IOException) {
|
} catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
} catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
|
||||||
} catch (e2: VorbisCommentReaderException) {
|
|
||||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -236,16 +213,12 @@ object LocalFeedUpdater {
|
||||||
*/
|
*/
|
||||||
private fun mustReportDownloadSuccessful(feed: Feed): Boolean {
|
private fun mustReportDownloadSuccessful(feed: Feed): Boolean {
|
||||||
val downloadResults = LogsAndStats.getFeedDownloadLog(feed.id).toMutableList()
|
val downloadResults = LogsAndStats.getFeedDownloadLog(feed.id).toMutableList()
|
||||||
|
|
||||||
// report success if never reported before
|
// report success if never reported before
|
||||||
if (downloadResults.isEmpty()) return true
|
if (downloadResults.isEmpty()) return true
|
||||||
|
|
||||||
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
|
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
|
||||||
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())
|
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())
|
||||||
}
|
}
|
||||||
|
|
||||||
val lastDownloadResult = downloadResults[downloadResults.size - 1]
|
val lastDownloadResult = downloadResults[downloadResults.size - 1]
|
||||||
|
|
||||||
// report success if the last update was not successful
|
// report success if the last update was not successful
|
||||||
// (avoid logging success again if the last update was ok)
|
// (avoid logging success again if the last update was ok)
|
||||||
return !lastDownloadResult.isSuccessful
|
return !lastDownloadResult.isSuccessful
|
||||||
|
|
|
@ -26,8 +26,7 @@ class ItunesTopListLoader(private val context: Context) {
|
||||||
var loadCountry = country
|
var loadCountry = country
|
||||||
if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country
|
if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country
|
||||||
|
|
||||||
feedString = try {
|
feedString = try { getTopListFeed(client, loadCountry)
|
||||||
getTopListFeed(client, loadCountry)
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US")
|
if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US")
|
||||||
else throw e
|
else throw e
|
||||||
|
@ -60,9 +59,7 @@ class ItunesTopListLoader(private val context: Context) {
|
||||||
try {
|
try {
|
||||||
feed = result.getJSONObject("feed")
|
feed = result.getJSONObject("feed")
|
||||||
entries = feed.getJSONArray("entry")
|
entries = feed.getJSONArray("entry")
|
||||||
} catch (e: JSONException) {
|
} catch (e: JSONException) { return ArrayList() }
|
||||||
return ArrayList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val results: MutableList<PodcastSearchResult> = ArrayList()
|
val results: MutableList<PodcastSearchResult> = ArrayList()
|
||||||
for (i in 0 until entries.length()) {
|
for (i in 0 until entries.length()) {
|
||||||
|
@ -90,9 +87,8 @@ class ItunesTopListLoader(private val context: Context) {
|
||||||
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
|
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
|
||||||
val subscribedPodcastsSet: MutableSet<String> = HashSet()
|
val subscribedPodcastsSet: MutableSet<String> = HashSet()
|
||||||
for (subscribedFeed in subscribedFeeds) {
|
for (subscribedFeed in subscribedFeeds) {
|
||||||
if (subscribedFeed.title != null && subscribedFeed.author != null) {
|
if (subscribedFeed.title != null && subscribedFeed.author != null)
|
||||||
subscribedPodcastsSet.add(subscribedFeed.title!!.trim { it <= ' ' } + " - " + subscribedFeed.author!!.trim { it <= ' ' })
|
subscribedPodcastsSet.add(subscribedFeed.title!!.trim { it <= ' ' } + " - " + subscribedFeed.author!!.trim { it <= ' ' })
|
||||||
}
|
|
||||||
}
|
}
|
||||||
val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList()
|
val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList()
|
||||||
for (suggested in suggestedPodcasts) {
|
for (suggested in suggestedPodcasts) {
|
||||||
|
|
|
@ -20,19 +20,6 @@ import javax.xml.parsers.ParserConfigurationException
|
||||||
import javax.xml.parsers.SAXParserFactory
|
import javax.xml.parsers.SAXParserFactory
|
||||||
|
|
||||||
class FeedHandler {
|
class FeedHandler {
|
||||||
enum class Type {
|
|
||||||
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun fromName(name: String): Type {
|
|
||||||
for (t in entries) {
|
|
||||||
if (t.name == name) return t
|
|
||||||
}
|
|
||||||
return INVALID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class)
|
@Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class)
|
||||||
fun parseFeed(feed: Feed): FeedHandlerResult {
|
fun parseFeed(feed: Feed): FeedHandlerResult {
|
||||||
// val tg = TypeGetter()
|
// val tg = TypeGetter()
|
||||||
|
@ -157,6 +144,19 @@ class FeedHandler {
|
||||||
return reader
|
return reader
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): Type {
|
||||||
|
for (t in entries) {
|
||||||
|
if (t.name == name) return t
|
||||||
|
}
|
||||||
|
return INVALID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Superclass for all SAX Handlers which process Syndication formats */
|
/** Superclass for all SAX Handlers which process Syndication formats */
|
||||||
class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
|
class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
|
||||||
@JvmField
|
@JvmField
|
||||||
|
|
|
@ -5,4 +5,7 @@ import ac.mdiq.podcini.storage.model.Feed
|
||||||
/**
|
/**
|
||||||
* Container for results returned by the Feed parser
|
* Container for results returned by the Feed parser
|
||||||
*/
|
*/
|
||||||
class FeedHandlerResult(@JvmField val feed: Feed, @JvmField val alternateFeedUrls: Map<String, String>, val redirectUrl: String)
|
class FeedHandlerResult(
|
||||||
|
@JvmField val feed: Feed,
|
||||||
|
@JvmField val alternateFeedUrls: Map<String, String>,
|
||||||
|
val redirectUrl: String)
|
||||||
|
|
|
@ -4,7 +4,10 @@ import androidx.core.text.HtmlCompat
|
||||||
import ac.mdiq.podcini.net.feed.parser.namespace.Namespace
|
import ac.mdiq.podcini.net.feed.parser.namespace.Namespace
|
||||||
|
|
||||||
/** Represents Atom Element which contains text (content, title, summary). */
|
/** Represents Atom Element which contains text (content, title, summary). */
|
||||||
class AtomText(name: String?, namespace: Namespace?, private val type: String?) : SyndElement(name!!, namespace!!) {
|
class AtomText(
|
||||||
|
name: String,
|
||||||
|
namespace: Namespace,
|
||||||
|
private val type: String?) : SyndElement(name, namespace) {
|
||||||
|
|
||||||
private var content: String? = null
|
private var content: String? = null
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ import java.net.URLDecoder
|
||||||
* Reads ID3 chapters.
|
* Reads ID3 chapters.
|
||||||
* See https://id3.org/id3v2-chapters-1.0
|
* See https://id3.org/id3v2-chapters-1.0
|
||||||
*/
|
*/
|
||||||
class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
class ChapterReader(input: CountingInputStream) : ID3Reader(input) {
|
||||||
private val chapters: MutableList<Chapter> = ArrayList()
|
private val chapters: MutableList<Chapter> = ArrayList()
|
||||||
|
|
||||||
@Throws(IOException::class, ID3ReaderException::class)
|
@Throws(IOException::class, ID3ReaderException::class)
|
||||||
|
@ -23,9 +23,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
||||||
val chapter = readChapter(frameHeader)
|
val chapter = readChapter(frameHeader)
|
||||||
Logd(TAG, "Chapter done: $chapter")
|
Logd(TAG, "Chapter done: $chapter")
|
||||||
chapters.add(chapter)
|
chapters.add(chapter)
|
||||||
} else {
|
} else super.readFrame(frameHeader)
|
||||||
super.readFrame(frameHeader)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, ID3ReaderException::class)
|
@Throws(IOException::class, ID3ReaderException::class)
|
||||||
|
@ -63,9 +61,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
||||||
val decodedLink = URLDecoder.decode(url, "ISO-8859-1")
|
val decodedLink = URLDecoder.decode(url, "ISO-8859-1")
|
||||||
chapter.link = decodedLink
|
chapter.link = decodedLink
|
||||||
Logd(TAG, "Found link: " + chapter.link)
|
Logd(TAG, "Found link: " + chapter.link)
|
||||||
} catch (iae: IllegalArgumentException) {
|
} catch (iae: IllegalArgumentException) { Log.w(TAG, "Bad URL found in ID3 data") }
|
||||||
Log.w(TAG, "Bad URL found in ID3 data")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FRAME_ID_PICTURE -> {
|
FRAME_ID_PICTURE -> {
|
||||||
val encoding = readByte()
|
val encoding = readByte()
|
||||||
|
|
|
@ -17,6 +17,8 @@ import java.nio.charset.MalformedInputException
|
||||||
*/
|
*/
|
||||||
open class ID3Reader(private val inputStream: CountingInputStream) {
|
open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
private var tagHeader: TagHeader? = null
|
private var tagHeader: TagHeader? = null
|
||||||
|
val position: Int
|
||||||
|
get() = inputStream.count
|
||||||
|
|
||||||
@Throws(IOException::class, ID3ReaderException::class)
|
@Throws(IOException::class, ID3ReaderException::class)
|
||||||
fun readInputStream() {
|
fun readInputStream() {
|
||||||
|
@ -38,16 +40,12 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
skipBytes(frameHeader.size)
|
skipBytes(frameHeader.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
val position: Int
|
|
||||||
get() = inputStream.count
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip a certain number of bytes on the given input stream.
|
* Skip a certain number of bytes on the given input stream.
|
||||||
*/
|
*/
|
||||||
@Throws(IOException::class, ID3ReaderException::class)
|
@Throws(IOException::class, ID3ReaderException::class)
|
||||||
fun skipBytes(number: Int) {
|
fun skipBytes(number: Int) {
|
||||||
if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes")
|
if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes")
|
||||||
|
|
||||||
IOUtils.skipFully(inputStream, number.toLong())
|
IOUtils.skipFully(inputStream, number.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +96,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
val id = readPlainBytesToString(FRAME_ID_LENGTH)
|
val id = readPlainBytesToString(FRAME_ID_LENGTH)
|
||||||
var size = readInt()
|
var size = readInt()
|
||||||
if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size)
|
if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size)
|
||||||
|
|
||||||
val flags = readShort()
|
val flags = readShort()
|
||||||
return FrameHeader(id, size, flags)
|
return FrameHeader(id, size, flags)
|
||||||
}
|
}
|
||||||
|
@ -106,13 +103,11 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
private fun unsynchsafe(inVal: Int): Int {
|
private fun unsynchsafe(inVal: Int): Int {
|
||||||
var out = 0
|
var out = 0
|
||||||
var mask = 0x7F000000
|
var mask = 0x7F000000
|
||||||
|
|
||||||
while (mask != 0) {
|
while (mask != 0) {
|
||||||
out = out shr 1
|
out = out shr 1
|
||||||
out = out or (inVal and mask)
|
out = out or (inVal and mask)
|
||||||
mask = mask shr 8
|
mask = mask shr 8
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +156,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
val c = readByte()
|
val c = readByte()
|
||||||
bytesRead++
|
bytesRead++
|
||||||
if (c.toInt() == 0) break
|
if (c.toInt() == 0) break
|
||||||
|
|
||||||
bytes.write(c.toInt())
|
bytes.write(c.toInt())
|
||||||
}
|
}
|
||||||
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
||||||
|
@ -191,11 +185,7 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||||
val c = readByte()
|
val c = readByte()
|
||||||
if (c.toInt() != 0) bytes.write(c.toInt())
|
if (c.toInt() != 0) bytes.write(c.toInt())
|
||||||
}
|
}
|
||||||
return try {
|
return try { charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString() } catch (e: MalformedInputException) { "" }
|
||||||
charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
|
||||||
} catch (e: MalformedInputException) {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import java.io.IOException
|
||||||
/**
|
/**
|
||||||
* Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support.
|
* Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support.
|
||||||
*/
|
*/
|
||||||
class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
class Id3MetadataReader(input: CountingInputStream) : ID3Reader(input) {
|
||||||
var comment: String? = null
|
var comment: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
@ -20,9 +20,7 @@ class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
||||||
val shortDescription = readEncodedString(encoding, frameHeader.size - 4)
|
val shortDescription = readEncodedString(encoding, frameHeader.size - 4)
|
||||||
val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt())
|
val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt())
|
||||||
comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription
|
comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription
|
||||||
} else {
|
} else super.readFrame(frameHeader)
|
||||||
super.readFrame(frameHeader)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||||
|
|
||||||
class FrameHeader(id: String?, size: Int, flags: Short) : Header(id!!, size)
|
class FrameHeader(
|
||||||
|
id: String,
|
||||||
|
size: Int,
|
||||||
|
flags: Short) : Header(id, size)
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||||
|
|
||||||
abstract class Header internal constructor(@JvmField val id: String, @JvmField val size: Int) {
|
abstract class Header internal constructor(
|
||||||
|
@JvmField val id: String,
|
||||||
|
@JvmField val size: Int) {
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Header [id=$id, size=$size]"
|
return "Header [id=$id, size=$size]"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||||
|
|
||||||
class TagHeader(id: String?, size: Int, @JvmField val version: Short, private val flags: Byte) : Header(id!!, size) {
|
class TagHeader(
|
||||||
|
id: String,
|
||||||
|
size: Int,
|
||||||
|
@JvmField val version: Short,
|
||||||
|
private val flags: Byte) : Header(id, size) {
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return ("TagHeader [version=" + version + ", flags=" + flags + ", id=" + id + ", size=" + size + "]")
|
return ("TagHeader [version=" + version + ", flags=" + flags + ", id=" + id + ", size=" + size + "]")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import ac.mdiq.podcini.util.Logd
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(input!!) {
|
class VorbisCommentChapterReader(input: InputStream) : VorbisCommentReader(input) {
|
||||||
private val chapters: MutableList<Chapter> = ArrayList()
|
private val chapters: MutableList<Chapter> = ArrayList()
|
||||||
|
|
||||||
public override fun handles(key: String?): Boolean {
|
public override fun handles(key: String?): Boolean {
|
||||||
|
@ -59,19 +59,13 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
||||||
val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||||
if (parts.size >= 3) {
|
if (parts.size >= 3) {
|
||||||
try {
|
try {
|
||||||
val hours = TimeUnit.MILLISECONDS.convert(
|
val hours = TimeUnit.MILLISECONDS.convert(parts[0].toLong(), TimeUnit.HOURS)
|
||||||
parts[0].toLong(), TimeUnit.HOURS)
|
val minutes = TimeUnit.MILLISECONDS.convert(parts[1].toLong(), TimeUnit.MINUTES)
|
||||||
val minutes = TimeUnit.MILLISECONDS.convert(
|
|
||||||
parts[1].toLong(), TimeUnit.MINUTES)
|
|
||||||
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
|
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
|
||||||
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
|
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
|
||||||
return hours + minutes + seconds
|
return hours + minutes + seconds
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
|
||||||
throw VorbisCommentReaderException(e)
|
} else throw VorbisCommentReaderException("Invalid time string")
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw VorbisCommentReaderException("Invalid time string")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,9 +80,7 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
||||||
try {
|
try {
|
||||||
val strId = key.substring(8, 10)
|
val strId = key.substring(8, 10)
|
||||||
return strId.toInt()
|
return strId.toInt()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
|
||||||
throw VorbisCommentReaderException(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
throw VorbisCommentReaderException("key is too short ($key)")
|
throw VorbisCommentReaderException("key is too short ($key)")
|
||||||
}
|
}
|
||||||
|
@ -99,7 +91,6 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
||||||
*/
|
*/
|
||||||
private fun getAttributeTypeFromKey(key: String?): String? {
|
private fun getAttributeTypeFromKey(key: String?): String? {
|
||||||
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
|
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 + "]")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.net.feed.parser.media.vorbis
|
||||||
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
class VorbisCommentMetadataReader(input: InputStream?) : VorbisCommentReader(input!!) {
|
class VorbisCommentMetadataReader(input: InputStream) : VorbisCommentReader(input!!) {
|
||||||
var description: String? = null
|
var description: String? = null
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
|
@ -171,6 +171,15 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
||||||
@Throws(VorbisCommentReaderException::class)
|
@Throws(VorbisCommentReaderException::class)
|
||||||
protected abstract fun onContentVectorValue(key: String?, value: String?)
|
protected abstract fun onContentVectorValue(key: String?, value: String?)
|
||||||
|
|
||||||
|
internal class VorbisCommentHeader(
|
||||||
|
val vendorString: String,
|
||||||
|
val userCommentLength: Long) {
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return ("VorbisCommentHeader [vendorString=" + vendorString + ", userCommentLength=" + userCommentLength + "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous"
|
private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous"
|
||||||
private const val FIRST_OGG_PAGE_LENGTH = 58
|
private const val FIRST_OGG_PAGE_LENGTH = 58
|
||||||
|
|
|
@ -53,8 +53,7 @@ class Atom : Namespace() {
|
||||||
// a) no type-attribute is given and feed-object has no link yet
|
// a) no type-attribute is given and feed-object has no link yet
|
||||||
// b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
|
// b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
|
||||||
when {
|
when {
|
||||||
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type ->
|
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> state.feed.link = href
|
||||||
state.feed.link = href
|
|
||||||
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
|
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
|
||||||
// treat as podlove alternate feed
|
// treat as podlove alternate feed
|
||||||
var title: String? = attributes.getValue(LINK_TITLE)
|
var title: String? = attributes.getValue(LINK_TITLE)
|
||||||
|
@ -71,9 +70,8 @@ class Atom : Namespace() {
|
||||||
if (title.isNullOrEmpty()) title = href?:""
|
if (title.isNullOrEmpty()) title = href?:""
|
||||||
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
|
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
|
||||||
}
|
}
|
||||||
LINK_TYPE_HTML, LINK_TYPE_XHTML -> {
|
//A Link such as to a directory such as iTunes
|
||||||
//A Link such as to a directory such as iTunes
|
LINK_TYPE_HTML, LINK_TYPE_XHTML -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))
|
LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))
|
||||||
|
|
|
@ -10,9 +10,8 @@ class Content : Namespace() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||||
if (ENCODED == localName && state.currentItem != null && state.contentBuf != null) {
|
if (ENCODED == localName && state.contentBuf != null)
|
||||||
state.currentItem!!.setDescriptionIfLonger(state.contentBuf.toString())
|
state.currentItem?.setDescriptionIfLonger(state.contentBuf.toString())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -8,24 +8,19 @@ import ac.mdiq.podcini.net.feed.parser.utils.DurationParser.inMillis
|
||||||
import org.xml.sax.Attributes
|
import org.xml.sax.Attributes
|
||||||
|
|
||||||
class Itunes : Namespace() {
|
class Itunes : Namespace() {
|
||||||
override fun handleElementStart(localName: String, state: HandlerState,
|
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
|
||||||
attributes: Attributes): SyndElement {
|
|
||||||
if (IMAGE == localName) {
|
if (IMAGE == localName) {
|
||||||
val url: String? = attributes.getValue(IMAGE_HREF)
|
val url: String? = attributes.getValue(IMAGE_HREF)
|
||||||
|
|
||||||
if (state.currentItem != null) state.currentItem!!.imageUrl = url
|
if (state.currentItem != null) state.currentItem!!.imageUrl = url
|
||||||
else {
|
// this is the feed image
|
||||||
// this is the feed image
|
// prefer to all other images
|
||||||
// prefer to all other images
|
else if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
||||||
if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return SyndElement(localName, this)
|
return SyndElement(localName, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||||
if (state.contentBuf == null) return
|
if (state.contentBuf == null) return
|
||||||
|
|
||||||
val content = state.contentBuf.toString()
|
val content = state.contentBuf.toString()
|
||||||
if (content.isEmpty()) return
|
if (content.isEmpty()) return
|
||||||
|
|
||||||
|
@ -38,9 +33,7 @@ class Itunes : Namespace() {
|
||||||
try {
|
try {
|
||||||
val durationMs = inMillis(content)
|
val durationMs = inMillis(content)
|
||||||
state.tempObjects[DURATION] = durationMs.toInt()
|
state.tempObjects[DURATION] = durationMs.toInt()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
|
||||||
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SUBTITLE == localName -> {
|
SUBTITLE == localName -> {
|
||||||
when {
|
when {
|
||||||
|
|
|
@ -53,11 +53,7 @@ class Media : Namespace() {
|
||||||
var size: Long = 0
|
var size: Long = 0
|
||||||
val sizeStr: String? = attributes.getValue(SIZE)
|
val sizeStr: String? = attributes.getValue(SIZE)
|
||||||
if (!sizeStr.isNullOrEmpty()) {
|
if (!sizeStr.isNullOrEmpty()) {
|
||||||
try {
|
try { size = sizeStr.toLong() } catch (e: NumberFormatException) { Log.e(TAG, "Size \"$sizeStr\" could not be parsed.") }
|
||||||
size = sizeStr.toLong()
|
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
Log.e(TAG, "Size \"$sizeStr\" could not be parsed.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var durationMs = 0
|
var durationMs = 0
|
||||||
val durationStr: String? = attributes.getValue(DURATION)
|
val durationStr: String? = attributes.getValue(DURATION)
|
||||||
|
@ -65,19 +61,14 @@ class Media : Namespace() {
|
||||||
try {
|
try {
|
||||||
val duration = durationStr.toLong()
|
val duration = durationStr.toLong()
|
||||||
durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()
|
durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { Log.e(TAG, "Duration \"$durationStr\" could not be parsed") }
|
||||||
Log.e(TAG, "Duration \"$durationStr\" could not be parsed")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
|
Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
|
||||||
val media = EpisodeMedia(state.currentItem, url, size, mimeType)
|
val media = EpisodeMedia(state.currentItem, url, size, mimeType)
|
||||||
if (durationMs > 0) media.setDuration( durationMs)
|
if (durationMs > 0) media.setDuration( durationMs)
|
||||||
|
|
||||||
state.currentItem!!.media = media
|
state.currentItem!!.media = media
|
||||||
}
|
}
|
||||||
state.currentItem != null && url != null && validTypeImage -> {
|
state.currentItem != null && url != null && validTypeImage -> state.currentItem!!.imageUrl = url
|
||||||
state.currentItem!!.imageUrl = url
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IMAGE -> {
|
IMAGE -> {
|
||||||
|
|
|
@ -6,8 +6,7 @@ import ac.mdiq.podcini.net.feed.parser.element.SyndElement
|
||||||
import org.xml.sax.Attributes
|
import org.xml.sax.Attributes
|
||||||
|
|
||||||
class PodcastIndex : Namespace() {
|
class PodcastIndex : Namespace() {
|
||||||
override fun handleElementStart(localName: String, state: HandlerState,
|
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
|
||||||
attributes: Attributes): SyndElement {
|
|
||||||
when (localName) {
|
when (localName) {
|
||||||
FUNDING -> {
|
FUNDING -> {
|
||||||
val href: String? = attributes.getValue(URL)
|
val href: String? = attributes.getValue(URL)
|
||||||
|
@ -25,7 +24,6 @@ class PodcastIndex : Namespace() {
|
||||||
|
|
||||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||||
if (state.contentBuf == null) return
|
if (state.contentBuf == null) return
|
||||||
|
|
||||||
val content = state.contentBuf.toString()
|
val content = state.contentBuf.toString()
|
||||||
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content)
|
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,7 @@ class SimpleChapters : Namespace() {
|
||||||
val imageUrl: String? = attributes.getValue(IMAGE)
|
val imageUrl: String? = attributes.getValue(IMAGE)
|
||||||
val chapter = Chapter(start, title, link, imageUrl)
|
val chapter = Chapter(start, title, link, imageUrl)
|
||||||
currentItem.chapters?.add(chapter)
|
currentItem.chapters?.add(chapter)
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { Log.e(TAG, "Unable to read chapter", e) }
|
||||||
Log.e(TAG, "Unable to read chapter", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,9 +36,7 @@ class YouTube : Namespace() {
|
||||||
try {
|
try {
|
||||||
val durationMs = inMillis(content)
|
val durationMs = inMillis(content)
|
||||||
state.tempObjects[DURATION] = durationMs.toInt()
|
state.tempObjects[DURATION] = durationMs.toInt()
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
|
||||||
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
SUBTITLE == localName -> {
|
SUBTITLE == localName -> {
|
||||||
when {
|
when {
|
||||||
|
|
|
@ -8,7 +8,6 @@ import java.text.ParsePosition
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses several date formats.
|
* Parses several date formats.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -25,8 +25,6 @@ object DurationParser {
|
||||||
val value = seconds.toFloat()
|
val value = seconds.toFloat()
|
||||||
val millis = value % 1
|
val millis = value % 1
|
||||||
return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong()
|
return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong()
|
||||||
} else {
|
} else return TimeUnit.SECONDS.toMillis(seconds.toLong())
|
||||||
return TimeUnit.SECONDS.toMillis(seconds.toLong())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,10 +22,8 @@ object MimeTypeUtils {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getMimeType(type: String?, filename: String?): String? {
|
fun getMimeType(type: String?, filename: String?): String? {
|
||||||
if (isMediaFile(type) && OCTET_STREAM != type) return type
|
if (isMediaFile(type) && OCTET_STREAM != type) return type
|
||||||
|
|
||||||
val filenameType = getMimeTypeFromUrl(filename)
|
val filenameType = getMimeTypeFromUrl(filename)
|
||||||
if (isMediaFile(filenameType)) return filenameType
|
if (isMediaFile(filenameType)) return filenameType
|
||||||
|
|
||||||
return type
|
return type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,14 +14,10 @@ import java.util.regex.Pattern
|
||||||
* This class is based on `HtmlToPlainText` from jsoup's examples package.
|
* This class is based on `HtmlToPlainText` from jsoup's examples package.
|
||||||
*
|
*
|
||||||
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
|
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
|
||||||
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
|
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a scrape.
|
||||||
* scrape.
|
|
||||||
*
|
|
||||||
*
|
*
|
||||||
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
|
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
*
|
|
||||||
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:
|
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:
|
||||||
*
|
*
|
||||||
* `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]`
|
* `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]`
|
||||||
|
@ -40,7 +36,6 @@ class HtmlToPlainText {
|
||||||
val formatter = FormattingVisitor()
|
val formatter = FormattingVisitor()
|
||||||
// walk the DOM, and call .head() and .tail() for each node
|
// walk the DOM, and call .head() and .tail() for each node
|
||||||
NodeTraversor.traverse(formatter, element)
|
NodeTraversor.traverse(formatter, element)
|
||||||
|
|
||||||
return formatter.toString()
|
return formatter.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +67,6 @@ class HtmlToPlainText {
|
||||||
private fun append(text: String) {
|
private fun append(text: String) {
|
||||||
if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n")))
|
if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n")))
|
||||||
return // don't accumulate long runs of empty spaces
|
return // don't accumulate long runs of empty spaces
|
||||||
|
|
||||||
accum.append(text)
|
accum.append(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,7 @@ object URIUtil {
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun getURIFromRequestUrl(source: String): URI {
|
fun getURIFromRequestUrl(source: String): URI {
|
||||||
// try without encoding the URI
|
// try without encoding the URI
|
||||||
try {
|
try { return URI(source) } catch (e: URISyntaxException) { Logd(TAG, "Source is not encoded, encoding now") }
|
||||||
return URI(source)
|
|
||||||
} catch (e: URISyntaxException) {
|
|
||||||
Logd(TAG, "Source is not encoded, encoding now")
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
val url = URL(source)
|
val url = URL(source)
|
||||||
return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
|
return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
|
||||||
|
|
|
@ -46,9 +46,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
|
||||||
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
||||||
if (media is EpisodeMedia) {
|
if (media is EpisodeMedia) {
|
||||||
curMedia = media
|
curMedia = media
|
||||||
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
|
|
||||||
curEpisode = media.episodeOrFetch()
|
curEpisode = media.episodeOrFetch()
|
||||||
// curMedia = curEpisode?.media
|
|
||||||
} else curMedia = media
|
} else curMedia = media
|
||||||
|
|
||||||
if (PlaybackService.isRunning && !callEvenIfRunning) return
|
if (PlaybackService.isRunning && !callEvenIfRunning) return
|
||||||
|
|
|
@ -50,6 +50,20 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
*/
|
*/
|
||||||
private var wifiLock: WifiLock? = null
|
private var wifiLock: WifiLock? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
|
||||||
|
* @return The PSMPInfo object.
|
||||||
|
*/
|
||||||
|
@get:Synchronized
|
||||||
|
val playerInfo: MediaPlayerInfo
|
||||||
|
get() = MediaPlayerInfo(oldStatus, status, curMedia)
|
||||||
|
|
||||||
|
val isAudioChannelInUse: Boolean
|
||||||
|
get() {
|
||||||
|
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||||
|
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
status = PlayerStatus.STOPPED
|
status = PlayerStatus.STOPPED
|
||||||
}
|
}
|
||||||
|
@ -185,14 +199,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player")
|
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
|
|
||||||
* @return The PSMPInfo object.
|
|
||||||
*/
|
|
||||||
@get:Synchronized
|
|
||||||
val playerInfo: MediaPlayerInfo
|
|
||||||
get() = MediaPlayerInfo(oldStatus, status, curMedia)
|
|
||||||
|
|
||||||
open fun setAudioTrack(track: Int) {}
|
open fun setAudioTrack(track: Int) {}
|
||||||
|
|
||||||
fun skip() {
|
fun skip() {
|
||||||
|
@ -276,13 +282,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
|
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
|
||||||
}
|
}
|
||||||
|
|
||||||
val isAudioChannelInUse: Boolean
|
class MediaPlayerInfo(
|
||||||
get() {
|
@JvmField val oldPlayerStatus: PlayerStatus?,
|
||||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
@JvmField var playerStatus: PlayerStatus,
|
||||||
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
@JvmField var playable: Playable?)
|
||||||
}
|
|
||||||
|
|
||||||
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
|
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
|
||||||
|
@ -308,8 +311,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
||||||
|
|
||||||
val audioPlaybackSpeed: Float
|
val audioPlaybackSpeed: Float
|
||||||
get() {
|
get() {
|
||||||
try {
|
try { return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
||||||
return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
|
||||||
} catch (e: NumberFormatException) {
|
} catch (e: NumberFormatException) {
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
setPlaybackSpeed(1.0f)
|
setPlaybackSpeed(1.0f)
|
||||||
|
|
|
@ -495,7 +495,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
||||||
|
|
||||||
override fun getPosition(): Int {
|
override fun getPosition(): Int {
|
||||||
var retVal = Playable.INVALID_TIME
|
var retVal = Playable.INVALID_TIME
|
||||||
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
if (exoPlayer != null && status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
||||||
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
|
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
|
||||||
return retVal
|
return retVal
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ac.mdiq.podcini.receiver
|
package ac.mdiq.podcini.receiver
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
|
@ -18,7 +18,6 @@ class ConnectivityActionReceiver : BroadcastReceiver() {
|
||||||
Log.d(TAG, "onReceive called with action: ${intent.action}")
|
Log.d(TAG, "onReceive called with action: ${intent.action}")
|
||||||
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
|
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
|
||||||
Logd(TAG, "Received intent")
|
Logd(TAG, "Received intent")
|
||||||
|
|
||||||
ClientConfigurator.initialize(context)
|
ClientConfigurator.initialize(context)
|
||||||
networkChangedDetected(context)
|
networkChangedDetected(context)
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,6 @@ class FeedUpdateReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
Logd(TAG, "Received intent")
|
Logd(TAG, "Received intent")
|
||||||
ClientConfigurator.initialize(context)
|
ClientConfigurator.initialize(context)
|
||||||
|
|
||||||
FeedUpdateManager.runOnce(context)
|
FeedUpdateManager.runOnce(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
val extras = intent.extras
|
val extras = intent.extras
|
||||||
Log.d(TAG, "onReceive Extras: $extras")
|
Log.d(TAG, "onReceive Extras: $extras")
|
||||||
if (extras == null) return
|
if (extras == null) return
|
||||||
|
|
||||||
Log.d(TAG, "onReceive Extras: ${extras.keySet()}")
|
Log.d(TAG, "onReceive Extras: ${extras.keySet()}")
|
||||||
for (key in extras.keySet()) {
|
for (key in extras.keySet()) {
|
||||||
Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}")
|
Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}")
|
||||||
|
@ -40,11 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
||||||
serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode)
|
serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode)
|
||||||
serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source)
|
serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source)
|
||||||
serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0)
|
serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0)
|
||||||
try {
|
try { ContextCompat.startForegroundService(context, serviceIntent) } catch (e: Exception) { e.printStackTrace() }
|
||||||
ContextCompat.startForegroundService(context, serviceIntent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ class PlayerWidget : AppWidgetProvider() {
|
||||||
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
||||||
getSharedPrefs(context)
|
getSharedPrefs(context)
|
||||||
WidgetUpdaterWorker.enqueueWork(context)
|
WidgetUpdaterWorker.enqueueWork(context)
|
||||||
|
|
||||||
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
|
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
|
||||||
scheduleWorkaround(context)
|
scheduleWorkaround(context)
|
||||||
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
|
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
|
||||||
|
@ -75,9 +74,7 @@ class PlayerWidget : AppWidgetProvider() {
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
|
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
|
||||||
private const val PREFS_NAME: String = "PlayerWidgetPrefs"
|
private const val PREFS_NAME: String = "PlayerWidgetPrefs"
|
||||||
|
|
||||||
const val DEFAULT_COLOR: Int = -0xd9d3cf
|
const val DEFAULT_COLOR: Int = -0xd9d3cf
|
||||||
|
|
||||||
var prefs: SharedPreferences? = null
|
var prefs: SharedPreferences? = null
|
||||||
|
|
||||||
fun getSharedPrefs(context: Context) {
|
fun getSharedPrefs(context: Context) {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
@ -20,7 +20,6 @@ class PowerConnectionReceiver : BroadcastReceiver() {
|
||||||
@UnstableApi override fun onReceive(context: Context, intent: Intent) {
|
@UnstableApi override fun onReceive(context: Context, intent: Intent) {
|
||||||
val action = intent.action
|
val action = intent.action
|
||||||
Log.d(TAG, "onReceive charging intent: $action")
|
Log.d(TAG, "onReceive charging intent: $action")
|
||||||
|
|
||||||
ClientConfigurator.initialize(context)
|
ClientConfigurator.initialize(context)
|
||||||
if (Intent.ACTION_POWER_CONNECTED == action) {
|
if (Intent.ACTION_POWER_CONNECTED == action) {
|
||||||
Logd(TAG, "charging, starting auto-download")
|
Logd(TAG, "charging, starting auto-download")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ac.mdiq.podcini.storage.algorithms
|
package ac.mdiq.podcini.storage.algorithms
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package ac.mdiq.podcini.storage.database
|
package ac.mdiq.podcini.storage.database
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
|
||||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ac.mdiq.podcini.storage.database
|
package ac.mdiq.podcini.storage.database
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
|
|
|
@ -12,7 +12,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
|
|
||||||
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||||
override val visibility: Int
|
override val visibility: Int
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package ac.mdiq.podcini.ui.actions.actionbutton
|
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
import ac.mdiq.podcini.storage.model.MediaType
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.actions.handler
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.storage.database.Episodes
|
import ac.mdiq.podcini.storage.database.Episodes
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.BuildConfig
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.MainActivityBinding
|
import ac.mdiq.podcini.databinding.MainActivityBinding
|
||||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
||||||
|
@ -91,7 +91,6 @@ class MainActivity : CastEnabledActivity() {
|
||||||
private lateinit var navDrawerFragment: NavDrawerFragment
|
private lateinit var navDrawerFragment: NavDrawerFragment
|
||||||
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
||||||
private lateinit var audioPlayerView: View
|
private lateinit var audioPlayerView: View
|
||||||
// private lateinit var controllerFuture: ListenableFuture<MediaController>
|
|
||||||
private lateinit var navDrawer: View
|
private lateinit var navDrawer: View
|
||||||
private lateinit var dummyView : View
|
private lateinit var dummyView : View
|
||||||
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
||||||
|
@ -104,6 +103,53 @@ class MainActivity : CastEnabledActivity() {
|
||||||
private var lastTheme = 0
|
private var lastTheme = 0
|
||||||
private var navigationBarInsets = Insets.NONE
|
private var navigationBarInsets = Insets.NONE
|
||||||
|
|
||||||
|
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted) return@registerForActivityResult
|
||||||
|
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setMessage(R.string.notification_permission_text)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||||
|
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var prevState: Int = 0
|
||||||
|
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||||
|
override fun onStateChanged(view: View, state: Int) {
|
||||||
|
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
|
||||||
|
when (state) {
|
||||||
|
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||||
|
audioPlayerFragment.onCollaped()
|
||||||
|
onSlide(view,0.0f)
|
||||||
|
prevState = state
|
||||||
|
}
|
||||||
|
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||||
|
audioPlayerFragment.onExpanded()
|
||||||
|
onSlide(view, 1.0f)
|
||||||
|
prevState = state
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onSlide(view: View, slideOffset: Float) {
|
||||||
|
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
|
||||||
|
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
||||||
|
// audioPlayer.scrollToTop()
|
||||||
|
// }
|
||||||
|
audioPlayer.fadePlayerToToolbar(slideOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isDrawerOpen: Boolean
|
||||||
|
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||||
|
|
||||||
|
private val screenWidth: Int
|
||||||
|
get() {
|
||||||
|
val displayMetrics = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||||
|
return displayMetrics.widthPixels
|
||||||
|
}
|
||||||
|
|
||||||
@UnstableApi public override fun onCreate(savedInstanceState: Bundle?) {
|
@UnstableApi public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
lastTheme = getNoTitleTheme(this)
|
lastTheme = getNoTitleTheme(this)
|
||||||
setTheme(lastTheme)
|
setTheme(lastTheme)
|
||||||
|
@ -131,7 +177,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||||
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
||||||
OnlineFeedViewFragment.getSharedPrefs(this@MainActivity)
|
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
|
||||||
ItunesTopListLoader.getSharedPrefs(this@MainActivity)
|
ItunesTopListLoader.getSharedPrefs(this@MainActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +201,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
// Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
// Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||||
// requestPostNotificationPermission()
|
// requestPostNotificationPermission()
|
||||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Consume navigation bar insets - we apply them in setPlayerVisible()
|
// Consume navigation bar insets - we apply them in setPlayerVisible()
|
||||||
|
@ -213,9 +259,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
when (workInfo.state) {
|
when (workInfo.state) {
|
||||||
WorkInfo.State.RUNNING -> isRefreshingFeeds = true
|
WorkInfo.State.RUNNING -> isRefreshingFeeds = true
|
||||||
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
|
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
|
||||||
else -> {
|
else -> {}
|
||||||
// Log.d(TAG, "workInfo.state ${workInfo.state}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds))
|
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds))
|
||||||
|
@ -225,9 +269,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
|
|
||||||
private fun observeDownloads() {
|
private fun observeDownloads() {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) { WorkManager.getInstance(this@MainActivity).pruneWork().result.get() }
|
||||||
WorkManager.getInstance(this@MainActivity).pruneWork().result.get()
|
|
||||||
}
|
|
||||||
WorkManager.getInstance(this@MainActivity)
|
WorkManager.getInstance(this@MainActivity)
|
||||||
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
|
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
|
||||||
.observe(this@MainActivity) { workInfos: List<WorkInfo> ->
|
.observe(this@MainActivity) { workInfos: List<WorkInfo> ->
|
||||||
|
@ -267,20 +309,10 @@ class MainActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fun requestPostNotificationPermission() {
|
// fun requestPostNotificationPermission() {
|
||||||
// if (Build.VERSION.SDK_INT >= 33) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
// if (Build.VERSION.SDK_INT >= 33) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted) return@registerForActivityResult
|
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(R.string.notification_permission_text)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
|
||||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
updateInsets()
|
updateInsets()
|
||||||
|
@ -303,33 +335,6 @@ class MainActivity : CastEnabledActivity() {
|
||||||
outState.putInt(Extras.generated_view_id.name, View.generateViewId())
|
outState.putInt(Extras.generated_view_id.name, View.generateViewId())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var prevState: Int = 0
|
|
||||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
|
||||||
override fun onStateChanged(view: View, state: Int) {
|
|
||||||
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
|
|
||||||
when (state) {
|
|
||||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
|
||||||
audioPlayerFragment.onCollaped()
|
|
||||||
onSlide(view,0.0f)
|
|
||||||
prevState = state
|
|
||||||
}
|
|
||||||
BottomSheetBehavior.STATE_EXPANDED -> {
|
|
||||||
audioPlayerFragment.onExpanded()
|
|
||||||
onSlide(view, 1.0f)
|
|
||||||
prevState = state
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onSlide(view: View, slideOffset: Float) {
|
|
||||||
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
|
|
||||||
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
|
||||||
// audioPlayer.scrollToTop()
|
|
||||||
// }
|
|
||||||
audioPlayer.fadePlayerToToolbar(slideOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
|
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
|
||||||
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
|
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
|
||||||
// Tablet layout does not have a drawer
|
// Tablet layout does not have a drawer
|
||||||
|
@ -375,9 +380,6 @@ class MainActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val isDrawerOpen: Boolean
|
|
||||||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
|
||||||
|
|
||||||
private fun updateInsets() {
|
private fun updateInsets() {
|
||||||
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
|
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
|
||||||
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||||
|
@ -507,13 +509,6 @@ class MainActivity : CastEnabledActivity() {
|
||||||
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
|
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
|
||||||
}
|
}
|
||||||
|
|
||||||
private val screenWidth: Int
|
|
||||||
get() {
|
|
||||||
val displayMetrics = DisplayMetrics()
|
|
||||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
|
||||||
return displayMetrics.widthPixels
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||||
super.onRestoreInstanceState(savedInstanceState)
|
super.onRestoreInstanceState(savedInstanceState)
|
||||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
|
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||||
|
@ -523,21 +518,10 @@ class MainActivity : CastEnabledActivity() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
procFlowEvents()
|
procFlowEvents()
|
||||||
RatingDialog.init(this)
|
RatingDialog.init(this)
|
||||||
|
|
||||||
// val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
|
||||||
// controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
|
||||||
// controllerFuture.addListener({
|
|
||||||
// // Call controllerFuture.get() to retrieve the MediaController.
|
|
||||||
// // MediaController implements the Player interface, so it can be
|
|
||||||
// // attached to the PlayerView UI component.
|
|
||||||
//// playerView.setPlayer(controllerFuture.get())
|
|
||||||
// val player = controllerFuture.get()
|
|
||||||
// }, MoreExecutors.directExecutor())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
// MediaController.releaseFuture(controllerFuture)
|
|
||||||
cancelFlowEvents()
|
cancelFlowEvents()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -634,7 +618,7 @@ class MainActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
intent.hasExtra(Extras.fragment_feed_url.name) -> {
|
intent.hasExtra(Extras.fragment_feed_url.name) -> {
|
||||||
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
||||||
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
if (feedurl != null) loadChildFragment(OnlineFeedFragment.newInstance(feedurl))
|
||||||
}
|
}
|
||||||
intent.hasExtra(Extras.search_string.name) -> {
|
intent.hasExtra(Extras.search_string.name) -> {
|
||||||
val query = intent.getStringExtra(Extras.search_string.name)
|
val query = intent.getStringExtra(Extras.search_string.name)
|
||||||
|
|
|
@ -54,6 +54,28 @@ class OpmlImportActivity : AppCompatActivity() {
|
||||||
private var listAdapter: ArrayAdapter<String>? = null
|
private var listAdapter: ArrayAdapter<String>? = null
|
||||||
private var readElements: ArrayList<OpmlElement>? = null
|
private var readElements: ArrayList<OpmlElement>? = null
|
||||||
|
|
||||||
|
private val titleList: List<String>
|
||||||
|
get() {
|
||||||
|
val result: MutableList<String> = ArrayList()
|
||||||
|
if (!readElements.isNullOrEmpty()) {
|
||||||
|
for (element in readElements!!) {
|
||||||
|
if (element.text != null) result.add(element.text!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||||
|
if (isGranted) startImport()
|
||||||
|
else {
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setMessage(R.string.opml_import_ask_read_permission)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
|
||||||
|
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
|
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
setTheme(getTheme(this))
|
setTheme(getTheme(this))
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -135,17 +157,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
||||||
startImport()
|
startImport()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val titleList: List<String>
|
|
||||||
get() {
|
|
||||||
val result: MutableList<String> = ArrayList()
|
|
||||||
if (!readElements.isNullOrEmpty()) {
|
|
||||||
for (element in readElements!!) {
|
|
||||||
if (element.text != null) result.add(element.text!!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
super.onCreateOptionsMenu(menu)
|
super.onCreateOptionsMenu(menu)
|
||||||
val inflater = menuInflater
|
val inflater = menuInflater
|
||||||
|
@ -186,18 +197,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
||||||
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val requestPermissionLauncher =
|
|
||||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
|
||||||
if (isGranted) startImport()
|
|
||||||
else {
|
|
||||||
MaterialAlertDialogBuilder(this)
|
|
||||||
.setMessage(R.string.opml_import_ask_read_permission)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
|
|
||||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Starts the import process. */
|
/** Starts the import process. */
|
||||||
private fun startImport() {
|
private fun startImport() {
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
binding.progressBar.visibility = View.VISIBLE
|
||||||
|
|
|
@ -39,6 +39,5 @@ class SplashActivity : Activity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,36 +2,51 @@ package ac.mdiq.podcini.ui.activity
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.AudioControlsBinding
|
import ac.mdiq.podcini.databinding.AudioControlsBinding
|
||||||
|
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||||
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||||
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||||
|
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||||
|
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
||||||
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||||
|
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||||
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
import ac.mdiq.podcini.storage.model.Playable
|
||||||
import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
|
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||||
import ac.mdiq.podcini.ui.dialog.ShareDialog
|
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
import ac.mdiq.podcini.ui.dialog.*
|
||||||
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
||||||
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
|
|
||||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||||
|
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||||
|
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
|
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
import android.app.Activity
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.graphics.PixelFormat
|
import android.graphics.PixelFormat
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
|
@ -39,17 +54,33 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.util.Pair
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
||||||
|
import android.view.animation.*
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.SeekBar
|
||||||
|
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||||
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
import androidx.window.layout.WindowMetricsCalculator
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity for playing video files.
|
* Activity for playing video files.
|
||||||
|
@ -57,18 +88,9 @@ import kotlinx.coroutines.launch
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
class VideoplayerActivity : CastEnabledActivity() {
|
class VideoplayerActivity : CastEnabledActivity() {
|
||||||
|
|
||||||
enum class VideoMode(val mode: Int) {
|
|
||||||
None(0),
|
|
||||||
WINDOW_VIEW(1),
|
|
||||||
FULL_SCREEN_VIEW(2),
|
|
||||||
AUDIO_ONLY(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var _binding: VideoplayerActivityBinding? = null
|
private var _binding: VideoplayerActivityBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var videoEpisodeFragment: VideoEpisodeFragment
|
private lateinit var videoEpisodeFragment: VideoEpisodeFragment
|
||||||
|
|
||||||
var switchToAudioOnly = false
|
var switchToAudioOnly = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
@ -97,11 +119,12 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
val fm = supportFragmentManager
|
val fm = supportFragmentManager
|
||||||
val transaction = fm.beginTransaction()
|
val transaction = fm.beginTransaction()
|
||||||
videoEpisodeFragment = VideoEpisodeFragment()
|
videoEpisodeFragment = VideoEpisodeFragment()
|
||||||
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
|
transaction.replace(R.id.main_view, videoEpisodeFragment, "VideoEpisodeFragment")
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setForVideoMode() {
|
private fun setForVideoMode() {
|
||||||
|
Logd(TAG, "setForVideoMode videoMode: $videoMode")
|
||||||
when (videoMode) {
|
when (videoMode) {
|
||||||
VideoMode.FULL_SCREEN_VIEW -> {
|
VideoMode.FULL_SCREEN_VIEW -> {
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||||
|
@ -114,7 +137,8 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
VideoMode.WINDOW_VIEW -> {
|
VideoMode.WINDOW_VIEW -> {
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||||
setTheme(R.style.Theme_Podcini_VideoEpisode)
|
// setTheme(R.style.Theme_Podcini_VideoEpisode)
|
||||||
|
setTheme(R.style.Theme_Podcini_VideoPlayer)
|
||||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||||
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||||
window.setFormat(PixelFormat.TRANSPARENT)
|
window.setFormat(PixelFormat.TRANSPARENT)
|
||||||
|
@ -127,6 +151,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
setForVideoMode()
|
||||||
switchToAudioOnly = false
|
switchToAudioOnly = false
|
||||||
if (isCasting) {
|
if (isCasting) {
|
||||||
val intent = getPlayerActivityIntent(this)
|
val intent = getPlayerActivityIntent(this)
|
||||||
|
@ -189,7 +214,6 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onEventMainThread(event: FlowEvent.MessageEvent) {
|
private fun onEventMainThread(event: FlowEvent.MessageEvent) {
|
||||||
// Logd(TAG, "onEvent($event)")
|
|
||||||
val errorDialog = MaterialAlertDialogBuilder(this)
|
val errorDialog = MaterialAlertDialogBuilder(this)
|
||||||
errorDialog.setMessage(event.message)
|
errorDialog.setMessage(event.message)
|
||||||
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
|
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
|
||||||
|
@ -369,6 +393,13 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
return super.onKeyUp(keyCode, event)
|
return super.onKeyUp(keyCode, event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class VideoMode(val mode: Int) {
|
||||||
|
None(0),
|
||||||
|
WINDOW_VIEW(1),
|
||||||
|
FULL_SCREEN_VIEW(2),
|
||||||
|
AUDIO_ONLY(3)
|
||||||
|
}
|
||||||
|
|
||||||
class PlaybackControlsDialog : DialogFragment() {
|
class PlaybackControlsDialog : DialogFragment() {
|
||||||
private lateinit var dialog: AlertDialog
|
private lateinit var dialog: AlertDialog
|
||||||
private var _binding: AudioControlsBinding? = null
|
private var _binding: AudioControlsBinding? = null
|
||||||
|
@ -425,6 +456,472 @@ class VideoplayerActivity : CastEnabledActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||||
|
private var _binding: VideoEpisodeFragmentBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
private lateinit var root: ViewGroup
|
||||||
|
private var videoControlsVisible = true
|
||||||
|
private var videoSurfaceCreated = false
|
||||||
|
private var lastScreenTap: Long = 0
|
||||||
|
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||||
|
private var showTimeLeft = false
|
||||||
|
private var prog = 0f
|
||||||
|
|
||||||
|
private var itemsLoaded = false
|
||||||
|
private var episode: Episode? = null
|
||||||
|
private var webviewData: String? = null
|
||||||
|
private var webvDescription: ShownotesWebView? = null
|
||||||
|
|
||||||
|
var destroyingDueToReload = false
|
||||||
|
private var statusHandler: ServiceStatusHandler? = null
|
||||||
|
var isFavorite = false
|
||||||
|
|
||||||
|
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||||
|
Logd(TAG, "onVideoviewTouched ${event.action}")
|
||||||
|
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
||||||
|
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
||||||
|
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
Logd(TAG, "onVideoviewTouched $videoControlsVisible ${System.currentTimeMillis() - lastScreenTap}")
|
||||||
|
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||||
|
if (event.x > v.measuredWidth / 2.0f) {
|
||||||
|
onFastForward()
|
||||||
|
showSkipAnimation(true)
|
||||||
|
} else {
|
||||||
|
onRewind()
|
||||||
|
showSkipAnimation(false)
|
||||||
|
}
|
||||||
|
if (videoControlsVisible) {
|
||||||
|
hideVideoControls(false)
|
||||||
|
videoControlsVisible = false
|
||||||
|
}
|
||||||
|
return@OnTouchListener true
|
||||||
|
}
|
||||||
|
toggleVideoControlsVisibility()
|
||||||
|
if (videoControlsVisible) setupVideoControlsToggler()
|
||||||
|
lastScreenTap = System.currentTimeMillis()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
||||||
|
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||||
|
holder.setFixedSize(width, height)
|
||||||
|
}
|
||||||
|
@UnstableApi
|
||||||
|
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||||
|
Logd(TAG, "Videoview holder created")
|
||||||
|
videoSurfaceCreated = true
|
||||||
|
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||||
|
setupVideoAspectRatio()
|
||||||
|
}
|
||||||
|
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||||
|
Logd(TAG, "Videosurface was destroyed")
|
||||||
|
videoSurfaceCreated = false
|
||||||
|
(activity as? VideoplayerActivity)?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val hideVideoControls = Runnable {
|
||||||
|
if (videoControlsVisible) {
|
||||||
|
Logd(TAG, "Hiding video controls")
|
||||||
|
hideVideoControls(true)
|
||||||
|
videoControlsVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
super.onCreateView(inflater, container, savedInstanceState)
|
||||||
|
Logd(TAG, "fragment onCreateView")
|
||||||
|
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(activity))
|
||||||
|
root = binding.root
|
||||||
|
statusHandler = newStatusHandler()
|
||||||
|
statusHandler!!.init()
|
||||||
|
setupView()
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
||||||
|
return object : ServiceStatusHandler(requireActivity()) {
|
||||||
|
override fun updatePlayButton(showPlay: Boolean) {
|
||||||
|
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||||
|
binding.playButton.setIsShowPlay(showPlay)
|
||||||
|
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
else {
|
||||||
|
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
setupVideoAspectRatio()
|
||||||
|
if (videoSurfaceCreated) {
|
||||||
|
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||||
|
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun loadMediaInfo() {
|
||||||
|
this@VideoEpisodeFragment.loadMediaInfo()
|
||||||
|
}
|
||||||
|
override fun onPlaybackEnd() {
|
||||||
|
activity?.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
onPositionObserverUpdate()
|
||||||
|
procFlowEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
cancelFlowEvents()
|
||||||
|
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
// Controller released; we will not receive buffering updates
|
||||||
|
binding.progressBar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
Logd(TAG, "onDestroyView")
|
||||||
|
if (webvDescription != null) {
|
||||||
|
root.removeView(webvDescription!!)
|
||||||
|
webvDescription!!.destroy()
|
||||||
|
}
|
||||||
|
_binding = null
|
||||||
|
statusHandler?.release()
|
||||||
|
statusHandler = null // prevent leak
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var eventSink: Job? = null
|
||||||
|
private fun cancelFlowEvents() {
|
||||||
|
eventSink?.cancel()
|
||||||
|
eventSink = null
|
||||||
|
}
|
||||||
|
private fun procFlowEvents() {
|
||||||
|
if (eventSink != null) return
|
||||||
|
eventSink = lifecycleScope.launch {
|
||||||
|
EventFlow.events.collectLatest { event ->
|
||||||
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
|
when (event) {
|
||||||
|
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
|
||||||
|
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setForVideoMode() {
|
||||||
|
when (videoMode) {
|
||||||
|
VideoMode.FULL_SCREEN_VIEW -> {
|
||||||
|
Logd(TAG, "setForVideoMode setting for FULL_SCREEN_VIEW")
|
||||||
|
webvDescription?.visibility = View.GONE
|
||||||
|
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||||
|
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||||
|
binding.topBar.visibility = View.GONE
|
||||||
|
}
|
||||||
|
VideoMode.WINDOW_VIEW -> {
|
||||||
|
Logd(TAG, "setForVideoMode setting for WINDOW_VIEW")
|
||||||
|
webvDescription?.visibility = View.VISIBLE
|
||||||
|
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||||
|
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||||
|
binding.topBar.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
setupVideoAspectRatio()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
|
||||||
|
when {
|
||||||
|
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
|
||||||
|
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
|
||||||
|
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupVideoAspectRatio() {
|
||||||
|
if (videoSurfaceCreated) {
|
||||||
|
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity as Activity)
|
||||||
|
val videoWidth = if (videoMode == VideoMode.FULL_SCREEN_VIEW) max(windowMetrics.bounds.width(), windowMetrics.bounds.height())
|
||||||
|
else min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
|
||||||
|
var videoHeight = 0
|
||||||
|
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
||||||
|
Logd(TAG, "setupVideoAspectRatio Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
||||||
|
videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
|
||||||
|
Logd(TAG, "setupVideoAspectRatio Width,height of video 1: $videoWidth, $videoHeight")
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "setupVideoAspectRatio Could not determine video size")
|
||||||
|
videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
|
||||||
|
Logd(TAG, "setupVideoAspectRatio Width,height of video 2: $videoWidth, $videoHeight")
|
||||||
|
}
|
||||||
|
val lp = binding.videoView.layoutParams
|
||||||
|
lp.width = videoWidth
|
||||||
|
lp.height = videoHeight
|
||||||
|
binding.videoView.layoutParams = lp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadItemsRunning = false
|
||||||
|
@OptIn(UnstableApi::class)
|
||||||
|
private fun loadMediaInfo() {
|
||||||
|
Logd(TAG, "loadMediaInfo called")
|
||||||
|
if (curMedia == null) return
|
||||||
|
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||||
|
Logd(TAG, "Closing, no longer video")
|
||||||
|
destroyingDueToReload = true
|
||||||
|
activity?.finish()
|
||||||
|
MainActivityStarter(requireContext()).withOpenPlayer().start()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showTimeLeft = shouldShowRemainingTime()
|
||||||
|
onPositionObserverUpdate()
|
||||||
|
if (!loadItemsRunning) {
|
||||||
|
loadItemsRunning = true
|
||||||
|
lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
episode = withContext(Dispatchers.IO) {
|
||||||
|
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
|
||||||
|
if (feedItem != null) {
|
||||||
|
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
|
||||||
|
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
|
||||||
|
}
|
||||||
|
feedItem
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
Logd(TAG, "load() item ${episode?.id}")
|
||||||
|
if (episode != null) {
|
||||||
|
val isFav = episode!!.isFavorite
|
||||||
|
if (isFavorite != isFav) {
|
||||||
|
isFavorite = isFav
|
||||||
|
invalidateOptionsMenu(activity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
||||||
|
"text/html", "utf-8", "about:blank")
|
||||||
|
itemsLoaded = true
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
} finally { loadItemsRunning = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val media = curMedia
|
||||||
|
if (media != null) {
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.subtitle = media.getEpisodeTitle()
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.title = media.getFeedTitle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
private fun setupView() {
|
||||||
|
showTimeLeft = shouldShowRemainingTime()
|
||||||
|
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
||||||
|
binding.durationLabel.setOnClickListener {
|
||||||
|
showTimeLeft = !showTimeLeft
|
||||||
|
val media = curMedia ?: return@setOnClickListener
|
||||||
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
|
val length: String
|
||||||
|
if (showTimeLeft) {
|
||||||
|
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||||
|
length = "-" + getDurationStringLong(remainingTime)
|
||||||
|
} else {
|
||||||
|
val duration = converter.convert(media.getDuration())
|
||||||
|
length = getDurationStringLong(duration)
|
||||||
|
}
|
||||||
|
binding.durationLabel.text = length
|
||||||
|
setShowRemainTimeSetting(showTimeLeft)
|
||||||
|
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||||
|
}
|
||||||
|
binding.sbPosition.setOnSeekBarChangeListener(this)
|
||||||
|
binding.rewindButton.setOnClickListener { onRewind() }
|
||||||
|
binding.rewindButton.setOnLongClickListener {
|
||||||
|
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Logd(TAG, "setupView 1")
|
||||||
|
binding.playButton.setIsVideoScreen(true)
|
||||||
|
binding.playButton.setOnClickListener { onPlayPause() }
|
||||||
|
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
||||||
|
binding.fastForwardButton.setOnLongClickListener {
|
||||||
|
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Logd(TAG, "setupView 2")
|
||||||
|
// To suppress touches directly below the slider
|
||||||
|
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
|
||||||
|
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||||
|
setupVideoControlsToggler()
|
||||||
|
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||||
|
Logd(TAG, "setupView 2")
|
||||||
|
webvDescription = binding.webvDescription
|
||||||
|
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||||
|
// val cMedia = getMedia
|
||||||
|
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
|
||||||
|
// seekTo(time ?: 0)
|
||||||
|
// } else {
|
||||||
|
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
|
||||||
|
// Snackbar.LENGTH_LONG)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// registerForContextMenu(webvDescription)
|
||||||
|
// webvDescription.visibility = View.GONE
|
||||||
|
binding.toggleViews.setOnClickListener { (activity as VideoplayerActivity).toggleViews() }
|
||||||
|
binding.audioOnly.setOnClickListener {
|
||||||
|
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
||||||
|
(activity as? VideoplayerActivity)?.finish()
|
||||||
|
}
|
||||||
|
Logd(TAG, "setupView 4")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleVideoControlsVisibility() {
|
||||||
|
if (videoControlsVisible) hideVideoControls(true)
|
||||||
|
else showVideoControls()
|
||||||
|
videoControlsVisible = !videoControlsVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showSkipAnimation(isForward: Boolean) {
|
||||||
|
val skipAnimation = AnimationSet(true)
|
||||||
|
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
||||||
|
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
||||||
|
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
||||||
|
skipAnimation.fillAfter = false
|
||||||
|
skipAnimation.duration = 800
|
||||||
|
|
||||||
|
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
||||||
|
if (isForward) {
|
||||||
|
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
||||||
|
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||||
|
} else {
|
||||||
|
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||||
|
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||||
|
}
|
||||||
|
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||||
|
binding.skipAnimationImage.layoutParams = params
|
||||||
|
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||||
|
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||||
|
override fun onAnimationStart(animation: Animation) {}
|
||||||
|
override fun onAnimationEnd(animation: Animation) {
|
||||||
|
binding.skipAnimationImage.visibility = View.GONE
|
||||||
|
}
|
||||||
|
override fun onAnimationRepeat(animation: Animation) {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun onRewind() {
|
||||||
|
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||||
|
setupVideoControlsToggler()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun onPlayPause() {
|
||||||
|
playPause()
|
||||||
|
setupVideoControlsToggler()
|
||||||
|
}
|
||||||
|
|
||||||
|
@UnstableApi
|
||||||
|
fun onFastForward() {
|
||||||
|
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||||
|
setupVideoControlsToggler()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupVideoControlsToggler() {
|
||||||
|
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showVideoControls() {
|
||||||
|
Logd(TAG, "showVideoControls")
|
||||||
|
binding.bottomControlsContainer.visibility = View.VISIBLE
|
||||||
|
binding.controlsContainer.visibility = View.VISIBLE
|
||||||
|
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_in)
|
||||||
|
if (animation != null) {
|
||||||
|
binding.bottomControlsContainer.startAnimation(animation)
|
||||||
|
binding.controlsContainer.startAnimation(animation)
|
||||||
|
}
|
||||||
|
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||||
|
(activity as AppCompatActivity).supportActionBar?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideVideoControls(showAnimation: Boolean) {
|
||||||
|
Logd(TAG, "hideVideoControls $showAnimation")
|
||||||
|
if (!isAdded) return
|
||||||
|
if (showAnimation) {
|
||||||
|
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_out)
|
||||||
|
if (animation != null) {
|
||||||
|
binding.bottomControlsContainer.startAnimation(animation)
|
||||||
|
binding.controlsContainer.startAnimation(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||||
|
binding.bottomControlsContainer.visibility = View.GONE
|
||||||
|
binding.controlsContainer.visibility = View.GONE
|
||||||
|
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPositionObserverUpdate() {
|
||||||
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
|
val currentPosition = converter.convert(curPositionFB)
|
||||||
|
val duration_ = converter.convert(curDurationFB)
|
||||||
|
val remainingTime = converter.convert(curDurationFB - curPositionFB)
|
||||||
|
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||||
|
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
||||||
|
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binding.positionLabel.text = getDurationStringLong(currentPosition)
|
||||||
|
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
||||||
|
else binding.durationLabel.text = getDurationStringLong(duration_)
|
||||||
|
updateProgressbarPosition(currentPosition, duration_)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
||||||
|
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
|
||||||
|
val progress = (position.toFloat()) / duration
|
||||||
|
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||||
|
if (fromUser) {
|
||||||
|
prog = progress / (seekBar.max.toFloat())
|
||||||
|
val converter = TimeSpeedConverter(curSpeedFB)
|
||||||
|
val position = converter.convert((prog * curDurationFB).toInt())
|
||||||
|
binding.seekPositionLabel.text = getDurationStringLong(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||||
|
binding.seekCardView.scaleX = .8f
|
||||||
|
binding.seekCardView.scaleY = .8f
|
||||||
|
binding.seekCardView.animate()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||||
|
.setDuration(200)
|
||||||
|
.start()
|
||||||
|
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||||
|
seekTo((prog * curDurationFB).toInt())
|
||||||
|
binding.seekCardView.scaleX = 1f
|
||||||
|
binding.seekCardView.scaleY = 1f
|
||||||
|
binding.seekCardView.animate()
|
||||||
|
.setInterpolator(FastOutSlowInInterpolator())
|
||||||
|
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||||
|
.setDuration(200)
|
||||||
|
.start()
|
||||||
|
setupVideoControlsToggler()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val videoSize: Pair<Int, Int>?
|
||||||
|
get() = playbackService?.mPlayer?.getVideoSize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
|
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
|
||||||
const val VIDEO_MODE = "Video_Mode"
|
const val VIDEO_MODE = "Video_Mode"
|
||||||
|
|
|
@ -14,6 +14,9 @@ import android.os.Bundle
|
||||||
class MainActivityStarter(private val context: Context) {
|
class MainActivityStarter(private val context: Context) {
|
||||||
private val intent: Intent = Intent(INTENT)
|
private val intent: Intent = Intent(INTENT)
|
||||||
private var fragmentArgs: Bundle? = null
|
private var fragmentArgs: Bundle? = null
|
||||||
|
val pendingIntent: PendingIntent
|
||||||
|
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
intent.setPackage(context.packageName)
|
intent.setPackage(context.packageName)
|
||||||
|
@ -24,10 +27,6 @@ class MainActivityStarter(private val context: Context) {
|
||||||
return intent
|
return intent
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent: PendingIntent
|
|
||||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
context.startActivity(getIntent())
|
context.startActivity(getIntent())
|
||||||
}
|
}
|
||||||
|
@ -64,7 +63,6 @@ class MainActivityStarter(private val context: Context) {
|
||||||
|
|
||||||
fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter {
|
fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter {
|
||||||
if (fragmentArgs == null) fragmentArgs = Bundle()
|
if (fragmentArgs == null) fragmentArgs = Bundle()
|
||||||
|
|
||||||
fragmentArgs!!.putBoolean(name, value)
|
fragmentArgs!!.putBoolean(name, value)
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@ import androidx.media3.common.util.UnstableApi
|
||||||
*/
|
*/
|
||||||
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
|
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
|
||||||
val intent: Intent = Intent(INTENT)
|
val intent: Intent = Intent(INTENT)
|
||||||
|
val pendingIntent: PendingIntent
|
||||||
|
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
intent.setPackage(context.packageName)
|
intent.setPackage(context.packageName)
|
||||||
|
@ -23,10 +26,6 @@ import androidx.media3.common.util.UnstableApi
|
||||||
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
|
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent: PendingIntent
|
|
||||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
context.startActivity(intent)
|
context.startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
|
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
|
||||||
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
|
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
|
||||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
|
|
|
@ -33,10 +33,6 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) {
|
||||||
feed = unmanaged(feed)
|
feed = unmanaged(feed)
|
||||||
feed.setCustomTitle1(newTitle)
|
feed.setCustomTitle1(newTitle)
|
||||||
feed = upsertBlk(feed) {}
|
feed = upsertBlk(feed) {}
|
||||||
|
|
||||||
// feed = upsertBlk(feed) {
|
|
||||||
// it.setCustomTitle1(newTitle)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
.setNeutralButton(R.string.reset, null)
|
.setNeutralButton(R.string.reset, null)
|
||||||
.setNegativeButton(R.string.cancel_label, null)
|
.setNegativeButton(R.string.cancel_label, null)
|
||||||
|
|
|
@ -50,9 +50,8 @@ abstract class DatesFilterDialog(private val context: Context, oldestDate: Long)
|
||||||
binding.allTimeButton.isEnabled = !checked
|
binding.allTimeButton.isEnabled = !checked
|
||||||
binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f
|
binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f
|
||||||
}
|
}
|
||||||
if (showMarkPlayed) {
|
if (showMarkPlayed) binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
|
||||||
binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
|
else {
|
||||||
} else {
|
|
||||||
binding.includeMarkedCheckbox.visibility = View.GONE
|
binding.includeMarkedCheckbox.visibility = View.GONE
|
||||||
binding.noticeMessage.visibility = View.GONE
|
binding.noticeMessage.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
|
@ -136,11 +136,7 @@ open class FeedSortDialog : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFeedOrder(selected: String, dir: Int) {
|
private fun setFeedOrder(selected: String, dir: Int) {
|
||||||
appPrefs.edit()
|
appPrefs.edit().putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected).apply()
|
||||||
.putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected)
|
appPrefs.edit().putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir).apply()
|
||||||
.apply()
|
|
||||||
appPrefs.edit()
|
|
||||||
.putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir)
|
|
||||||
.apply()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,17 +25,13 @@ class ShareDialog : BottomSheetDialogFragment() {
|
||||||
private var item: Episode? = null
|
private var item: Episode? = null
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
|
|
||||||
ctx = requireContext()
|
ctx = requireContext()
|
||||||
prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
_binding = ShareEpisodeDialogBinding.inflate(inflater)
|
_binding = ShareEpisodeDialogBinding.inflate(inflater)
|
||||||
binding.shareDialogRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
|
binding.shareDialogRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
|
||||||
binding.sharePositionCheckbox.isEnabled = checkedId == binding.shareSocialRadio.id
|
binding.sharePositionCheckbox.isEnabled = checkedId == binding.shareSocialRadio.id
|
||||||
}
|
}
|
||||||
|
|
||||||
setupOptions()
|
setupOptions()
|
||||||
|
|
||||||
binding.shareButton.setOnClickListener {
|
binding.shareButton.setOnClickListener {
|
||||||
val includePlaybackPosition = binding.sharePositionCheckbox.isChecked
|
val includePlaybackPosition = binding.sharePositionCheckbox.isChecked
|
||||||
val position: Int
|
val position: Int
|
||||||
|
@ -52,14 +48,9 @@ class ShareDialog : BottomSheetDialogFragment() {
|
||||||
shareFeedItemFile(ctx, item!!.media!!)
|
shareFeedItemFile(ctx, item!!.media!!)
|
||||||
position = 3
|
position = 3
|
||||||
}
|
}
|
||||||
else -> {
|
else -> throw IllegalStateException("Unknown share method")
|
||||||
throw IllegalStateException("Unknown share method")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
prefs.edit()
|
prefs.edit().putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition).putInt(PREF_SHARE_EPISODE_TYPE, position).apply()
|
||||||
.putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition)
|
|
||||||
.putInt(PREF_SHARE_EPISODE_TYPE, position)
|
|
||||||
.apply()
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -85,15 +85,9 @@ class SleepTimerDialog : DialogFragment() {
|
||||||
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
|
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
|
||||||
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
|
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
|
||||||
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
|
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
|
||||||
extendSleepFiveMinutesButton.setOnClickListener {
|
extendSleepFiveMinutesButton.setOnClickListener { extendSleepTimer((5 * 1000 * 60).toLong()) }
|
||||||
extendSleepTimer((5 * 1000 * 60).toLong())
|
extendSleepTenMinutesButton.setOnClickListener { extendSleepTimer((10 * 1000 * 60).toLong()) }
|
||||||
}
|
extendSleepTwentyMinutesButton.setOnClickListener { extendSleepTimer((20 * 1000 * 60).toLong()) }
|
||||||
extendSleepTenMinutesButton.setOnClickListener {
|
|
||||||
extendSleepTimer((10 * 1000 * 60).toLong())
|
|
||||||
}
|
|
||||||
extendSleepTwentyMinutesButton.setOnClickListener {
|
|
||||||
extendSleepTimer((20 * 1000 * 60).toLong())
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||||
if (isChecked) etxtTime.visibility = View.GONE
|
if (isChecked) etxtTime.visibility = View.GONE
|
||||||
|
@ -115,9 +109,7 @@ class SleepTimerDialog : DialogFragment() {
|
||||||
changeTimesButton.isEnabled = chAutoEnable.isChecked
|
changeTimesButton.isEnabled = chAutoEnable.isChecked
|
||||||
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
|
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
|
||||||
|
|
||||||
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setShakeToReset(isChecked) }
|
||||||
setShakeToReset(isChecked)
|
|
||||||
}
|
|
||||||
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
|
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
|
||||||
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||||
setAutoEnable(isChecked)
|
setAutoEnable(isChecked)
|
||||||
|
@ -132,9 +124,7 @@ class SleepTimerDialog : DialogFragment() {
|
||||||
showTimeRangeDialog(from, to)
|
showTimeRangeDialog(from, to)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.disableSleeptimerButton.setOnClickListener {
|
binding.disableSleeptimerButton.setOnClickListener { playbackService?.taskManager?.disableSleepTimer() }
|
||||||
playbackService?.taskManager?.disableSleepTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.setSleeptimerButton.setOnClickListener {
|
binding.setSleeptimerButton.setOnClickListener {
|
||||||
if (!PlaybackService.isRunning) {
|
if (!PlaybackService.isRunning) {
|
||||||
|
@ -237,18 +227,16 @@ class SleepTimerDialog : DialogFragment() {
|
||||||
|
|
||||||
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
|
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
|
||||||
private val view = TimeRangeView(context, from, to)
|
private val view = TimeRangeView(context, from, to)
|
||||||
|
val from: Int
|
||||||
|
get() = view.from
|
||||||
|
val to: Int
|
||||||
|
get() = view.to
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setView(view)
|
setView(view)
|
||||||
setPositiveButton(android.R.string.ok, null)
|
setPositiveButton(android.R.string.ok, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val from: Int
|
|
||||||
get() = view.from
|
|
||||||
|
|
||||||
val to: Int
|
|
||||||
get() = view.to
|
|
||||||
|
|
||||||
internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) {
|
internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) {
|
||||||
private val paintDial = Paint()
|
private val paintDial = Paint()
|
||||||
private val paintSelected = Paint()
|
private val paintSelected = Paint()
|
||||||
|
|
|
@ -135,7 +135,6 @@ class SwipeActionsDialog(private val context: Context, private val tag: String)
|
||||||
item.root.setOnClickListener {
|
item.root.setOnClickListener {
|
||||||
if (direction == LEFT) leftAction = keys[i]
|
if (direction == LEFT) leftAction = keys[i]
|
||||||
else rightAction = keys[i]
|
else rightAction = keys[i]
|
||||||
|
|
||||||
setupSwipeDirectionView(view, direction)
|
setupSwipeDirectionView(view, direction)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,7 @@ class SwitchQueueDialog(activity: Activity) {
|
||||||
val items = mutableListOf<Episode>()
|
val items = mutableListOf<Episode>()
|
||||||
items.addAll(curQueue.episodes)
|
items.addAll(curQueue.episodes)
|
||||||
items.addAll(curQueue_.episodes)
|
items.addAll(curQueue_.episodes)
|
||||||
// unmanaged(curQueue_)
|
curQueue = upsertBlk(curQueue_) { it.update() }
|
||||||
curQueue = upsertBlk(curQueue_) {
|
|
||||||
it.update()
|
|
||||||
}
|
|
||||||
EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items))
|
EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,9 +25,7 @@ class TagSettingsDialog : DialogFragment() {
|
||||||
|
|
||||||
private var _binding: EditTagsDialogBinding? = null
|
private var _binding: EditTagsDialogBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private var feedList: MutableList<Feed> = mutableListOf()
|
private var feedList: MutableList<Feed> = mutableListOf()
|
||||||
|
|
||||||
private lateinit var displayedTags: MutableList<String>
|
private lateinit var displayedTags: MutableList<String>
|
||||||
private lateinit var adapter: SimpleChipAdapter
|
private lateinit var adapter: SimpleChipAdapter
|
||||||
|
|
||||||
|
@ -53,7 +51,6 @@ class TagSettingsDialog : DialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.tagsRecycler.adapter = adapter
|
binding.tagsRecycler.adapter = adapter
|
||||||
|
|
||||||
binding.newTagTextInput.setEndIconOnClickListener {
|
binding.newTagTextInput.setEndIconOnClickListener {
|
||||||
addTag(binding.newTagEditText.text.toString().trim { it <= ' ' })
|
addTag(binding.newTagEditText.text.toString().trim { it <= ' ' })
|
||||||
}
|
}
|
||||||
|
@ -87,7 +84,6 @@ class TagSettingsDialog : DialogFragment() {
|
||||||
|
|
||||||
private fun addTag(name: String) {
|
private fun addTag(name: String) {
|
||||||
if (name.isEmpty() || displayedTags.contains(name)) return
|
if (name.isEmpty() || displayedTags.contains(name)) return
|
||||||
|
|
||||||
displayedTags.add(name)
|
displayedTags.add(name)
|
||||||
binding.newTagEditText.setText("")
|
binding.newTagEditText.setText("")
|
||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
|
|
|
@ -47,17 +47,14 @@ import java.util.*
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||||
|
private var _binding: SpeedSelectDialogBinding? = null
|
||||||
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private lateinit var adapter: SpeedSelectionAdapter
|
private lateinit var adapter: SpeedSelectionAdapter
|
||||||
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
|
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
|
||||||
private lateinit var addCurrentSpeedChip: Chip
|
private lateinit var addCurrentSpeedChip: Chip
|
||||||
|
|
||||||
private var _binding: SpeedSelectDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val selectedSpeeds: MutableList<Float>
|
|
||||||
|
|
||||||
private lateinit var settingCode: BooleanArray
|
private lateinit var settingCode: BooleanArray
|
||||||
|
private val selectedSpeeds: MutableList<Float>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val format = DecimalFormatSymbols(Locale.US)
|
val format = DecimalFormatSymbols(Locale.US)
|
||||||
|
@ -262,9 +259,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||||
if (codeArray[1]) {
|
if (codeArray[1]) {
|
||||||
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
|
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
|
||||||
if (episode?.feed?.preferences != null) {
|
if (episode?.feed?.preferences != null) {
|
||||||
upsertBlk(episode.feed!!) {
|
upsertBlk(episode.feed!!) { it.preferences!!.playSpeed = speed }
|
||||||
it.preferences!!.playSpeed = speed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (codeArray[0]) {
|
if (codeArray[0]) {
|
||||||
|
@ -283,9 +278,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun setCurTempSpeed(speed: Float) {
|
private fun setCurTempSpeed(speed: Float) {
|
||||||
curState = upsertBlk(curState) {
|
curState = upsertBlk(curState) { it.curTempSpeed = speed }
|
||||||
it.curTempSpeed = speed
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
|
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||||
}
|
}
|
||||||
|
@ -310,7 +303,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||||
args.putBooleanArray("settingCode", settingCode)
|
args.putBooleanArray("settingCode", settingCode)
|
||||||
if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault)
|
if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault)
|
||||||
dialog.arguments = args
|
dialog.arguments = args
|
||||||
|
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||||
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||||
import ac.mdiq.podcini.net.download.service.Downloader
|
import ac.mdiq.podcini.net.download.service.Downloader
|
||||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
|
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
|
||||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||||
|
@ -81,12 +81,11 @@ import kotlin.math.min
|
||||||
* feed object that was parsed. This activity MUST be started with a given URL
|
* feed object that was parsed. This activity MUST be started with a given URL
|
||||||
* or an Exception will be thrown.
|
* or an Exception will be thrown.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||||
* and the activity will finish as soon as the error dialog is closed.
|
* and the activity will finish as soon as the error dialog is closed.
|
||||||
*/
|
*/
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
class OnlineFeedViewFragment : Fragment() {
|
class OnlineFeedFragment : Fragment() {
|
||||||
private var _binding: OnlineFeedviewFragmentBinding? = null
|
private var _binding: OnlineFeedviewFragmentBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
@ -147,17 +146,6 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showNoPodcastFoundError() {
|
|
||||||
requireActivity().runOnUiThread {
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
|
||||||
.setTitle(R.string.error_label)
|
|
||||||
.setMessage(R.string.null_value_podcast_error)
|
|
||||||
.setOnDismissListener {}
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a progress indicator.
|
* Displays a progress indicator.
|
||||||
*/
|
*/
|
||||||
|
@ -392,7 +380,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
try {
|
try {
|
||||||
val feeds = withContext(Dispatchers.IO) { getFeedList() }
|
val feeds = withContext(Dispatchers.IO) { getFeedList() }
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@OnlineFeedViewFragment.feeds = feeds
|
this@OnlineFeedFragment.feeds = feeds
|
||||||
handleUpdatedFeedStatus()
|
handleUpdatedFeedStatus()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
|
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||||
|
@ -492,7 +480,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (feedSource == "VistaGuide" && isEnableAutodownload)
|
if (feedSource != "VistaGuide" && isEnableAutodownload)
|
||||||
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||||
|
|
||||||
if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
||||||
|
@ -563,7 +551,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false,
|
if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false,
|
||||||
FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
|
FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
|
||||||
|
|
||||||
if (isEnableAutodownload) {
|
if (feedSource != "VistaGuide" && isEnableAutodownload) {
|
||||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
||||||
feed1.preferences!!.autoDownload = autoDownload
|
feed1.preferences!!.autoDownload = autoDownload
|
||||||
val editor = prefs!!.edit()
|
val editor = prefs!!.edit()
|
||||||
|
@ -679,11 +667,22 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showNoPodcastFoundError() {
|
||||||
|
requireActivity().runOnUiThread {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||||
|
.setTitle(R.string.error_label)
|
||||||
|
.setMessage(R.string.null_value_podcast_error)
|
||||||
|
.setOnDismissListener {}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
|
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
|
||||||
AuthenticationDialog(context, titleRes, true, username, password) {
|
AuthenticationDialog(context, titleRes, true, username, password) {
|
||||||
override fun onConfirmed(username: String, password: String) {
|
override fun onConfirmed(username: String, password: String) {
|
||||||
this@OnlineFeedViewFragment.username = username
|
this@OnlineFeedFragment.username = username
|
||||||
this@OnlineFeedViewFragment.password = password
|
this@OnlineFeedFragment.password = password
|
||||||
startFeedBuilding(feedUrl)
|
startFeedBuilding(feedUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -836,7 +835,7 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||||
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
||||||
private const val RESULT_ERROR = 2
|
private const val RESULT_ERROR = 2
|
||||||
private val TAG: String = OnlineFeedViewFragment::class.simpleName ?: "Anonymous"
|
private val TAG: String = OnlineFeedFragment::class.simpleName ?: "Anonymous"
|
||||||
private const val PREFS = "OnlineFeedViewFragmentPreferences"
|
private const val PREFS = "OnlineFeedViewFragmentPreferences"
|
||||||
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
||||||
private const val KEY_UP_ARROW = "up_arrow"
|
private const val KEY_UP_ARROW = "up_arrow"
|
||||||
|
@ -846,8 +845,8 @@ class OnlineFeedViewFragment : Fragment() {
|
||||||
if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newInstance(feedUrl: String): OnlineFeedViewFragment {
|
fun newInstance(feedUrl: String): OnlineFeedFragment {
|
||||||
val fragment = OnlineFeedViewFragment()
|
val fragment = OnlineFeedFragment()
|
||||||
val b = Bundle()
|
val b = Bundle()
|
||||||
b.putString(ARG_FEEDURL, feedUrl)
|
b.putString(ARG_FEEDURL, feedUrl)
|
||||||
fragment.arguments = b
|
fragment.arguments = b
|
|
@ -127,7 +127,7 @@ class OnlineSearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUrl(url: String) {
|
private fun addUrl(url: String) {
|
||||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url)
|
val fragment: Fragment = OnlineFeedFragment.newInstance(url)
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -183,7 +183,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
||||||
val podcast: PodcastSearchResult? = adapter.getItem(position)
|
val podcast: PodcastSearchResult? = adapter.getItem(position)
|
||||||
if (podcast?.feedUrl.isNullOrEmpty()) return
|
if (podcast?.feedUrl.isNullOrEmpty()) return
|
||||||
|
|
||||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast!!.feedUrl!!)
|
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast!!.feedUrl!!)
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -318,7 +318,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
||||||
val podcast = searchResults!![position]
|
val podcast = searchResults!![position]
|
||||||
if (podcast.feedUrl == null) return@OnItemClickListener
|
if (podcast.feedUrl == null) return@OnItemClickListener
|
||||||
|
|
||||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -395,7 +395,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
||||||
inVal.hideSoftInputFromWindow(searchView.windowToken, 0)
|
inVal.hideSoftInputFromWindow(searchView.windowToken, 0)
|
||||||
val query = searchView.query.toString()
|
val query = searchView.query.toString()
|
||||||
if (query.matches("http[s]?://.*".toRegex())) {
|
if (query.matches("http[s]?://.*".toRegex())) {
|
||||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(query)
|
val fragment: Fragment = OnlineFeedFragment.newInstance(query)
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ class SearchResultsFragment : Fragment() {
|
||||||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||||
val podcast = searchResults[position]
|
val podcast = searchResults[position]
|
||||||
if (podcast.feedUrl != null) {
|
if (podcast.feedUrl != null) {
|
||||||
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
val fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||||
fragment.feedSource = podcast.source
|
fragment.feedSource = podcast.source
|
||||||
(activity as MainActivity).loadChildFragment(fragment)
|
(activity as MainActivity).loadChildFragment(fragment)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,559 +0,0 @@
|
||||||
package ac.mdiq.podcini.ui.fragment
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
|
||||||
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
|
||||||
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
|
||||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
|
||||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
|
||||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
|
||||||
import ac.mdiq.podcini.storage.model.Playable
|
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity
|
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
|
||||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
|
||||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
|
||||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
|
||||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
|
||||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
|
||||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
|
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Pair
|
|
||||||
import android.view.*
|
|
||||||
import android.view.animation.*
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.media3.common.util.UnstableApi
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|
||||||
private var _binding: VideoEpisodeFragmentBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
private lateinit var root: ViewGroup
|
|
||||||
|
|
||||||
private var videoControlsVisible = true
|
|
||||||
private var videoSurfaceCreated = false
|
|
||||||
private var lastScreenTap: Long = 0
|
|
||||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
|
||||||
private var showTimeLeft = false
|
|
||||||
private var prog = 0f
|
|
||||||
|
|
||||||
private var itemsLoaded = false
|
|
||||||
private var episode: Episode? = null
|
|
||||||
private var webviewData: String? = null
|
|
||||||
private var webvDescription: ShownotesWebView? = null
|
|
||||||
|
|
||||||
var destroyingDueToReload = false
|
|
||||||
var statusHandler: ServiceStatusHandler? = null
|
|
||||||
var isFavorite = false
|
|
||||||
|
|
||||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
|
||||||
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
|
||||||
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
|
||||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
|
||||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
|
||||||
if (event.x > v.measuredWidth / 2.0f) {
|
|
||||||
onFastForward()
|
|
||||||
showSkipAnimation(true)
|
|
||||||
} else {
|
|
||||||
onRewind()
|
|
||||||
showSkipAnimation(false)
|
|
||||||
}
|
|
||||||
if (videoControlsVisible) {
|
|
||||||
hideVideoControls(false)
|
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
|
||||||
videoControlsVisible = false
|
|
||||||
}
|
|
||||||
return@OnTouchListener true
|
|
||||||
}
|
|
||||||
toggleVideoControlsVisibility()
|
|
||||||
if (videoControlsVisible) setupVideoControlsToggler()
|
|
||||||
lastScreenTap = System.currentTimeMillis()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
|
||||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
|
||||||
holder.setFixedSize(width, height)
|
|
||||||
}
|
|
||||||
@UnstableApi
|
|
||||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
|
||||||
Logd(TAG, "Videoview holder created")
|
|
||||||
videoSurfaceCreated = true
|
|
||||||
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) setVideoSurface(holder)
|
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
|
||||||
setupVideoAspectRatio()
|
|
||||||
}
|
|
||||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
|
||||||
Logd(TAG, "Videosurface was destroyed")
|
|
||||||
videoSurfaceCreated = false
|
|
||||||
(activity as? VideoplayerActivity)?.finish()
|
|
||||||
// TODO: test
|
|
||||||
// if (controller != null && !destroyingDueToReload && !(activity as VideoplayerActivity).switchToAudioOnly)
|
|
||||||
// notifyVideoSurfaceAbandoned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val hideVideoControls = Runnable {
|
|
||||||
if (videoControlsVisible) {
|
|
||||||
Logd(TAG, "Hiding video controls")
|
|
||||||
hideVideoControls(true)
|
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as? AppCompatActivity)?.supportActionBar?.hide()
|
|
||||||
videoControlsVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
|
||||||
super.onCreateView(inflater, container, savedInstanceState)
|
|
||||||
Logd(TAG, "fragment onCreateView")
|
|
||||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
|
||||||
root = binding.root
|
|
||||||
statusHandler = newStatusHandler()
|
|
||||||
statusHandler!!.init()
|
|
||||||
// loadMediaInfo()
|
|
||||||
setupView()
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
|
||||||
return object : ServiceStatusHandler(requireActivity()) {
|
|
||||||
override fun updatePlayButton(showPlay: Boolean) {
|
|
||||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
|
||||||
binding.playButton.setIsShowPlay(showPlay)
|
|
||||||
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
else {
|
|
||||||
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
setupVideoAspectRatio()
|
|
||||||
if (videoSurfaceCreated) {
|
|
||||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
|
||||||
// setVideoSurface(binding.videoView.holder)
|
|
||||||
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun loadMediaInfo() {
|
|
||||||
this@VideoEpisodeFragment.loadMediaInfo()
|
|
||||||
}
|
|
||||||
override fun onPlaybackEnd() {
|
|
||||||
activity?.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
onPositionObserverUpdate()
|
|
||||||
procFlowEvents()
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
cancelFlowEvents()
|
|
||||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
|
||||||
// Controller released; we will not receive buffering updates
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
override fun onPause() {
|
|
||||||
// this does nothing
|
|
||||||
// if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
|
|
||||||
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
|
|
||||||
// }
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
Logd(TAG, "onDestroyView")
|
|
||||||
if (webvDescription != null) {
|
|
||||||
root.removeView(webvDescription!!)
|
|
||||||
webvDescription!!.destroy()
|
|
||||||
}
|
|
||||||
_binding = null
|
|
||||||
statusHandler?.release()
|
|
||||||
statusHandler = null // prevent leak
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var eventSink: Job? = null
|
|
||||||
private fun cancelFlowEvents() {
|
|
||||||
eventSink?.cancel()
|
|
||||||
eventSink = null
|
|
||||||
}
|
|
||||||
private fun procFlowEvents() {
|
|
||||||
if (eventSink != null) return
|
|
||||||
eventSink = lifecycleScope.launch {
|
|
||||||
EventFlow.events.collectLatest { event ->
|
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
|
||||||
when (event) {
|
|
||||||
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
|
|
||||||
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setForVideoMode() {
|
|
||||||
when (videoMode) {
|
|
||||||
VideoMode.FULL_SCREEN_VIEW ->{
|
|
||||||
webvDescription?.visibility = View.GONE
|
|
||||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
|
||||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
VideoMode.WINDOW_VIEW -> {
|
|
||||||
webvDescription?.visibility = View.VISIBLE
|
|
||||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
|
||||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
|
|
||||||
when {
|
|
||||||
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
|
|
||||||
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
|
|
||||||
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupVideoAspectRatio() {
|
|
||||||
if (videoSurfaceCreated) {
|
|
||||||
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
|
||||||
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
|
||||||
val videoWidth = resources.displayMetrics.widthPixels
|
|
||||||
val videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
|
|
||||||
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
|
|
||||||
binding.videoView.setVideoSize(videoWidth, videoHeight)
|
|
||||||
// binding.videoView.setVideoSize(videoSize.first, videoSize.second)
|
|
||||||
// binding.videoView.setVideoSize(-1, -1)
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Could not determine video size")
|
|
||||||
val videoWidth = resources.displayMetrics.widthPixels
|
|
||||||
val videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
|
|
||||||
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
|
|
||||||
binding.videoView.setVideoSize(videoWidth, videoHeight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var loadItemsRunning = false
|
|
||||||
@OptIn(UnstableApi::class)
|
|
||||||
private fun loadMediaInfo() {
|
|
||||||
Logd(TAG, "loadMediaInfo called")
|
|
||||||
if (curMedia == null) return
|
|
||||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
|
||||||
Logd(TAG, "Closing, no longer video")
|
|
||||||
destroyingDueToReload = true
|
|
||||||
activity?.finish()
|
|
||||||
MainActivityStarter(requireContext()).withOpenPlayer().start()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
showTimeLeft = shouldShowRemainingTime()
|
|
||||||
onPositionObserverUpdate()
|
|
||||||
if (!loadItemsRunning) {
|
|
||||||
loadItemsRunning = true
|
|
||||||
lifecycleScope.launch {
|
|
||||||
try {
|
|
||||||
episode = withContext(Dispatchers.IO) {
|
|
||||||
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
|
|
||||||
if (feedItem != null) {
|
|
||||||
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
|
|
||||||
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
|
|
||||||
}
|
|
||||||
feedItem
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
Logd(TAG, "load() item ${episode?.id}")
|
|
||||||
if (episode != null) {
|
|
||||||
val isFav = episode!!.isFavorite
|
|
||||||
if (isFavorite != isFav) {
|
|
||||||
isFavorite = isFav
|
|
||||||
invalidateOptionsMenu(requireActivity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
|
||||||
"text/html", "utf-8", "about:blank")
|
|
||||||
itemsLoaded = true
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
|
||||||
} finally { loadItemsRunning = false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val media = curMedia
|
|
||||||
if (media != null) {
|
|
||||||
(activity as AppCompatActivity).supportActionBar!!.subtitle = media.getEpisodeTitle()
|
|
||||||
(activity as AppCompatActivity).supportActionBar!!.title = media.getFeedTitle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
private fun setupView() {
|
|
||||||
showTimeLeft = shouldShowRemainingTime()
|
|
||||||
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
|
||||||
binding.durationLabel.setOnClickListener {
|
|
||||||
showTimeLeft = !showTimeLeft
|
|
||||||
val media = curMedia ?: return@setOnClickListener
|
|
||||||
val converter = TimeSpeedConverter(curSpeedFB)
|
|
||||||
val length: String
|
|
||||||
if (showTimeLeft) {
|
|
||||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
|
||||||
length = "-" + getDurationStringLong(remainingTime)
|
|
||||||
} else {
|
|
||||||
val duration = converter.convert(media.getDuration())
|
|
||||||
length = getDurationStringLong(duration)
|
|
||||||
}
|
|
||||||
binding.durationLabel.text = length
|
|
||||||
setShowRemainTimeSetting(showTimeLeft)
|
|
||||||
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.sbPosition.setOnSeekBarChangeListener(this)
|
|
||||||
binding.rewindButton.setOnClickListener { onRewind() }
|
|
||||||
binding.rewindButton.setOnLongClickListener {
|
|
||||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
binding.playButton.setIsVideoScreen(true)
|
|
||||||
binding.playButton.setOnClickListener { onPlayPause() }
|
|
||||||
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
|
||||||
binding.fastForwardButton.setOnLongClickListener {
|
|
||||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
// To suppress touches directly below the slider
|
|
||||||
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
|
|
||||||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
|
||||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
|
||||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
setupVideoControlsToggler()
|
|
||||||
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
|
||||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
|
||||||
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
|
||||||
binding.videoView.setAvailableSize(binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
|
||||||
}
|
|
||||||
webvDescription = binding.webvDescription
|
|
||||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
|
||||||
// val cMedia = getMedia
|
|
||||||
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
|
|
||||||
// seekTo(time ?: 0)
|
|
||||||
// } else {
|
|
||||||
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
|
|
||||||
// Snackbar.LENGTH_LONG)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// registerForContextMenu(webvDescription)
|
|
||||||
// webvDescription.visibility = View.GONE
|
|
||||||
binding.toggleViews.setOnClickListener { (activity as? VideoplayerActivity)?.toggleViews() }
|
|
||||||
binding.audioOnly.setOnClickListener {
|
|
||||||
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
|
||||||
(activity as? VideoplayerActivity)?.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toggleVideoControlsVisibility() {
|
|
||||||
if (videoControlsVisible) {
|
|
||||||
hideVideoControls(true)
|
|
||||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
|
||||||
} else {
|
|
||||||
showVideoControls()
|
|
||||||
(activity as AppCompatActivity).supportActionBar?.show()
|
|
||||||
}
|
|
||||||
videoControlsVisible = !videoControlsVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSkipAnimation(isForward: Boolean) {
|
|
||||||
val skipAnimation = AnimationSet(true)
|
|
||||||
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
|
||||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
|
||||||
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
|
||||||
skipAnimation.fillAfter = false
|
|
||||||
skipAnimation.duration = 800
|
|
||||||
|
|
||||||
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
|
||||||
if (isForward) {
|
|
||||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
|
||||||
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
|
||||||
} else {
|
|
||||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
|
||||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
|
||||||
}
|
|
||||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
|
||||||
binding.skipAnimationImage.layoutParams = params
|
|
||||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
|
||||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(animation: Animation) {}
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
|
||||||
binding.skipAnimationImage.visibility = View.GONE
|
|
||||||
}
|
|
||||||
override fun onAnimationRepeat(animation: Animation) {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notifyVideoSurfaceAbandoned() {
|
|
||||||
// playbackService?.notifyVideoSurfaceAbandoned()
|
|
||||||
playbackService?.mPlayer?.pause(abandonFocus = true, reinit = false)
|
|
||||||
playbackService?.mPlayer?.resetVideoSurface()
|
|
||||||
}
|
|
||||||
|
|
||||||
// fun setVideoSurface(holder: SurfaceHolder?) {
|
|
||||||
// playbackService?.mPlayer?.setVideoSurface(holder)
|
|
||||||
// }
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
fun onRewind() {
|
|
||||||
// if (statusHandler == null) return
|
|
||||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
|
||||||
setupVideoControlsToggler()
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
fun onPlayPause() {
|
|
||||||
playPause()
|
|
||||||
setupVideoControlsToggler()
|
|
||||||
}
|
|
||||||
|
|
||||||
@UnstableApi
|
|
||||||
fun onFastForward() {
|
|
||||||
// if (statusHandler == null) return
|
|
||||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
|
||||||
setupVideoControlsToggler()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupVideoControlsToggler() {
|
|
||||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
|
||||||
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showVideoControls() {
|
|
||||||
binding.bottomControlsContainer.visibility = View.VISIBLE
|
|
||||||
binding.controlsContainer.visibility = View.VISIBLE
|
|
||||||
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in)
|
|
||||||
if (animation != null) {
|
|
||||||
binding.bottomControlsContainer.startAnimation(animation)
|
|
||||||
binding.controlsContainer.startAnimation(animation)
|
|
||||||
}
|
|
||||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
|
||||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
|
||||||
// binding.bottomControlsContainer.fitsSystemWindows = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideVideoControls(showAnimation: Boolean) {
|
|
||||||
if (!isAdded) return
|
|
||||||
if (showAnimation) {
|
|
||||||
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out)
|
|
||||||
if (animation != null) {
|
|
||||||
binding.bottomControlsContainer.startAnimation(animation)
|
|
||||||
binding.controlsContainer.startAnimation(animation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
|
||||||
// (activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
|
|
||||||
// or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
// or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
|
||||||
// binding.bottomControlsContainer.fitsSystemWindows = true
|
|
||||||
binding.bottomControlsContainer.visibility = View.GONE
|
|
||||||
binding.controlsContainer.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPositionObserverUpdate() {
|
|
||||||
// if (statusHandler == null) return
|
|
||||||
val converter = TimeSpeedConverter(curSpeedFB)
|
|
||||||
val currentPosition = converter.convert(curPositionFB)
|
|
||||||
val duration_ = converter.convert(curDurationFB)
|
|
||||||
val remainingTime = converter.convert(curDurationFB - curPositionFB)
|
|
||||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
|
||||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
|
||||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
binding.positionLabel.text = getDurationStringLong(currentPosition)
|
|
||||||
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
|
||||||
else binding.durationLabel.text = getDurationStringLong(duration_)
|
|
||||||
|
|
||||||
updateProgressbarPosition(currentPosition, duration_)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
|
||||||
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
|
|
||||||
val progress = (position.toFloat()) / duration
|
|
||||||
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
// if (statusHandler == null) return
|
|
||||||
if (fromUser) {
|
|
||||||
prog = progress / (seekBar.max.toFloat())
|
|
||||||
val converter = TimeSpeedConverter(curSpeedFB)
|
|
||||||
val position = converter.convert((prog * curDurationFB).toInt())
|
|
||||||
binding.seekPositionLabel.text = getDurationStringLong(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
|
||||||
binding.seekCardView.scaleX = .8f
|
|
||||||
binding.seekCardView.scaleY = .8f
|
|
||||||
binding.seekCardView.animate()
|
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
|
||||||
.setDuration(200)
|
|
||||||
.start()
|
|
||||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
||||||
seekTo((prog * curDurationFB).toInt())
|
|
||||||
binding.seekCardView.scaleX = 1f
|
|
||||||
binding.seekCardView.scaleY = 1f
|
|
||||||
binding.seekCardView.animate()
|
|
||||||
.setInterpolator(FastOutSlowInInterpolator())
|
|
||||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
|
||||||
.setDuration(200)
|
|
||||||
.start()
|
|
||||||
setupVideoControlsToggler()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
|
|
||||||
|
|
||||||
val videoSize: Pair<Int, Int>?
|
|
||||||
get() = playbackService?.mPlayer?.getVideoSize()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.view
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
|
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre
|
import ac.mdiq.podcini.playback.base.InTheatre
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.util.config
|
||||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterfaceImpl
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterfaceImpl
|
||||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setCacheDirectory
|
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setCacheDirectory
|
||||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig
|
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.ssl.SslProviderInstaller
|
import ac.mdiq.podcini.net.ssl.SslProviderInstaller
|
||||||
import ac.mdiq.podcini.net.sync.SyncService
|
import ac.mdiq.podcini.net.sync.SyncService
|
||||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||||
|
|
|
@ -7,13 +7,22 @@
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
android:id="@+id/videoEpisodeContainer">
|
android:id="@+id/videoEpisodeContainer">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/topBar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="100dp"
|
||||||
|
android:minHeight="?android:attr/actionBarSize"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:background="@color/black" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/videoPlayerContainer"
|
android:id="@+id/videoPlayerContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/topBar"
|
||||||
android:background="@color/black">
|
android:background="@color/black">
|
||||||
|
|
||||||
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
|
<VideoView
|
||||||
android:id="@+id/videoView"
|
android:id="@+id/videoView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|
|
@ -239,8 +239,8 @@
|
||||||
|
|
||||||
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
|
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
|
||||||
<item name="windowActionBarOverlay">true</item>
|
<item name="windowActionBarOverlay">true</item>
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">true</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">false</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Podcini.TextView.Heading" parent="@android:style/TextAppearance.Medium">
|
<style name="Podcini.TextView.Heading" parent="@android:style/TextAppearance.Medium">
|
||||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.feed
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ac.mdiq.podcini.net.download.serviceinterface
|
package ac.mdiq.podcini.net.download.serviceinterface
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ac.mdiq.podcini.net.download.serviceinterface
|
package ac.mdiq.podcini.net.download.serviceinterface
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
import ac.mdiq.podcini.storage.model.Episode
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ac.mdiq.podcini.storage
|
package ac.mdiq.podcini.storage
|
||||||
|
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package ac.mdiq.podcini.storage
|
package ac.mdiq.podcini.storage
|
||||||
|
|
||||||
import ac.mdiq.podcini.feed.FeedMother.anyFeed
|
import ac.mdiq.podcini.feed.FeedMother.anyFeed
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||||
import ac.mdiq.podcini.storage.database.Queues
|
import ac.mdiq.podcini.storage.database.Queues
|
||||||
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
|
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package ac.mdiq.podcini.util.syndication
|
package ac.mdiq.podcini.util.syndication
|
||||||
|
|
||||||
import ac.mdiq.podcini.ui.fragment.OnlineFeedViewFragment
|
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.apache.commons.io.IOUtils
|
import org.apache.commons.io.IOUtils
|
||||||
|
@ -19,13 +19,13 @@ import java.nio.charset.StandardCharsets
|
||||||
*/
|
*/
|
||||||
@RunWith(RobolectricTestRunner::class)
|
@RunWith(RobolectricTestRunner::class)
|
||||||
class FeedDiscovererTest {
|
class FeedDiscovererTest {
|
||||||
private var fd: OnlineFeedViewFragment.FeedDiscoverer? = null
|
private var fd: OnlineFeedFragment.FeedDiscoverer? = null
|
||||||
|
|
||||||
private var testDir: File? = null
|
private var testDir: File? = null
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
fd = OnlineFeedViewFragment.FeedDiscoverer()
|
fd = OnlineFeedFragment.FeedDiscoverer()
|
||||||
testDir = File(InstrumentationRegistry
|
testDir = File(InstrumentationRegistry
|
||||||
.getInstrumentation().targetContext.filesDir, "FeedDiscovererTest")
|
.getInstrumentation().targetContext.filesDir, "FeedDiscovererTest")
|
||||||
testDir!!.mkdir()
|
testDir!!.mkdir()
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
# 6.5.5
|
||||||
|
|
||||||
|
* corrected issue of Youtube channel being set for auto-download when subscribing
|
||||||
|
* fixed various issues on video sizing and further refined the video player
|
||||||
|
* some class restructuring and refactoring and nullalability adjustments
|
||||||
|
* updated various dependencies
|
||||||
|
|
||||||
# 6.5.4
|
# 6.5.4
|
||||||
|
|
||||||
* in the search bar of OnlineSearch view, search button is moved to the end of the bar
|
* in the search bar of OnlineSearch view, search button is moved to the end of the bar
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
Version 6.5.5 brings several changes:
|
||||||
|
|
||||||
|
* corrected issue of Youtube channel being set for auto-download when subscribing
|
||||||
|
* fixed various issues on video sizing and further refined the video player
|
||||||
|
* some class restructuring and refactoring and nullalability adjustments
|
||||||
|
* updated various dependencies
|
Loading…
Reference in New Issue