5.0.0 commit

This commit is contained in:
Xilin Jia 2024-05-11 17:38:02 +01:00
parent eeea4bfd17
commit 5fcb4c3939
154 changed files with 2637 additions and 2211 deletions

View File

@ -16,7 +16,9 @@ Apache License 2.0
[org.jsoup](https://jsoup.org/license) The MIT License
[com.github.bumptech.glide](https://github.com/bumptech/glide/blob/master/LICENSE) Various
[io.coil-kt](https://github.com/coil-kt/coil/blob/main/LICENSE.txt) Apache License 2.0
[net.dankito.readability4j](https://github.com/dankito/Readability4J/blob/master/LICENSE) Apache License 2.0
[com.squareup.okhttp3](https://github.com/square/okhttp/blob/master/LICENSE.txt) Apache License 2.0

View File

@ -1,18 +1,24 @@
# Podcini
<img width="100" src="https://raw.githubusercontent.com/xilinjia/podcini/main/images/icon 256x256.png" align="left" style="margin-right:15px"/>
Podcini is an open source podcast manager/player project.
Podcini is an open source podcast instrument, attuned to Puccini <img src="./images/Puccini.jpg" height="50" />, adorned with pasticcini <img src="./images/pasticcini.jpg" height="50" /> and aromatized with porcini <img src="./images/porcini.jpg" height="50" />.
This project is a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
This project is based on a fork of [AntennaPod](<https://github.com/AntennaPod/AntennaPod>) as of Feb 5 2024.
Compared to AntennaPod this project:
1. Migrated the media player to `androidx.media3`,
2. Added `AudioOffloadMode` support, which is supposed to be kind to device battery,
3. Relies on the most recent dependencies,
4. Is __purely__ Kotlin based,
4. Targets Android 14,
5. Aims to improve efficiency and provide more user-friendly features
1. Migrated all media routines to `androidx.media3`,
2. Plays in `AudioOffloadMode`, kind to device battery,
3. Is purely `Kotlin` based and mono-modular,
4. Targets Android 14 with updated dependencies,
5. Outfits with Viewbinding and modern image library Coil,
6. Boasts new UI's including streamlined drawer, subscriptions view and player controller
7. Offers Readability for RSS contents and TTS integration,
8. Features `instant sync` across devices without a server.
The project aims to improve efficiency and provide more useful and user-friendly features.
~Even so, the database remains backward compatible, and AntennaPod's db can be easily imported.~ Since version 4.10.0 and/or AntennaPod 3.3.2, AntennaPod's DB can not be directly imported
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
@ -20,20 +26,6 @@ Compared to AntennaPod this project:
Or download the latest APK from the [Releases Section](https://github.com/XilinJia/Podcini/releases/latest).
## Version 4
Some drastic changes are made in the project since version 4.0:
- A whole new interface of the Subscriptions page showing only the feeds with tags as filters, no longer having tags as folders in the page,
- Home, Echo and Inbox pages are removed from the project,
- Subscriptions is now the default page,
- Feed list are no longer shown in the drawer,
- Access to statistics is in the drawer.
- `OnlineFeedView` activity is stripped down to only receive externally shared feeds,
- Viewbindings are enabled for most views,
- Project became mono-modular.
~Even so, the database remains backward compatible, and AntennaPod's db can be easily imported.~ Since version 4.10.0 and/or AntennaPod 3.3.2, AntennaPod's DB can not be directly imported
## Notable new features & enhancements
### Player
@ -67,6 +59,7 @@ Some drastic changes are made in the project since version 4.0:
### Podcast/Episode list
* A whole new interface of the Subscriptions page showing only the feeds with tags as filters, no longer having tags as folders in the page,
* New and efficient ways of click and long-click operations on lists:
* click on title area opens the podcast/episode
* long-press on title area automatically enters in selection mode
@ -94,8 +87,15 @@ Some drastic changes are made in the project since version 4.0:
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes
* Online feed episodes can be freely played (streamed) without a subscription
* externally shared feed opens in the new online feed view fragment
* OnlineFeedView` activity is stripped down to only receive externally shared feeds
* Youtube channels are accepted from external share or paste of address in podcast search view, and can be subscribed as a normal podcast, though video play is handled externally
### Instant (or Wifi) sync
* Ability to sync between devices on the same wifi network without a server (experimental release, back up before use)
* It syncs the play states (position and played) of episodes that exist in both devices (ensure to refresh first) and that have been played (completed or not)
* So far, every sync is a full sync, no subscription feeds sync, and no media files sync
### Security
* Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure

View File

@ -74,6 +74,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
}
@ -158,8 +159,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020139
versionName "4.10.1"
versionCode 3020140
versionName "5.0.0"
def commit = ""
try {
@ -197,10 +198,12 @@ android {
resValue "string", "provider_authority", "ac.mdiq.podcini.debug.provider"
}
release {
resValue "string", "app_name", "Podcini"
resValue "string", "provider_authority", "ac.mdiq.podcini.provider"
minifyEnabled true
shrinkResources true
signingConfig signingConfigs.releaseConfig
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg"
}
}
applicationVariants.configureEach { variant ->
@ -219,6 +222,7 @@ android {
dependencies {
implementation "androidx.core:core-ktx:1.12.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.android.volley:volley:1.2.1'
constraints {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
@ -256,13 +260,14 @@ dependencies {
implementation 'commons-io:commons-io:2.16.1'
implementation "org.jsoup:jsoup:1.17.2"
implementation "com.github.bumptech.glide:glide:4.16.0"
implementation "com.github.bumptech.glide:okhttp3-integration:4.16.0@aar"
kapt "com.github.bumptech.glide:ksp:4.16.0"
implementation "io.coil-kt:coil:2.6.0"
// implementation "com.github.bumptech.glide:glide:4.16.0"
// implementation "com.github.bumptech.glide:okhttp3-integration:4.16.0@aar"
// kapt "com.github.bumptech.glide:ksp:4.16.0"
implementation "com.squareup.okhttp3:okhttp:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
implementation 'com.squareup.okio:okio:3.9.0'
implementation "org.greenrobot:eventbus:3.3.1"

View File

@ -22,6 +22,7 @@ import ac.mdiq.podcini.storage.DBReader.getQueue
import ac.mdiq.podcini.storage.DBReader.getQueueIDList
import ac.mdiq.podcini.storage.DBWriter.clearQueue
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter.Companion.unfiltered
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.playback.base.PlayerStatus
@ -87,7 +88,7 @@ class PlaybackTest {
setupPlaybackController()
playFromQueue(0)
Awaitility.await().atMost(5, TimeUnit.SECONDS)
.until { controller!!.status == PlayerStatus.INITIALIZED }
.until { MediaPlayerBase.status == PlayerStatus.INITIALIZED }
}
@Test
@ -148,11 +149,11 @@ class PlaybackTest {
// let playback run a bit then pause
Awaitility.await()
.atMost(1000, TimeUnit.MILLISECONDS)
.until { PlayerStatus.PLAYING == controller!!.status }
.until { PlayerStatus.PLAYING == MediaPlayerBase.status }
pauseEpisode()
Awaitility.await()
.atMost(1000, TimeUnit.MILLISECONDS)
.until { PlayerStatus.PAUSED == controller!!.status }
.until { PlayerStatus.PAUSED == MediaPlayerBase.status }
Assert.assertThat("Ensure even with smart mark as play, after pause, the item remains in the queue.",
getQueue(), Matchers.hasItems(feedItem))

View File

@ -3,7 +3,7 @@ package de.test.podcini.service.playback
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPCallback {
private var isCancelled = false
@ -12,7 +12,7 @@ class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPC
isCancelled = true
}
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
if (isCancelled) {
return
}

View File

@ -3,10 +3,10 @@ package de.test.podcini.service.playback
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
open class DefaultPSMPCallback : PSMPCallback {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
}
override fun shouldStop() {

View File

@ -7,7 +7,7 @@ import ac.mdiq.podcini.playback.service.LocalMediaPlayer
import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance
@ -80,7 +80,7 @@ class MediaPlayerBaseTest {
Assert.assertEquals(0, httpServer!!.serveFile(dest).toLong())
}
private fun checkPSMPInfo(info: PSMPInfo?) {
private fun checkPSMPInfo(info: MediaPlayerInfo?) {
try {
when (info!!.playerStatus) {
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.PREPARING, PlayerStatus.INITIALIZED, PlayerStatus.INITIALIZING, PlayerStatus.SEEKING -> Assert.assertNotNull(
@ -126,7 +126,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -168,7 +168,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -211,7 +211,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(4)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -257,7 +257,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(5)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -305,7 +305,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -346,7 +346,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -387,7 +387,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(4)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -432,7 +432,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(5)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
check(newInfo!!.playerStatus != PlayerStatus.ERROR) { "MediaPlayer error" }
@ -486,7 +486,7 @@ class MediaPlayerBaseTest {
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
when {
newInfo!!.playerStatus == PlayerStatus.ERROR -> {
@ -607,7 +607,7 @@ class MediaPlayerBaseTest {
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
when {
newInfo!!.playerStatus == PlayerStatus.ERROR -> {
@ -666,7 +666,7 @@ class MediaPlayerBaseTest {
val latchCount = 1
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
if (newInfo!!.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null) assertionError = UnexpectedStateChange(newInfo.playerStatus)
@ -739,7 +739,7 @@ class MediaPlayerBaseTest {
val latchCount = 2
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
if (newInfo!!.playerStatus == PlayerStatus.ERROR) {
if (assertionError == null) assertionError = UnexpectedStateChange(newInfo.playerStatus)

View File

@ -1,6 +1,6 @@
package de.test.podcini.util.service.download
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.util.Logd
import android.util.Base64
import android.util.Log
import fi.iki.elonen.NanoHTTPD
@ -62,7 +62,7 @@ class HTTPBin : NanoHTTPD(0) {
}
override fun serve(session: IHTTPSession): Response {
if (BuildConfig.DEBUG) Log.d(TAG, "Requested url: " + session.uri)
Logd(TAG, "Requested url: " + session.uri)
val segments = session.uri.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (segments.size < 3) {

View File

@ -7,8 +7,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

View File

@ -1,8 +1,8 @@
package ac.mdiq.podcini.feed.parser
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.feed.parser.util.TypeGetter
import android.util.Log
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import org.apache.commons.io.input.XmlStreamReader
import org.xml.sax.InputSource
import org.xml.sax.SAXException
@ -30,7 +30,7 @@ class FeedHandler {
val inputStreamReader: Reader = XmlStreamReader(File(feed.file_url!!))
val inputSource = InputSource(inputStreamReader)
Log.d("FeedHandler", "starting saxParser.parse")
Logd("FeedHandler", "starting saxParser.parse")
saxParser.parse(inputSource, handler)
inputStreamReader.close()
}

View File

@ -11,12 +11,10 @@ import java.util.*
* Contains all relevant information to describe the current state of a
* SyndHandler.
*/
class HandlerState(
/**
* Feed that the Handler is currently processing.
*/
@JvmField var feed: Feed
) {
/**
* Feed that the Handler is currently processing.
*/
class HandlerState(@JvmField var feed: Feed) {
/**
* Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the
* URL of the feed, the value is the title

View File

@ -1,9 +1,9 @@
package ac.mdiq.podcini.feed.parser
import android.util.Log
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.feed.parser.namespace.*
import ac.mdiq.podcini.feed.parser.util.TypeGetter
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import org.xml.sax.Attributes
import org.xml.sax.SAXException
import org.xml.sax.helpers.DefaultHandler
@ -57,37 +57,37 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
DEFAULT_PREFIX -> state.defaultNamespaces.push(Atom())
Atom.NSTAG -> {
state.namespaces[uri] = Atom()
Log.d(TAG, "Recognized Atom namespace")
Logd(TAG, "Recognized Atom namespace")
}
}
}
uri == Content.NSURI && prefix == Content.NSTAG -> {
state.namespaces[uri] = Content()
Log.d(TAG, "Recognized Content namespace")
Logd(TAG, "Recognized Content namespace")
}
uri == Itunes.NSURI && prefix == Itunes.NSTAG -> {
state.namespaces[uri] = Itunes()
Log.d(TAG, "Recognized ITunes namespace")
Logd(TAG, "Recognized ITunes namespace")
}
uri == YouTube.NSURI && prefix == YouTube.NSTAG -> {
state.namespaces[uri] = YouTube()
Log.d(TAG, "Recognized YouTube namespace")
Logd(TAG, "Recognized YouTube namespace")
}
uri == SimpleChapters.NSURI && prefix.matches(SimpleChapters.NSTAG.toRegex()) -> {
state.namespaces[uri] = SimpleChapters()
Log.d(TAG, "Recognized SimpleChapters namespace")
Logd(TAG, "Recognized SimpleChapters namespace")
}
uri == Media.NSURI && prefix == Media.NSTAG -> {
state.namespaces[uri] = Media()
Log.d(TAG, "Recognized media namespace")
Logd(TAG, "Recognized media namespace")
}
uri == DublinCore.NSURI && prefix == DublinCore.NSTAG -> {
state.namespaces[uri] = DublinCore()
Log.d(TAG, "Recognized DublinCore namespace")
Logd(TAG, "Recognized DublinCore namespace")
}
uri == PodcastIndex.NSURI || uri == PodcastIndex.NSURI2 && prefix == PodcastIndex.NSTAG -> {
state.namespaces[uri] = PodcastIndex()
Log.d(TAG, "Recognized PodcastIndex namespace")
Logd(TAG, "Recognized PodcastIndex namespace")
}
}
}

View File

@ -1,8 +1,7 @@
package ac.mdiq.podcini.feed.parser.media.vorbis
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.storage.model.feed.Chapter
import android.util.Log
import ac.mdiq.podcini.util.Logd
import java.io.InputStream
import java.util.concurrent.TimeUnit
@ -15,7 +14,7 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
@Throws(VorbisCommentReaderException::class)
public override fun onContentVectorValue(key: String?, value: String?) {
if (BuildConfig.DEBUG) Log.d(TAG, "Key: $key, value: $value")
Logd(TAG, "Key: $key, value: $value")
val attribute = getAttributeTypeFromKey(key)
val id = getIdFromKey(key)

View File

@ -1,21 +1,21 @@
package ac.mdiq.podcini.feed.parser.namespace
import ac.mdiq.podcini.feed.parser.HandlerState
import android.util.Log
import ac.mdiq.podcini.storage.model.feed.FeedFunding
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.feed.parser.element.AtomText
import ac.mdiq.podcini.feed.parser.element.SyndElement
import ac.mdiq.podcini.feed.parser.util.DateUtils.parseOrNullIfFuture
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.getMimeType
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.isMediaFile
import ac.mdiq.podcini.feed.parser.util.SyndStringUtils.trimAllWhitespace
import ac.mdiq.podcini.storage.model.feed.FeedFunding
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.util.Logd
import org.xml.sax.Attributes
class Atom : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
Log.d(TAG, "handleElementStart $localName")
// Log.d(TAG, "handleElementStart $localName")
when {
ENTRY == localName -> {
state.currentItem = FeedItem()
@ -40,7 +40,7 @@ class Atom : Namespace() {
try {
if (strSize != null) size = strSize.toLong()
} catch (e: NumberFormatException) {
Log.d(TAG, "Length attribute could not be parsed.")
Logd(TAG, "Length attribute could not be parsed.")
}
val mimeType: String? = getMimeType(attributes.getValue(LINK_TYPE), href)
@ -98,7 +98,7 @@ class Atom : Namespace() {
}
override fun handleElementEnd(localName: String, state: HandlerState) {
Log.d(TAG, "handleElementEnd $localName")
// Log.d(TAG, "handleElementEnd $localName")
if (ENTRY == localName) {
if (state.currentItem != null && state.tempObjects.containsKey(Itunes.DURATION)) {
val currentItem = state.currentItem

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.feed.parser.namespace
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.feed.parser.HandlerState
import ac.mdiq.podcini.feed.parser.element.AtomText
import ac.mdiq.podcini.feed.parser.element.SyndElement
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.getMimeType
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.isImageFile
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.isMediaFile
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.util.Logd
import android.util.Log
import org.xml.sax.Attributes
import java.util.concurrent.TimeUnit
@ -14,7 +15,7 @@ import java.util.concurrent.TimeUnit
/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */
class Media : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
Log.d(TAG, "handleElementStart $localName")
// Log.d(TAG, "handleElementStart $localName")
when (localName) {
CONTENT -> {
val url: String? = attributes.getValue(DOWNLOAD_URL)
@ -68,7 +69,7 @@ class Media : Namespace() {
Log.e(TAG, "Duration \"$durationStr\" could not be parsed")
}
}
Log.d(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
val media = FeedMedia(state.currentItem, url, size, mimeType)
if (durationMs > 0) media.setDuration( durationMs)
@ -97,7 +98,7 @@ class Media : Namespace() {
}
override fun handleElementEnd(localName: String, state: HandlerState) {
Log.d(TAG, "handleElementEnd $localName")
// Log.d(TAG, "handleElementEnd $localName")
if (DESCRIPTION == localName) {
val content = state.contentBuf.toString()
state.currentItem?.setDescriptionIfLonger(content)

View File

@ -1,15 +1,15 @@
package ac.mdiq.podcini.feed.parser.namespace
import android.util.Log
import androidx.core.text.HtmlCompat
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.feed.parser.HandlerState
import ac.mdiq.podcini.feed.parser.element.SyndElement
import ac.mdiq.podcini.feed.parser.util.DateUtils.parseOrNullIfFuture
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.getMimeType
import ac.mdiq.podcini.feed.parser.util.MimeTypeUtils.isMediaFile
import ac.mdiq.podcini.feed.parser.util.SyndStringUtils.trimAllWhitespace
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.util.Logd
import androidx.core.text.HtmlCompat
import org.xml.sax.Attributes
/**
@ -17,6 +17,7 @@ import org.xml.sax.Attributes
*/
class Rss20 : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
// Log.d(TAG, "handleElementStart $localName")
when {
ITEM == localName && CHANNEL == state.tagstack.lastElement()?.name -> {
state.currentItem = FeedItem()
@ -35,7 +36,7 @@ class Rss20 : Namespace() {
// less than 16kb is suspicious, check manually
if (size < 16384) size = 0
} catch (e: NumberFormatException) {
Log.d(TAG, "Length attribute could not be parsed.")
Logd(TAG, "Length attribute could not be parsed.")
}
val media = FeedMedia(state.currentItem, url, size, mimeType)
if(state.currentItem != null) state.currentItem!!.media = media
@ -46,6 +47,7 @@ class Rss20 : Namespace() {
}
override fun handleElementEnd(localName: String, state: HandlerState) {
// Log.d(TAG, "handleElementEnd $localName")
when {
ITEM == localName -> {
if (state.currentItem != null) {
@ -76,9 +78,9 @@ class Rss20 : Namespace() {
if (state.tagstack.size >= 3) third = state.thirdTag.name
when {
// some feed creators include an empty or non-standard guid-element in their feed,
// which should be ignored
GUID == top && ITEM == second -> {
// some feed creators include an empty or non-standard guid-element in their feed,
// which should be ignored
if (contentRaw.isNotEmpty() && state.currentItem != null) state.currentItem!!.itemIdentifier = contentRaw
}
TITLE == top -> {
@ -94,8 +96,8 @@ class Rss20 : Namespace() {
}
}
PUBDATE == top && ITEM == second && state.currentItem != null -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)
// prefer itunes:image
URL == top && IMAGE == second && CHANNEL == third -> {
// prefer itunes:image
if (state.feed.imageUrl == null) state.feed.imageUrl = content
}
DESCR == localName -> {

View File

@ -9,7 +9,7 @@ import org.xml.sax.Attributes
class YouTube : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
Log.d(TAG, "handleElementStart $localName")
// Log.d(TAG, "handleElementStart $localName")
if (IMAGE == localName) {
val url: String? = attributes.getValue(IMAGE_HREF)
@ -25,7 +25,7 @@ class YouTube : Namespace() {
}
override fun handleElementEnd(localName: String, state: HandlerState) {
Log.d(TAG, "handleElementEnd $localName")
// Log.d(TAG, "handleElementEnd $localName")
if (state.contentBuf == null) return
val content = state.contentBuf.toString()

View File

@ -1,8 +1,8 @@
package ac.mdiq.podcini.feed.parser.util
import android.util.Log
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.feed.parser.UnsupportedFeedtypeException
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import org.apache.commons.io.input.XmlStreamReader
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlPullParser
@ -36,7 +36,7 @@ class TypeGetter {
when (val tag = xpp.name) {
ATOM_ROOT -> {
feed.type = Feed.TYPE_ATOM1
Log.d(TAG, "Recognized type Atom")
Logd(TAG, "Recognized type Atom")
val strLang = xpp.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang")
if (strLang != null) feed.language = strLang
@ -48,23 +48,23 @@ class TypeGetter {
when (strVersion) {
null -> {
feed.type = Feed.TYPE_RSS2
Log.d(TAG, "Assuming type RSS 2.0")
Logd(TAG, "Assuming type RSS 2.0")
return Type.RSS20
}
"2.0" -> {
feed.type = Feed.TYPE_RSS2
Log.d(TAG, "Recognized type RSS 2.0")
Logd(TAG, "Recognized type RSS 2.0")
return Type.RSS20
}
"0.91", "0.92" -> {
Log.d(TAG, "Recognized type RSS 0.91/0.92")
Logd(TAG, "Recognized type RSS 0.91/0.92")
return Type.RSS091
}
else -> throw UnsupportedFeedtypeException("Unsupported rss version")
}
}
else -> {
Log.d(TAG, "Type is invalid")
Logd(TAG, "Type is invalid")
throw UnsupportedFeedtypeException(Type.INVALID, tag)
}
}
@ -85,25 +85,25 @@ class TypeGetter {
Jsoup.parse(File(feed.file_url!!))
rootElement = "html"
} catch (e1: IOException) {
Log.d(TAG, "IOException: " + feed.file_url)
Logd(TAG, "IOException: " + feed.file_url)
e1.printStackTrace()
}
throw UnsupportedFeedtypeException(Type.INVALID, rootElement)
} catch (e: IOException) {
Log.d(TAG, "IOException: " + feed.file_url)
Logd(TAG, "IOException: " + feed.file_url)
e.printStackTrace()
} finally {
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
Log.d(TAG, "IOException: $reader")
Logd(TAG, "IOException: $reader")
e.printStackTrace()
}
}
}
}
Log.d(TAG, "Type is invalid")
Logd(TAG, "Type is invalid")
throw UnsupportedFeedtypeException(Type.INVALID)
}
@ -114,11 +114,11 @@ class TypeGetter {
try {
reader = XmlStreamReader(File(feed.file_url!!))
} catch (e: FileNotFoundException) {
Log.d(TAG, "FileNotFoundException: " + feed.file_url)
Logd(TAG, "FileNotFoundException: " + feed.file_url)
e.printStackTrace()
return null
} catch (e: IOException) {
Log.d(TAG, "IOException: " + feed.file_url)
Logd(TAG, "IOException: " + feed.file_url)
e.printStackTrace()
return null
}

View File

@ -1,49 +0,0 @@
package ac.mdiq.podcini.glide
import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import java.io.InputStream
import java.nio.ByteBuffer
/**
* {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule}
*/
@GlideModule
class ApGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDefaultRequestOptions(RequestOptions()
.format(DecodeFormat.PREFER_ARGB_8888)
.diskCacheStrategy(DiskCacheStrategy.ALL))
builder.setLogLevel(Log.WARN)
@SuppressLint("UsableSpace") val spaceAvailable = context.cacheDir.usableSpace
val imageCacheSize = if ((spaceAvailable > 2 * GIGABYTES)) (250 * MEGABYTES) else (50 * MEGABYTES)
Log.d(TAG, "Free space on cache dir: $spaceAvailable, using image cache size: $imageCacheSize")
builder.setDiskCache(InternalCacheDiskCacheFactory(context, imageCacheSize))
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.replace(String::class.java, InputStream::class.java, MetadataRetrieverLoader.Factory(context))
registry.append(String::class.java, InputStream::class.java, GenerativePlaceholderImageModelLoader.Factory())
registry.append(String::class.java, InputStream::class.java, ApOkHttpUrlLoader.Factory())
registry.append(String::class.java, InputStream::class.java, NoHttpStringLoader.StreamFactory())
registry.append(EmbeddedChapterImage::class.java, ByteBuffer::class.java, ChapterImageModelLoader.Factory())
}
companion object {
private const val TAG = "ApGlideModule"
private const val MEGABYTES = (1024 * 1024).toLong()
private const val GIGABYTES = (1024 * 1024 * 1024).toLong()
}
}

View File

@ -1,88 +0,0 @@
package ac.mdiq.podcini.glide
import android.content.ContentResolver
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.newBuilder
import ac.mdiq.podcini.util.NetworkUtils.isImageAllowed
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import okhttp3.*
import okhttp3.Interceptor.Chain
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.io.InputStream
import kotlin.concurrent.Volatile
/**
* {@see com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader}.
*/
internal class ApOkHttpUrlLoader private constructor(private val client: OkHttpClient?) :
ModelLoader<String, InputStream> {
/**
* The default factory for [ApOkHttpUrlLoader]s.
*/
class Factory internal constructor() : ModelLoaderFactory<String, InputStream> {
private val client: OkHttpClient? = internalClient
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> {
return ApOkHttpUrlLoader(client)
}
override fun teardown() {
// Do nothing, this instance doesn't own the client.
}
companion object {
@Volatile
private var internalClient: OkHttpClient? = null
get() {
if (field == null) {
synchronized(Factory::class.java) {
if (field == null) {
val builder: OkHttpClient.Builder = newBuilder()
builder.interceptors().add(NetworkAllowanceInterceptor())
builder.cache(null) // Handled by Glide
field = builder.build()
}
}
}
return field
}
}
}
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
return ModelLoader.LoadData(ObjectKey(model), ResizingOkHttpStreamFetcher(client, GlideUrl(model)))
}
override fun handles(model: String): Boolean {
return (model.isNotEmpty() // If the other loaders fail, do not attempt to load as web resource
&& !model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
&& !model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER) // Leave content URIs to Glide's default loaders
&& !model.startsWith(ContentResolver.SCHEME_CONTENT)
&& !model.startsWith(ContentResolver.SCHEME_ANDROID_RESOURCE))
}
private class NetworkAllowanceInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Chain): Response {
return if (isImageAllowed) {
chain.proceed(chain.request())
} else {
Response.Builder()
.protocol(Protocol.HTTP_2)
.code(420)
.message("Policy Not Fulfilled")
.body(ResponseBody.create(null, ByteArray(0)))
.request(chain.request())
.build()
}
}
}
}

View File

@ -1,44 +0,0 @@
package ac.mdiq.podcini.glide
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat
import java.io.ByteArrayInputStream
import java.io.InputStream
// see https://github.com/bumptech/glide/issues/699
internal class AudioCoverFetcher(private val path: String, private val context: Context) : DataFetcher<InputStream?> {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream?>) {
try {
MediaMetadataRetrieverCompat().use { retriever ->
if (path.startsWith(ContentResolver.SCHEME_CONTENT)) retriever.setDataSource(context, Uri.parse(path))
else retriever.setDataSource(path)
val picture = retriever.embeddedPicture
if (picture != null) callback.onDataReady(ByteArrayInputStream(picture))
}
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}
override fun cleanup() {
// nothing to clean up
}
override fun cancel() {
// cannot cancel
}
override fun getDataClass(): Class<InputStream?> {
return InputStream::class.java as Class<InputStream?>
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
}

View File

@ -1,85 +0,0 @@
package ac.mdiq.podcini.glide
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import okhttp3.Request.Builder
import org.apache.commons.io.IOUtils
import java.io.BufferedInputStream
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.nio.ByteBuffer
class ChapterImageModelLoader : ModelLoader<EmbeddedChapterImage?, ByteBuffer?> {
class Factory : ModelLoaderFactory<EmbeddedChapterImage?, ByteBuffer?> {
override fun build(unused: MultiModelLoaderFactory): ModelLoader<EmbeddedChapterImage?, ByteBuffer?> {
return ChapterImageModelLoader()
}
override fun teardown() {
// Do nothing.
}
}
override fun buildLoadData(model: EmbeddedChapterImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<ByteBuffer?> {
return ModelLoader.LoadData(ObjectKey(model), EmbeddedImageFetcher(model))
}
override fun handles(model: EmbeddedChapterImage): Boolean {
return true
}
internal class EmbeddedImageFetcher(private val image: EmbeddedChapterImage) : DataFetcher<ByteBuffer?> {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in ByteBuffer?>) {
var stream: BufferedInputStream? = null
try {
if (image.media.localFileAvailable()) {
val localFile = File(image.media.getLocalMediaUrl())
stream = BufferedInputStream(FileInputStream(localFile))
IOUtils.skip(stream, image.position.toLong())
val imageContent = ByteArray(image.length)
IOUtils.read(stream, imageContent, 0, image.length)
callback.onDataReady(ByteBuffer.wrap(imageContent))
} else {
val httpReq = Builder()
// Skipping would download the whole file
httpReq.header("Range", "bytes=" + image.position + "-" + (image.position + image.length))
val url = image.media.getStreamUrl()
if (url != null) httpReq.url(url)
val response = getHttpClient()!!.newCall(httpReq.build()).execute()
if (!response.isSuccessful || response.body == null) throw IOException("Invalid response: " + response.code + " " + response.message)
callback.onDataReady(ByteBuffer.wrap(response.body!!.bytes()))
}
} catch (e: IOException) {
callback.onLoadFailed(e)
} finally {
IOUtils.closeQuietly(stream)
}
}
override fun cleanup() {
// nothing to clean up
}
override fun cancel() {
// cannot cancel
}
override fun getDataClass(): Class<ByteBuffer?> {
return ByteBuffer::class.java as Class<ByteBuffer?>
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
}
}

View File

@ -1,109 +0,0 @@
package ac.mdiq.podcini.glide
import android.graphics.*
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import ac.mdiq.podcini.storage.model.feed.Feed
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.util.*
class GenerativePlaceholderImageModelLoader : ModelLoader<String, InputStream> {
class Factory : ModelLoaderFactory<String, InputStream> {
override fun build(unused: MultiModelLoaderFactory): ModelLoader<String, InputStream> {
return GenerativePlaceholderImageModelLoader()
}
override fun teardown() {
// Do nothing.
}
}
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream?> {
return ModelLoader.LoadData(ObjectKey(model), EmbeddedImageFetcher(model, width, height))
}
override fun handles(model: String): Boolean {
return model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
}
internal class EmbeddedImageFetcher(private val model: String, private val width: Int, private val height: Int) : DataFetcher<InputStream?> {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream?>) {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val generator = Random(model.hashCode().toLong())
val lineGridSteps = 4 + generator.nextInt(4)
val slope = width / 4
val shadowWidth = width * 0.01f
val lineDistance = (width.toFloat() / (lineGridSteps - 2))
val baseColor = PALETTES[generator.nextInt(PALETTES.size)]
val paint = Paint()
var color = randomShadeOfGrey(generator)
paint.color = color
paint.strokeWidth = lineDistance
paint.setColorFilter(PorterDuffColorFilter(baseColor, PorterDuff.Mode.MULTIPLY))
val paintShadow = Paint()
paintShadow.color = -0x1000000
paintShadow.strokeWidth = lineDistance
val forcedColorChange = 1 + generator.nextInt(lineGridSteps - 2)
for (i in lineGridSteps - 1 downTo 0) {
val linePos = (i - 0.5f) * lineDistance
val switchColor = generator.nextFloat() < 0.3f || i == forcedColorChange
if (switchColor) {
var newColor = color
while (newColor == color) {
newColor = randomShadeOfGrey(generator)
}
color = newColor
paint.color = newColor
canvas.drawLine(linePos + slope + shadowWidth, -slope.toFloat(), linePos - slope + shadowWidth, (height + slope).toFloat(), paintShadow)
}
canvas.drawLine(linePos + slope, -slope.toFloat(), linePos - slope, (height + slope).toFloat(), paint)
}
val gradientPaint = Paint()
paint.isDither = true
gradientPaint.setShader(LinearGradient(0f, 0f, 0f, height.toFloat(), 0x00000000, 0x55000000, Shader.TileMode.CLAMP))
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
val baos = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
val `is`: InputStream = ByteArrayInputStream(baos.toByteArray())
callback.onDataReady(`is`)
}
override fun cleanup() {
// nothing to clean up
}
override fun cancel() {
// cannot cancel
}
override fun getDataClass(): Class<InputStream?> {
return InputStream::class.java as Class<InputStream?>
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
companion object {
private val PALETTES = intArrayOf(-0x876f64, -0x9100, -0xc771c4, -0xff7c71, -0x84e05e, -0x48e3e4, -0xde690d)
private fun randomShadeOfGrey(generator: Random): Int {
return -0x888889 + 0x222222 * generator.nextInt(5)
}
}
}
}

View File

@ -1,33 +0,0 @@
package ac.mdiq.podcini.glide
import android.content.Context
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.signature.ObjectKey
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import java.io.InputStream
internal class MetadataRetrieverLoader private constructor(private val context: Context) : ModelLoader<String, InputStream> {
/**
* The default factory for [MetadataRetrieverLoader]s.
*/
class Factory internal constructor(private val context: Context) : ModelLoaderFactory<String, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> {
return MetadataRetrieverLoader(context)
}
override fun teardown() {
// Do nothing, this instance doesn't own the client.
}
}
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream?> {
return ModelLoader.LoadData(ObjectKey(model), AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context))
}
override fun handles(model: String): Boolean {
return model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER)
}
}

View File

@ -1,34 +0,0 @@
package ac.mdiq.podcini.glide
import android.net.Uri
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.load.model.StringLoader
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import java.io.InputStream
/**
* StringLoader that does not handle http/https urls. Used to avoid fallback to StringLoader when
* Podcini blocks mobile image loading.
*/
class NoHttpStringLoader(uriLoader: ModelLoader<Uri, InputStream?>?) : StringLoader<InputStream?>(uriLoader) {
class StreamFactory : ModelLoaderFactory<String, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, InputStream> {
val uriLoader_ = multiFactory.build(Uri::class.java, InputStream::class.java)
return NoHttpStringLoader(uriLoader_) as ModelLoader<String, InputStream>
}
override fun teardown() {
// Do nothing.
}
}
override fun handles(model: String): Boolean {
return (!model.startsWith("http") // If the custom loaders fail, do not attempt to load with Glide internal loaders
&& !model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
&& !model.startsWith(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER)
&& super.handles(model))
}
}

View File

@ -1,122 +0,0 @@
package ac.mdiq.podcini.glide
import android.graphics.Bitmap.CompressFormat
import android.graphics.BitmapFactory
import android.os.Build
import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl
import okhttp3.Call
import org.apache.commons.io.FileUtils
import org.apache.commons.io.IOUtils
import java.io.*
import kotlin.math.floor
import kotlin.math.ln
import kotlin.math.max
import kotlin.math.pow
class ResizingOkHttpStreamFetcher(client: Call.Factory?, url: GlideUrl?) : OkHttpStreamFetcher(client, url) {
private var stream: FileInputStream? = null
private var tempIn: File? = null
private var tempOut: File? = null
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
super.loadData(priority, object : DataFetcher.DataCallback<InputStream?> {
override fun onDataReady(data: InputStream?) {
if (data == null) {
callback.onDataReady(null)
return
}
try {
tempIn = File.createTempFile("resize_", null)
tempOut = File.createTempFile("resize_", null)
val outputStream: OutputStream = FileOutputStream(tempIn)
IOUtils.copy(data, outputStream)
outputStream.close()
IOUtils.closeQuietly(data)
if (tempIn != null && tempIn!!.length() <= MAX_FILE_SIZE) {
try {
stream = FileInputStream(tempIn)
callback.onDataReady(stream) // Just deliver the original, non-scaled image
} catch (fileNotFoundException: FileNotFoundException) {
callback.onLoadFailed(fileNotFoundException)
}
return
}
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
var inVal = FileInputStream(tempIn)
BitmapFactory.decodeStream(inVal, null, options)
IOUtils.closeQuietly(inVal)
when {
options.outWidth == -1 || options.outHeight == -1 -> throw IOException("Not a valid image")
max(options.outHeight.toDouble(), options.outWidth.toDouble()) >= MAX_DIMENSIONS -> {
val sampleSize = max(options.outHeight.toDouble(), options.outWidth.toDouble()) / MAX_DIMENSIONS
options.inSampleSize = 2.0.pow(floor(ln(sampleSize) / ln(2.0))).toInt()
}
}
options.inJustDecodeBounds = false
inVal = FileInputStream(tempIn)
val bitmap = BitmapFactory.decodeStream(inVal, null, options)
IOUtils.closeQuietly(inVal)
val format = if (Build.VERSION.SDK_INT < 30) CompressFormat.WEBP else CompressFormat.WEBP_LOSSY
var quality = 100
if (tempOut != null) while (true) {
val out = FileOutputStream(tempOut)
bitmap!!.compress(format, quality, out)
IOUtils.closeQuietly(out)
quality -= when {
tempOut!!.length() > 3 * MAX_FILE_SIZE && quality >= 45 -> 40
tempOut!!.length() > 2 * MAX_FILE_SIZE && quality >= 25 -> 20
tempOut!!.length() > MAX_FILE_SIZE && quality >= 15 -> 10
tempOut!!.length() > MAX_FILE_SIZE && quality >= 10 -> 5
else -> break
}
}
bitmap?.recycle()
stream = FileInputStream(tempOut)
callback.onDataReady(stream)
if (tempIn != null && tempOut != null)
Log.d(TAG, "Compressed image from ${tempIn!!.length() / 1024} to ${tempOut!!.length() / 1024} kB (quality: $quality%)")
} catch (e: Throwable) {
e.printStackTrace()
try {
stream = FileInputStream(tempIn)
callback.onDataReady(stream) // Just deliver the original, non-scaled image
} catch (fileNotFoundException: FileNotFoundException) {
e.printStackTrace()
callback.onLoadFailed(fileNotFoundException)
}
}
}
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
}
override fun cleanup() {
IOUtils.closeQuietly(stream)
FileUtils.deleteQuietly(tempIn)
FileUtils.deleteQuietly(tempOut)
super.cleanup()
}
companion object {
private const val TAG = "ResizingOkHttpStreamFet"
private const val MAX_DIMENSIONS = 1500
private const val MAX_FILE_SIZE = 1024 * 1024 // 1 MB
}
}

View File

@ -19,12 +19,12 @@ object UrlChecker {
/**
* Checks if URL is valid and modifies it if necessary.
*
* @param url The url which is going to be prepared
* @param url_ The url which is going to be prepared
* @return The prepared url
*/
@JvmStatic
fun prepareUrl(url: String): String {
var url = url
fun prepareUrl(url_: String): String {
var url = url_
url = url.trim { it <= ' ' }
val lowerCaseUrl = url.lowercase() // protocol names are case insensitive
when {
@ -69,15 +69,15 @@ object UrlChecker {
* Checks if URL is valid and modifies it if necessary.
* This method also handles protocol relative URLs.
*
* @param url The url which is going to be prepared
* @param base The url against which the (possibly relative) url is applied. If this is null,
* @param url_ The url which is going to be prepared
* @param base_ The url against which the (possibly relative) url is applied. If this is null,
* the result of prepareURL(url) is returned instead.
* @return The prepared url
*/
@JvmStatic
fun prepareUrl(url: String, base: String?): String {
var url = url
var base = base ?: return prepareUrl(url)
fun prepareUrl(url_: String, base_: String?): String {
var url = url_
var base = base_ ?: return prepareUrl(url)
url = url.trim { it <= ' ' }
base = prepareUrl(base)
val urlUri = Uri.parse(url)

View File

@ -28,8 +28,11 @@ object MediaSizeLoader {
var size = Int.MIN_VALUE.toLong()
when {
media.isDownloaded() -> {
val mediaFile = File(media.getLocalMediaUrl())
if (mediaFile.exists()) size = mediaFile.length()
val url = media.getLocalMediaUrl()
if (!url.isNullOrEmpty()) {
val mediaFile = File(url)
if (mediaFile.exists()) size = mediaFile.length()
}
}
!media.checkedOnSizeButUnknown() -> {
// only query the network if we haven't already checked
@ -62,7 +65,7 @@ object MediaSizeLoader {
}
}
}
Log.d(TAG, "new size: $size")
// Log.d(TAG, "new size: $size")
// they didn't tell us the size, but we don't want to keep querying on it
if (size <= 0) media.setCheckedOnSizeButUnknown()
else media.size = size

View File

@ -1,13 +1,13 @@
package ac.mdiq.podcini.net.download.service
import android.util.Log
import android.webkit.URLUtil
import ac.mdiq.podcini.util.FileNameGenerator
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.util.FileNameGenerator
import ac.mdiq.podcini.util.Logd
import android.webkit.URLUtil
import org.apache.commons.io.FilenameUtils
import java.io.File
@ -24,7 +24,7 @@ object DownloadRequestCreator {
val dest = File(feedfilePath, getFeedfileName(feed))
if (dest.exists()) dest.delete()
Log.d(TAG, "Requesting download feed from url " + feed.download_url)
Logd(TAG, "Requesting download feed from url " + feed.download_url)
val username = feed.preferences?.username
val password = feed.preferences?.password
@ -43,7 +43,7 @@ object DownloadRequestCreator {
if (dest.exists() && !partiallyDownloadedFileExists) dest = findUnusedFile(dest)!!
Log.d(TAG, "Requesting download media from url " + media.download_url)
Logd(TAG, "Requesting download media from url " + media.download_url)
val username = media.item?.feed?.preferences?.username
val password = media.item?.feed?.preferences?.password
@ -56,10 +56,10 @@ object DownloadRequestCreator {
var newDest: File? = null
for (i in 1 until Int.MAX_VALUE) {
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
Log.d(TAG, "Testing filename $newName")
Logd(TAG, "Testing filename $newName")
newDest = File(dest.parent, newName)
if (!newDest.exists()) {
Log.d(TAG, "File doesn't exist yet. Using $newName")
Logd(TAG, "File doesn't exist yet. Using $newName")
break
}
}

View File

@ -1,16 +1,17 @@
package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
import ac.mdiq.podcini.net.download.service.handler.MediaDownloadedHandler
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.download.DownloadError
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.config.ClientConfigurator
import ac.mdiq.podcini.util.event.MessageEvent
import android.app.Notification
@ -88,7 +89,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
nm.cancel(R.id.notification_downloading)
}
}
Log.d(TAG, "Worker for " + media.download_url + " returned.")
Logd(TAG, "Worker for " + media.download_url + " returned.")
return result
}
@ -122,7 +123,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
downloader = DefaultDownloaderFactory().create(request)
if (downloader == null) {
Log.d(TAG, "Unable to create downloader")
Logd(TAG, "Unable to create downloader")
return Result.failure()
}
@ -146,7 +147,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
}
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) {
Log.d(TAG, "Requested invalid range, restarting download from the beginning")
Logd(TAG, "Requested invalid range, restarting download from the beginning")
if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
sendMessage(request.title?:"", false)
return retry3times()

View File

@ -1,22 +1,19 @@
package ac.mdiq.podcini.net.download.service
import ac.mdiq.podcini.R
import android.text.TextUtils
import android.util.Log
import ac.mdiq.podcini.feed.parser.util.DateUtils.parse
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podcini.util.StorageUtils.freeSpaceAvailable
import ac.mdiq.podcini.util.URIUtil.getURIFromRequestUrl
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.storage.model.download.DownloadError
import ac.mdiq.podcini.storage.model.download.DownloadResult
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.feed.parser.util.DateUtils.parse
import okhttp3.CacheControl
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podcini.util.StorageUtils.freeSpaceAvailable
import ac.mdiq.podcini.util.URIUtil.getURIFromRequestUrl
import android.text.TextUtils
import android.util.Log
import okhttp3.*
import okhttp3.internal.http.StatusLine
import org.apache.commons.io.IOUtils
import java.io.*
@ -42,10 +39,10 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
httpReq.tag(downloadRequest)
httpReq.cacheControl(CacheControl.Builder().noStore().build())
Log.d(TAG, "starting download: " + downloadRequest.feedfileType + " " + uri.scheme)
Logd(TAG, "starting download: " + downloadRequest.feedfileType + " " + uri.scheme)
if (downloadRequest.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
// set header explicitly so that okhttp doesn't do transparent gzip
Log.d(TAG, "addHeader(\"Accept-Encoding\", \"identity\")")
Logd(TAG, "addHeader(\"Accept-Encoding\", \"identity\")")
httpReq.addHeader("Accept-Encoding", "identity")
httpReq.cacheControl(CacheControl.Builder().noCache().build()) // noStore breaks CDNs
}
@ -58,11 +55,11 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
if (lastModifiedDate != null) {
val threeDaysAgo = System.currentTimeMillis() - 1000 * 60 * 60 * 24 * 3
if (lastModifiedDate.time > threeDaysAgo) {
Log.d(TAG, "addHeader(\"If-Modified-Since\", \"$lastModified\")")
Logd(TAG, "addHeader(\"If-Modified-Since\", \"$lastModified\")")
httpReq.addHeader("If-Modified-Since", lastModified?:"")
}
} else {
Log.d(TAG, "addHeader(\"If-None-Match\", \"$lastModified\")")
Logd(TAG, "addHeader(\"If-None-Match\", \"$lastModified\")")
httpReq.addHeader("If-None-Match", lastModified?:"")
}
}
@ -71,7 +68,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
if (fileExists && destination.length() > 0) {
downloadRequest.soFar = destination.length()
httpReq.addHeader("Range", "bytes=" + downloadRequest.soFar + "-")
Log.d(TAG, "Adding range header: " + downloadRequest.soFar)
Logd(TAG, "Adding range header: " + downloadRequest.soFar)
}
val response = newCall(httpReq)
@ -80,12 +77,12 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
var isGzip = false
if (!contentEncodingHeader.isNullOrEmpty()) isGzip = TextUtils.equals(contentEncodingHeader.lowercase(Locale.getDefault()), "gzip")
Log.d(TAG, "Response code is " + response.code)// check if size specified in the response header is the same as the size of the
Logd(TAG, "Response code is " + response.code)// check if size specified in the response header is the same as the size of the
// written file. This check cannot be made if compression was used
// Log.d(TAG,"buffer: $buffer")
when {
!response.isSuccessful && response.code == HttpURLConnection.HTTP_NOT_MODIFIED -> {
Log.d(TAG, "Feed '" + downloadRequest.source + "' not modified since last update, Download canceled")
Logd(TAG, "Feed '" + downloadRequest.source + "' not modified since last update, Download canceled")
onCancelled()
return
}
@ -106,7 +103,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
if (fileExists && response.code == HttpURLConnection.HTTP_PARTIAL && !contentRangeHeader.isNullOrEmpty()) {
val start = contentRangeHeader.substring("bytes ".length, contentRangeHeader.indexOf("-"))
downloadRequest.soFar = start.toLong()
Log.d(TAG, "Starting download at position " + downloadRequest.soFar)
Logd(TAG, "Starting download at position " + downloadRequest.soFar)
out = RandomAccessFile(destination, "rw")
out.seek(downloadRequest.soFar)
@ -121,19 +118,19 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
val buffer = ByteArray(BUFFER_SIZE)
var count = 0
downloadRequest.setStatusMsg(R.string.download_running)
Log.d(TAG, "Getting size of download")
Logd(TAG, "Getting size of download")
downloadRequest.size = responseBody.contentLength() + downloadRequest.soFar
Log.d(TAG, "Size is " + downloadRequest.size)
Logd(TAG, "Size is " + downloadRequest.size)
if (downloadRequest.size < 0) downloadRequest.size = DownloadResult.SIZE_UNKNOWN.toLong()
val freeSpace = freeSpaceAvailable
Log.d(TAG, "Free space is $freeSpace")
Logd(TAG, "Free space is $freeSpace")
if (downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.size > freeSpace) {
onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null)
return
}
Log.d(TAG, "Starting download")
Logd(TAG, "Starting download")
try {
while (!cancelled && (connection.read(buffer).also { count = it }) != -1) {
// Log.d(TAG,"buffer: $buffer")
@ -230,9 +227,9 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
e.printStackTrace()
}
}
Log.d(TAG, "content length: $contentLength")
Logd(TAG, "content length: $contentLength")
val contentType = response.header("Content-Type")
Log.d(TAG, "content type: $contentType")
Logd(TAG, "content type: $contentType")
return contentType != null && contentType.startsWith("text/") && contentLength < 100 * 1024
}
@ -276,28 +273,28 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
val secondUrl = responses[1]!!.request.url.toString()
when {
firstCode == HttpURLConnection.HTTP_MOVED_PERM || firstCode == StatusLine.HTTP_PERM_REDIRECT -> {
Log.d(TAG, "Detected permanent redirect from " + downloadRequest.source + " to " + secondUrl)
Logd(TAG, "Detected permanent redirect from " + downloadRequest.source + " to " + secondUrl)
permanentRedirectUrl = secondUrl
}
secondUrl == firstUrl.replace("http://", "https://") -> {
Log.d(TAG, "Treating http->https non-permanent redirect as permanent: $firstUrl")
Logd(TAG, "Treating http->https non-permanent redirect as permanent: $firstUrl")
permanentRedirectUrl = secondUrl
}
}
}
private fun onSuccess() {
Log.d(TAG, "Download was successful")
Logd(TAG, "Download was successful")
result.setSuccessful()
}
private fun onFail(reason: DownloadError, reasonDetailed: String?) {
Log.d(TAG, "onFail() called with: reason = [$reason], reasonDetailed = [$reasonDetailed]")
Logd(TAG, "onFail() called with: reason = [$reason], reasonDetailed = [$reasonDetailed]")
result.setFailed(reason, reasonDetailed?:"")
}
private fun onCancelled() {
Log.d(TAG, "Download was cancelled")
Logd(TAG, "Download was cancelled")
result.setCancelled()
cancelled = true
}

View File

@ -1,19 +1,20 @@
package ac.mdiq.podcini.net.download.service.handler
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.storage.model.download.DownloadError
import ac.mdiq.podcini.storage.model.download.DownloadResult
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import android.content.Context
import android.media.MediaMetadataRetriever
import android.util.Log
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.storage.model.download.DownloadError
import ac.mdiq.podcini.storage.model.download.DownloadResult
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.concurrent.ExecutionException
@ -47,10 +48,10 @@ class MediaDownloadedHandler(private val context: Context, var updatedStatus: Do
mmr.setDataSource(media.file_url)
durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
media.setDuration( durationStr!!.toInt())
Log.d(TAG, "Duration of file is " + media.getDuration())
Logd(TAG, "Duration of file is " + media.getDuration())
}
} catch (e: NumberFormatException) {
Log.d(TAG, "Invalid file duration: $durationStr")
Logd(TAG, "Invalid file duration: $durationStr")
} catch (e: Exception) {
Log.e(TAG, "Get duration failed", e)
media.setDuration(30000)

View File

@ -1,53 +0,0 @@
package ac.mdiq.podcini.net.sync
import android.util.Log
import androidx.collection.ArrayMap
import androidx.core.util.Pair
import ac.mdiq.podcini.net.sync.model.EpisodeAction
object EpisodeActionFilter {
const val TAG: String = "EpisodeActionFilter"
fun getRemoteActionsOverridingLocalActions(remoteActions: List<EpisodeAction>, queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
// make sure more recent local actions are not overwritten by older remote actions
val remoteActionsThatOverrideLocalActions: MutableMap<Pair<String, String>, EpisodeAction> = ArrayMap()
val localMostRecentPlayActions = createUniqueLocalMostRecentPlayActions(queuedEpisodeActions)
for (remoteAction in remoteActions) {
if (remoteAction.podcast == null || remoteAction.episode == null) continue
val key = Pair(remoteAction.podcast!!, remoteAction.episode!!)
when (remoteAction.action) {
EpisodeAction.Action.NEW, EpisodeAction.Action.DOWNLOAD -> {}
EpisodeAction.Action.PLAY -> {
val localMostRecent = localMostRecentPlayActions[key]
if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) break
val remoteMostRecentAction = remoteActionsThatOverrideLocalActions[key]
if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) break
remoteActionsThatOverrideLocalActions[key] = remoteAction
}
EpisodeAction.Action.DELETE -> {}
else -> Log.e(TAG, "Unknown remoteAction: $remoteAction")
}
}
return remoteActionsThatOverrideLocalActions
}
private fun createUniqueLocalMostRecentPlayActions(queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
val localMostRecentPlayAction: MutableMap<Pair<String, String>, EpisodeAction> =
ArrayMap()
for (action in queuedEpisodeActions) {
if (action.podcast == null || action.episode == null) continue
val key = Pair(action.podcast!!, action.episode!!)
val mostRecent = localMostRecentPlayAction[key]
when {
mostRecent?.timestamp == null -> localMostRecentPlayAction[key] = action
mostRecent.timestamp!!.before(action.timestamp) -> localMostRecentPlayAction[key] = action
}
}
return localMostRecentPlayAction
}
private fun secondActionOverridesFirstAction(firstAction: EpisodeAction, secondAction: EpisodeAction?): Boolean {
return secondAction?.timestamp != null && (firstAction.timestamp == null || secondAction.timestamp!!.after(firstAction.timestamp))
}
}

View File

@ -1,9 +0,0 @@
package ac.mdiq.podcini.net.sync
object GuidValidator {
@JvmStatic
fun isValidGuid(guid: String?): Boolean {
return (guid != null && !guid.trim { it <= ' ' }.isEmpty() && guid != "null")
}
}

View File

@ -15,13 +15,14 @@ class HostnameParser(hosturl: String?) {
var subfolder: String? = null
init {
val m = URLSPLIT_REGEX.matcher(hosturl)
val m = URLSPLIT_REGEX.matcher(hosturl?:"")
if (m.matches()) {
scheme = m.group(1)
host = IDN.toASCII(m.group(2))
// regex -> can only be digits
port = if (m.group(3) == null) -1 else m.group(3).toInt()
subfolder = if (m.group(4) == null) "" else StringUtils.stripEnd(m.group(4), "/")
port = m.group(3)?.toInt() ?: -1
val mg4 = m.group(4)
subfolder = if (mg4 == null) "" else StringUtils.stripEnd(mg4, "/")
} else {
// URL does not match regex: use it anyway -> this will cause an exception on connect
scheme = "https"

View File

@ -28,8 +28,7 @@ object LockingAsyncExecutor {
} finally {
lock.unlock()
}
}.subscribeOn(Schedulers.io())
.subscribe()
}.subscribeOn(Schedulers.io()).subscribe()
}
}
}

View File

@ -1,4 +1,4 @@
package ac.mdiq.podcini.net.sync.gpoddernet.mapper
package ac.mdiq.podcini.net.sync
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject

View File

@ -3,8 +3,7 @@ package ac.mdiq.podcini.net.sync
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.common.UrlChecker.containsUrl
import ac.mdiq.podcini.net.download.FeedUpdateManager.runOnce
import ac.mdiq.podcini.net.sync.EpisodeActionFilter.getRemoteActionsOverridingLocalActions
import ac.mdiq.podcini.net.sync.GuidValidator.isValidGuid
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor.executeLockedAsync
import ac.mdiq.podcini.net.sync.SynchronizationCredentials.deviceID
import ac.mdiq.podcini.net.sync.SynchronizationCredentials.hosturl
@ -19,15 +18,14 @@ import ac.mdiq.podcini.net.sync.nextcloud.NextcloudSyncService
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueStorage
import ac.mdiq.podcini.preferences.UserPreferences.gpodnetNotificationsEnabled
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileSync
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.storage.DBReader.getEpisodes
import ac.mdiq.podcini.storage.DBReader.getFeedItemByGuidOrEpisodeUrl
import ac.mdiq.podcini.storage.DBReader.getFeedListDownloadUrls
import ac.mdiq.podcini.storage.DBReader.loadAdditionalFeedItemListData
import ac.mdiq.podcini.storage.DBTasks.removeFeedWithDownloadUrl
import ac.mdiq.podcini.storage.DBTasks.updateFeed
import ac.mdiq.podcini.storage.DBWriter.removeQueueItem
import ac.mdiq.podcini.storage.DBWriter.persistItemList
import ac.mdiq.podcini.storage.DBWriter.removeQueueItem
import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
@ -39,6 +37,8 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.util.Log
import androidx.annotation.OptIn
import androidx.collection.ArrayMap
import androidx.core.app.NotificationCompat
import androidx.media3.common.util.UnstableApi
import androidx.work.*
@ -47,11 +47,12 @@ import org.apache.commons.lang3.StringUtils
import org.greenrobot.eventbus.EventBus
import java.util.concurrent.TimeUnit
class SyncService(context: Context, params: WorkerParameters) : Worker(context, params) {
private val synchronizationQueueStorage = SynchronizationQueueStorage(context)
@OptIn(UnstableApi::class)
open class SyncService(context: Context, params: WorkerParameters) : Worker(context, params) {
protected val synchronizationQueueStorage = SynchronizationQueueStorage(context)
@UnstableApi override fun doWork(): Result {
Log.d(TAG, "doWork() called")
val activeSyncProvider = getActiveSyncProvider() ?: return Result.failure()
SynchronizationSettings.updateLastSynchronizationAttempt()
@ -60,7 +61,14 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
activeSyncProvider.login()
syncSubscriptions(activeSyncProvider)
waitForDownloadServiceCompleted()
syncEpisodeActions(activeSyncProvider)
// sync Episode changes
// syncEpisodeActions(activeSyncProvider)
var (lastSync, newTimeStamp) = getEpisodeActions(activeSyncProvider)
// upload local actions
newTimeStamp = pushEpisodeActions(activeSyncProvider, lastSync, newTimeStamp)
SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
activeSyncProvider.logout()
clearErrorNotifications()
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_success))
@ -86,14 +94,15 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
@UnstableApi @Throws(SyncServiceException::class)
private fun syncSubscriptions(syncServiceImpl: ISyncService) {
Log.d(TAG, "syncSubscriptions called")
val lastSync = SynchronizationSettings.lastSubscriptionSynchronizationTimestamp
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_subscriptions))
val localSubscriptions: List<String?> = getFeedListDownloadUrls()
val localSubscriptions: List<String> = getFeedListDownloadUrls()
val subscriptionChanges = syncServiceImpl.getSubscriptionChanges(lastSync)
var newTimeStamp = subscriptionChanges?.timestamp?:0L
val queuedRemovedFeeds: MutableList<String> = synchronizationQueueStorage.queuedRemovedFeeds
var queuedAddedFeeds: List<String?> = synchronizationQueueStorage.queuedAddedFeeds
var queuedAddedFeeds: List<String> = synchronizationQueueStorage.queuedAddedFeeds
Log.d(TAG, "Downloaded subscription changes: $subscriptionChanges")
if (subscriptionChanges != null) {
@ -122,6 +131,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
queuedRemovedFeeds.removeAll(subscriptionChanges.removed)
}
}
if (queuedAddedFeeds.isNotEmpty() || queuedRemovedFeeds.size > 0) {
Log.d(TAG, "Added: " + StringUtils.join(queuedAddedFeeds, ", "))
Log.d(TAG, "Removed: " + StringUtils.join(queuedRemovedFeeds, ", "))
@ -139,6 +149,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
}
private fun waitForDownloadServiceCompleted() {
Log.d(TAG, "waitForDownloadServiceCompleted called")
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_wait_for_downloads))
try {
while (true) {
@ -151,16 +162,18 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
}
}
@UnstableApi @Throws(SyncServiceException::class)
private fun syncEpisodeActions(syncServiceImpl: ISyncService) {
fun getEpisodeActions(syncServiceImpl: ISyncService) : Pair<Long, Long> {
val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_download))
val getResponse = syncServiceImpl.getEpisodeActionChanges(lastSync)
var newTimeStamp = getResponse?.timestamp?:0L
val newTimeStamp = getResponse?.timestamp?:0L
val remoteActions = getResponse?.episodeActions?: listOf()
processEpisodeActions(remoteActions)
return Pair(lastSync, newTimeStamp)
}
// upload local actions
open fun pushEpisodeActions(syncServiceImpl: ISyncService, lastSync: Long, newTimeStamp_: Long): Long {
var newTimeStamp = newTimeStamp_
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_upload))
val queuedEpisodeActions: MutableList<EpisodeAction> = synchronizationQueueStorage.queuedEpisodeActions
if (lastSync == 0L) {
@ -172,13 +185,13 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
.currentTimestamp()
.started(media.getDuration() / 1000)
.position(media.getDuration() / 1000)
.position(media.getPosition() / 1000)
.total(media.getDuration() / 1000)
.build()
queuedEpisodeActions.add(played)
}
}
if (queuedEpisodeActions.size > 0) {
if (queuedEpisodeActions.isNotEmpty()) {
LockingAsyncExecutor.lock.lock()
try {
Log.d(TAG, "Uploading ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
@ -190,11 +203,44 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
LockingAsyncExecutor.lock.unlock()
}
}
return newTimeStamp
}
@UnstableApi @Throws(SyncServiceException::class)
private fun syncEpisodeActions(syncServiceImpl: ISyncService) {
Log.d(TAG, "syncEpisodeActions called")
var (lastSync, newTimeStamp) = getEpisodeActions(syncServiceImpl)
// upload local actions
newTimeStamp = pushEpisodeActions(syncServiceImpl, lastSync, newTimeStamp)
SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
}
open fun processEpisodeAction(action: EpisodeAction): Pair<Long, FeedItem>? {
val guid = if (isValidGuid(action.guid)) action.guid else null
val feedItem = getFeedItemByGuidOrEpisodeUrl(guid, action.episode?:"")
if (feedItem == null) {
Log.i(TAG, "Unknown feed item: $action")
return null
}
if (feedItem.media == null) {
Log.i(TAG, "Feed item has no media: $action")
return null
}
var idRemove = 0L
feedItem.media!!.setPosition(action.position * 1000)
if (hasAlmostEnded(feedItem.media!!)) {
Log.d(TAG, "Marking as played: $action")
feedItem.setPlayed(true)
feedItem.media!!.setPosition(0)
idRemove = feedItem.id
} else Log.d(TAG, "Setting position: $action")
return Pair(idRemove, feedItem)
}
@UnstableApi @Synchronized
private fun processEpisodeActions(remoteActions: List<EpisodeAction>) {
fun processEpisodeActions(remoteActions: List<EpisodeAction>) {
Log.d(TAG, "Processing " + remoteActions.size + " actions")
if (remoteActions.isEmpty()) return
@ -202,38 +248,22 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
val queueToBeRemoved = LongList()
val updatedItems: MutableList<FeedItem> = ArrayList()
for (action in playActionsToUpdate.values) {
val guid = if (isValidGuid(action.guid)) action.guid else null
val feedItem = getFeedItemByGuidOrEpisodeUrl(guid, action.episode?:"")
if (feedItem == null) {
Log.i(TAG, "Unknown feed item: $action")
continue
}
if (feedItem.media == null) {
Log.i(TAG, "Feed item has no media: $action")
continue
}
feedItem.media!!.setPosition(action.position * 1000)
if (hasAlmostEnded(feedItem.media!!)) {
Log.d(TAG, "Marking as played: $action")
feedItem.setPlayed(true)
feedItem.media!!.setPosition(0)
queueToBeRemoved.add(feedItem.id)
} else Log.d(TAG, "Setting position: $action")
updatedItems.add(feedItem)
val result = processEpisodeAction(action) ?: continue
if (result.first != 0L) queueToBeRemoved.add(result.first)
updatedItems.add(result.second)
}
removeQueueItem(applicationContext, false, *queueToBeRemoved.toArray())
loadAdditionalFeedItemListData(updatedItems)
persistItemList(updatedItems)
}
private fun clearErrorNotifications() {
protected fun clearErrorNotifications() {
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.cancel(R.id.notification_gpodnet_sync_error)
nm.cancel(R.id.notification_gpodnet_sync_autherror)
}
private fun updateErrorNotification(exception: Exception) {
protected fun updateErrorNotification(exception: Exception) {
Log.d(TAG, "Posting sync error notification")
val description = ("${applicationContext.getString(R.string.gpodnetsync_error_descr)}${exception.message}")
@ -270,10 +300,13 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
if (selectedService == null) return null
return when (selectedService) {
SynchronizationProviderViewData.GPODDER_NET -> GpodnetService(getHttpClient(),
hosturl, deviceID?:"", username?:"", password?:"")
SynchronizationProviderViewData.NEXTCLOUD_GPODDER -> NextcloudSyncService(getHttpClient(),
hosturl, username?:"", password?:"")
// SynchronizationProviderViewData.WIFI -> {
//// if (hosturl != null) WifiImplSyncService(hosturl!!, hostport)
//// else null
// null
// }
SynchronizationProviderViewData.GPODDER_NET -> GpodnetService(getHttpClient(), hosturl, deviceID?:"", username?:"", password?:"")
SynchronizationProviderViewData.NEXTCLOUD_GPODDER -> NextcloudSyncService(getHttpClient(), hosturl, username?:"", password?:"")
}
}
@ -282,7 +315,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
private const val WORK_ID_SYNC = "SyncServiceWorkId"
private var isCurrentlyActive = false
private fun setCurrentlyActive(active: Boolean) {
internal fun setCurrentlyActive(active: Boolean) {
isCurrentlyActive = active
}
@ -308,14 +341,14 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
fun sync(context: Context) {
val workRequest: OneTimeWorkRequest = getWorkRequest().build()
WorkManager.getInstance(context!!).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
}
fun syncImmediately(context: Context) {
val workRequest: OneTimeWorkRequest = getWorkRequest()
.setInitialDelay(0L, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context!!).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
}
fun fullSync(context: Context) {
@ -324,8 +357,54 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
val workRequest: OneTimeWorkRequest = getWorkRequest()
.setInitialDelay(0L, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context!!).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
}
}
fun getRemoteActionsOverridingLocalActions(remoteActions: List<EpisodeAction>, queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
// make sure more recent local actions are not overwritten by older remote actions
val remoteActionsThatOverrideLocalActions: MutableMap<Pair<String, String>, EpisodeAction> = ArrayMap()
val localMostRecentPlayActions = createUniqueLocalMostRecentPlayActions(queuedEpisodeActions)
for (remoteAction in remoteActions) {
if (remoteAction.podcast == null || remoteAction.episode == null) continue
val key = Pair(remoteAction.podcast, remoteAction.episode)
when (remoteAction.action) {
EpisodeAction.Action.NEW, EpisodeAction.Action.DOWNLOAD -> {}
EpisodeAction.Action.PLAY -> {
val localMostRecent = localMostRecentPlayActions[key]
if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) break
val remoteMostRecentAction = remoteActionsThatOverrideLocalActions[key]
if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) break
remoteActionsThatOverrideLocalActions[key] = remoteAction
}
EpisodeAction.Action.DELETE -> {}
else -> Log.e(TAG, "Unknown remoteAction: $remoteAction")
}
}
return remoteActionsThatOverrideLocalActions
}
private fun createUniqueLocalMostRecentPlayActions(queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
val localMostRecentPlayAction: MutableMap<Pair<String, String>, EpisodeAction> = ArrayMap()
for (action in queuedEpisodeActions) {
if (action.podcast == null || action.episode == null) continue
val key = Pair(action.podcast, action.episode)
val mostRecent = localMostRecentPlayAction[key]
when {
mostRecent?.timestamp == null -> localMostRecentPlayAction[key] = action
mostRecent.timestamp.before(action.timestamp) -> localMostRecentPlayAction[key] = action
}
}
return localMostRecentPlayAction
}
private fun secondActionOverridesFirstAction(firstAction: EpisodeAction, secondAction: EpisodeAction?): Boolean {
return secondAction?.timestamp != null && (firstAction.timestamp == null || secondAction.timestamp.after(firstAction.timestamp))
}
fun isValidGuid(guid: String?): Boolean {
return (guid != null && guid.trim { it <= ' ' }.isNotEmpty() && guid != "null")
}
}
}

View File

@ -15,6 +15,7 @@ object SynchronizationCredentials {
private const val PREF_PASSWORD = "ac.mdiq.podcini.preferences.gpoddernet.password"
private const val PREF_DEVICEID = "ac.mdiq.podcini.preferences.gpoddernet.deviceID"
private const val PREF_HOSTNAME = "prefGpodnetHostname"
private const val PREF_HOSTPORT = "prefHostport"
private val preferences: SharedPreferences
get() = ClientConfig.applicationCallbacks!!.getApplicationInstance()!!.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
@ -47,6 +48,13 @@ object SynchronizationCredentials {
preferences.edit().putString(PREF_HOSTNAME, value).apply()
}
@JvmStatic
var hostport: Int
get() = preferences.getInt(PREF_HOSTPORT, 0)
set(value) {
preferences.edit().putInt(PREF_HOSTPORT, value).apply()
}
@Synchronized
fun clear(context: Context) {
username = null

View File

@ -2,7 +2,6 @@ package ac.mdiq.podcini.net.sync
import ac.mdiq.podcini.R
enum class SynchronizationProviderViewData(@JvmField val identifier: String, val summaryResource: Int, val iconResource: Int) {
GPODDER_NET(
"GPODDER_NET",

View File

@ -7,6 +7,7 @@ import android.content.SharedPreferences
object SynchronizationSettings {
const val LAST_SYNC_ATTEMPT_TIMESTAMP: String = "last_sync_attempt_timestamp"
private const val NAME = "synchronization"
private const val WIFI_SYNC_ENABLED = "wifi_sync_enabled"
private const val SELECTED_SYNC_PROVIDER = "selected_sync_provider"
private const val LAST_SYNC_ATTEMPT_SUCCESS = "last_sync_attempt_success"
private const val LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP = "last_episode_actions_sync_timestamp"
@ -39,6 +40,16 @@ object SynchronizationSettings {
val selectedSyncProviderKey: String?
get() = sharedPreferences.getString(SELECTED_SYNC_PROVIDER, null)
fun setWifiSyncEnabled(stat: Boolean) {
sharedPreferences
.edit()
.putBoolean(WIFI_SYNC_ENABLED, stat)
.apply()
}
val wifiSyncEnabledKey: Boolean
get() = sharedPreferences.getBoolean(WIFI_SYNC_ENABLED, false)
fun updateLastSynchronizationAttempt() {
sharedPreferences.edit().putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply()
}

View File

@ -1,16 +1,16 @@
package ac.mdiq.podcini.net.sync.gpoddernet
import ac.mdiq.podcini.BuildConfig
import android.util.Log
import ac.mdiq.podcini.net.sync.HostnameParser
import ac.mdiq.podcini.net.sync.gpoddernet.mapper.ResponseMapper.readEpisodeActionsFromJsonObject
import ac.mdiq.podcini.net.sync.gpoddernet.mapper.ResponseMapper.readSubscriptionChangesFromJsonObject
import ac.mdiq.podcini.net.sync.ResponseMapper.readEpisodeActionsFromJsonObject
import ac.mdiq.podcini.net.sync.ResponseMapper.readSubscriptionChangesFromJsonObject
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetDevice
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetDevice.DeviceType
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetEpisodeActionPostResponse
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetPodcast
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetUploadChangesResponse
import ac.mdiq.podcini.net.sync.model.*
import android.util.Log
import okhttp3.*
import okhttp3.Credentials.basic
import okhttp3.MediaType.Companion.toMediaType
@ -56,10 +56,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
*/
@Throws(GpodnetServiceException::class)
fun searchPodcasts(query: String?, scaledLogoSize: Int): List<GpodnetPodcast> {
val parameters = if ((scaledLogoSize in 1..256)) String.format(Locale.US,
"q=%s&scale_logo=%d",
query,
scaledLogoSize) else String.format("q=%s", query)
val parameters = if ((scaledLogoSize in 1..256)) String.format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) else String.format("q=%s", query)
try {
val url = URI(baseScheme, null, baseHost, basePort, "/search.json", parameters, null).toURL()
val request: Builder = Builder().url(url)
@ -122,21 +119,15 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
fun configureDevice(deviceId: String, caption: String?, type: DeviceType?) {
requireLoggedIn()
try {
val url = URI(baseScheme, null, baseHost, basePort,
String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL()
val url = URI(baseScheme, null, baseHost, basePort, String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL()
val content: String
if (caption != null || type != null) {
val jsonContent = JSONObject()
if (caption != null) {
jsonContent.put("caption", caption)
}
if (type != null) {
jsonContent.put("type", type.toString())
}
if (caption != null) jsonContent.put("caption", caption)
if (type != null) jsonContent.put("type", type.toString())
content = jsonContent.toString()
} else {
content = ""
}
} else content = ""
val body = RequestBody.create(JSON, content)
val request: Builder = Builder().post(body).url(url)
executeRequest(request)
@ -168,8 +159,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
fun uploadSubscriptions(deviceId: String, subscriptions: List<String?>) {
requireLoggedIn()
try {
val url = URI(baseScheme, null, baseHost, basePort,
String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL()
val url = URI(baseScheme, null, baseHost, basePort, String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL()
val builder = StringBuilder()
for (s in subscriptions) {
builder.append(s)
@ -201,11 +191,10 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
* is an authentication error.
*/
@Throws(GpodnetServiceException::class)
override fun uploadSubscriptionChanges(added: List<String?>?, removed: List<String?>?): UploadChangesResponse {
override fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse {
requireLoggedIn()
try {
val url = URI(baseScheme, null, baseHost, basePort,
String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL()
val url = URI(baseScheme, null, baseHost, basePort, String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL()
val requestObject = JSONObject()
requestObject.put("add", JSONArray(added))
@ -275,24 +264,19 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
* is an authentication error.
*/
@Throws(SyncServiceException::class)
override fun uploadEpisodeActions(episodeActions: List<EpisodeAction?>?): UploadChangesResponse? {
override fun uploadEpisodeActions(episodeActions: List<EpisodeAction>): UploadChangesResponse? {
requireLoggedIn()
var response: UploadChangesResponse? = null
var i = 0
while (i < episodeActions!!.size) {
response = uploadEpisodeActionsPartial(episodeActions,
i, min(episodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble())
.toInt())
while (i < episodeActions.size) {
response = uploadEpisodeActionsPartial(episodeActions, i, min(episodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble()).toInt())
i += UPLOAD_BULK_SIZE
}
return response
}
@Throws(SyncServiceException::class)
private fun uploadEpisodeActionsPartial(episodeActions: List<EpisodeAction?>?,
from: Int,
to: Int
): UploadChangesResponse {
private fun uploadEpisodeActionsPartial(episodeActions: List<EpisodeAction?>?, from: Int, to: Int): UploadChangesResponse {
try {
Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions!!.size)
val url = URI(baseScheme, null, baseHost, basePort,
@ -369,8 +353,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
override fun login() {
val url: URL
try {
url = URI(baseScheme, null, baseHost, basePort,
String.format("/api/2/auth/%s/login.json", username), null, null).toURL()
url = URI(baseScheme, null, baseHost, basePort, String.format("/api/2/auth/%s/login.json", username), null, null).toURL()
} catch (e: MalformedURLException) {
e.printStackTrace()
throw GpodnetServiceException(e)
@ -417,11 +400,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
private fun getStringFromResponseBody(body: ResponseBody): String {
val outputStream: ByteArrayOutputStream
val contentLength = body.contentLength().toInt()
outputStream = if (contentLength > 0) {
ByteArrayOutputStream(contentLength)
} else {
ByteArrayOutputStream()
}
outputStream = if (contentLength > 0) ByteArrayOutputStream(contentLength) else ByteArrayOutputStream()
try {
val buffer = ByteArray(8 * 1024)
val inVal = body.byteStream()
@ -451,11 +430,9 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
}
}
if (responseCode >= 500) {
throw GpodnetServiceBadStatusCodeException("Gpodder.net is currently unavailable (code "
+ responseCode + ")", responseCode)
throw GpodnetServiceBadStatusCodeException("Gpodder.net is currently unavailable (code " + responseCode + ")", responseCode)
} else {
throw GpodnetServiceBadStatusCodeException("Unable to connect to Gpodder.net (code "
+ responseCode + ": " + response.message + ")", responseCode)
throw GpodnetServiceBadStatusCodeException("Unable to connect to Gpodder.net (code " + responseCode + ": " + response.message + ")", responseCode)
}
}
}
@ -476,19 +453,11 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
val title: String
val titleObj = `object`.opt("title")
title = if (titleObj is String) {
titleObj
} else {
url
}
title = if (titleObj is String) titleObj else url
val description: String
val descriptionObj = `object`.opt("description")
description = if (descriptionObj is String) {
descriptionObj
} else {
""
}
description = if (descriptionObj is String) descriptionObj else ""
val subscribers = `object`.getInt("subscribers")
@ -496,23 +465,17 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
var logoUrl = if ((logoUrlObj is String)) logoUrlObj else null
if (logoUrl == null) {
val scaledLogoUrl = `object`.opt("scaled_logo_url")
if (scaledLogoUrl is String) {
logoUrl = scaledLogoUrl
}
if (scaledLogoUrl is String) logoUrl = scaledLogoUrl
}
var website: String? = null
val websiteObj = `object`.opt("website")
if (websiteObj is String) {
website = websiteObj
}
if (websiteObj is String) website = websiteObj
val mygpoLink = `object`.getString("mygpo_link")
var author: String? = null
val authorObj = `object`.opt("author")
if (authorObj is String) {
author = authorObj
}
if (authorObj is String) author = authorObj
return GpodnetPodcast(url, title, description, subscribers, logoUrl!!, website!!, mygpoLink, author!!)
}
@ -534,8 +497,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
return GpodnetDevice(id, caption, type, subscriptions)
}
override fun logout() {
}
override fun logout() {}
fun setCredentials(username: String, password: String) {
this.username = username

View File

@ -28,14 +28,15 @@ class GpodnetEpisodeActionPostResponse private constructor(timestamp: Long, priv
@JvmStatic
@Throws(JSONException::class)
fun fromJSONObject(objectString: String?): GpodnetEpisodeActionPostResponse {
val `object` = JSONObject(objectString)
val timestamp = `object`.getLong("timestamp")
val urls = `object`.getJSONArray("update_urls")
val updatedUrls: MutableMap<String, String> = ArrayMap(urls.length())
for (i in 0 until urls.length()) {
val urlPair = urls.getJSONArray(i)
updatedUrls[urlPair.getString(0)] = urlPair.getString(1)
}
// val `object` = JSONObject(objectString)
// val timestamp = `object`.getLong("timestamp")
// val urls = `object`.getJSONArray("update_urls")
// val updatedUrls: MutableMap<String, String> = ArrayMap(urls.length())
// for (i in 0 until urls.length()) {
// val urlPair = urls.getJSONArray(i)
// updatedUrls[urlPair.getString(0)] = urlPair.getString(1)
// }
val (timestamp, updatedUrls) = UploadChangesResponse.fromJSONObject(objectString)
return GpodnetEpisodeActionPostResponse(timestamp, updatedUrls)
}
}

View File

@ -28,14 +28,15 @@ class GpodnetUploadChangesResponse(timestamp: Long, val updatedUrls: Map<String,
@JvmStatic
@Throws(JSONException::class)
fun fromJSONObject(objectString: String?): GpodnetUploadChangesResponse {
val `object` = JSONObject(objectString)
val timestamp = `object`.getLong("timestamp")
val updatedUrls: MutableMap<String, String> = ArrayMap()
val urls = `object`.getJSONArray("update_urls")
for (i in 0 until urls.length()) {
val urlPair = urls.getJSONArray(i)
updatedUrls[urlPair.getString(0)] = urlPair.getString(1)
}
// val `object` = JSONObject(objectString)
// val timestamp = `object`.getLong("timestamp")
// val updatedUrls: MutableMap<String, String> = ArrayMap()
// val urls = `object`.getJSONArray("update_urls")
// for (i in 0 until urls.length()) {
// val urlPair = urls.getJSONArray(i)
// updatedUrls[urlPair.getString(0)] = urlPair.getString(1)
// }
val (timestamp, updatedUrls) = UploadChangesResponse.fromJSONObject(objectString)
return GpodnetUploadChangesResponse(timestamp, updatedUrls)
}
}

View File

@ -10,13 +10,13 @@ interface ISyncService {
fun getSubscriptionChanges(lastSync: Long): SubscriptionChanges?
@Throws(SyncServiceException::class)
fun uploadSubscriptionChanges(addedFeeds: List<String?>?, removedFeeds: List<String?>?): UploadChangesResponse?
fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse?
@Throws(SyncServiceException::class)
fun getEpisodeActionChanges(lastSync: Long): EpisodeActionChanges?
@Throws(SyncServiceException::class)
fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction?>?): UploadChangesResponse?
fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction>): UploadChangesResponse?
@Throws(SyncServiceException::class)
fun logout()

View File

@ -1,9 +1,7 @@
package ac.mdiq.podcini.net.sync.model
class SubscriptionChanges(val added: List<String>,
val removed: List<String>,
val timestamp: Long
) {
class SubscriptionChanges(val added: List<String>, val removed: List<String>, val timestamp: Long) {
override fun toString(): String {
return ("SubscriptionChange [added=$added, removed=$removed, timestamp=$timestamp]")
}

View File

@ -1,6 +1,25 @@
package ac.mdiq.podcini.net.sync.model
import androidx.collection.ArrayMap
import org.json.JSONObject
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
abstract class UploadChangesResponse(@JvmField val timestamp: Long)
abstract class UploadChangesResponse(@JvmField val timestamp: Long) {
companion object {
fun fromJSONObject(objectString: String?): Pair<Long, Map<String, String>> {
val `object` = JSONObject(objectString)
val timestamp = `object`.getLong("timestamp")
val updatedUrls: MutableMap<String, String> = ArrayMap()
val urls = `object`.getJSONArray("update_urls")
for (i in 0 until urls.length()) {
val urlPair = urls.getJSONArray(i)
updatedUrls[urlPair.getString(0)] = urlPair.getString(1)
}
return Pair(timestamp, updatedUrls)
}
}
}

View File

@ -43,8 +43,7 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient, private val rawHo
return
}
startDisposable = Observable.fromCallable {
val url = URI(hostname.scheme, null, hostname.host, hostname.port,
hostname.subfolder + "/index.php/login/v2", null, null).toURL()
val url = URI(hostname.scheme, null, hostname.host, hostname.port, hostname.subfolder + "/index.php/login/v2", null, null).toURL()
val result = doRequest(url, "")
val loginUrl = result.getString("login")
this.token = result.getJSONObject("poll").getString("token")
@ -102,7 +101,6 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient, private val rawHo
interface AuthenticationCallback {
fun onNextcloudAuthenticated(server: String, username: String, password: String)
fun onNextcloudAuthError(errorMessage: String?)
}

View File

@ -1,7 +1,7 @@
package ac.mdiq.podcini.net.sync.nextcloud
import ac.mdiq.podcini.net.sync.HostnameParser
import ac.mdiq.podcini.net.sync.gpoddernet.mapper.ResponseMapper
import ac.mdiq.podcini.net.sync.ResponseMapper
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetUploadChangesResponse
import ac.mdiq.podcini.net.sync.model.*
import okhttp3.*
@ -41,14 +41,12 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
}
@Throws(NextcloudSynchronizationServiceException::class)
override fun uploadSubscriptionChanges(addedFeeds: List<String?>?,
removedFeeds: List<String?>?
): UploadChangesResponse {
override fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse {
try {
val url: HttpUrl.Builder = makeUrl("/index.php/apps/gpoddersync/subscription_change/create")
val requestObject = JSONObject()
requestObject.put("add", JSONArray(addedFeeds))
requestObject.put("remove", JSONArray(removedFeeds))
requestObject.put("add", JSONArray(added))
requestObject.put("remove", JSONArray(removed))
val requestBody = RequestBody.create("application/json".toMediaType(), requestObject.toString())
performRequest(url, "POST", requestBody)
} catch (e: Exception) {
@ -80,9 +78,9 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
}
@Throws(NextcloudSynchronizationServiceException::class)
override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction?>?): UploadChangesResponse {
override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction>): UploadChangesResponse {
var i = 0
while (i < queuedEpisodeActions!!.size) {
while (i < queuedEpisodeActions.size) {
uploadEpisodeActionsPartial(queuedEpisodeActions, i, min(queuedEpisodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble()).toInt())
i += UPLOAD_BULK_SIZE
}

View File

@ -3,6 +3,9 @@ package ac.mdiq.podcini.net.sync.queue
import android.content.Context
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
import ac.mdiq.podcini.net.sync.SynchronizationSettings.lastSyncAttempt
import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.net.sync.model.EpisodeAction
@ -19,7 +22,7 @@ object SynchronizationQueueSink {
}
fun syncNowIfNotSyncedRecently() {
if (System.currentTimeMillis() - SynchronizationSettings.lastSyncAttempt > 1000 * 60 * 10) syncNow()
if (System.currentTimeMillis() - lastSyncAttempt > 1000 * 60 * 10) syncNow()
}
@JvmStatic
@ -28,7 +31,7 @@ object SynchronizationQueueSink {
}
fun enqueueFeedAddedIfSynchronizationIsActive(context: Context, downloadUrl: String) {
if (!SynchronizationSettings.isProviderConnected) return
if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl)
@ -37,7 +40,7 @@ object SynchronizationQueueSink {
}
fun enqueueFeedRemovedIfSynchronizationIsActive(context: Context, downloadUrl: String) {
if (!SynchronizationSettings.isProviderConnected) return
if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl)
@ -46,7 +49,7 @@ object SynchronizationQueueSink {
}
fun enqueueEpisodeActionIfSynchronizationIsActive(context: Context, action: EpisodeAction) {
if (!SynchronizationSettings.isProviderConnected) return
if (!isProviderConnected) return
LockingAsyncExecutor.executeLockedAsync {
SynchronizationQueueStorage(context).enqueueEpisodeAction(action)
@ -55,7 +58,7 @@ object SynchronizationQueueSink {
}
fun enqueueEpisodePlayedIfSynchronizationIsActive(context: Context, media: FeedMedia, completed: Boolean) {
if (!SynchronizationSettings.isProviderConnected) return
if (!isProviderConnected) return
if (media.item?.feed == null || media.item!!.feed!!.isLocalFeed) return
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return

View File

@ -0,0 +1,383 @@
package ac.mdiq.podcini.net.sync.wifi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor.executeLockedAsync
import ac.mdiq.podcini.net.sync.SyncService
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.model.*
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
import ac.mdiq.podcini.storage.DBReader.getEpisodes
import ac.mdiq.podcini.storage.DBReader.getFeedItemByGuidOrEpisodeUrl
import ac.mdiq.podcini.storage.DBReader.getFeedMedia
import ac.mdiq.podcini.storage.DBWriter.persistFeedMediaPlaybackInfo
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.storage.model.feed.SortOrder
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
import ac.mdiq.podcini.util.event.SyncServiceEvent
import android.content.Context
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat.getString
import androidx.media3.common.util.UnstableApi
import androidx.work.*
import org.apache.commons.lang3.StringUtils
import org.greenrobot.eventbus.EventBus
import org.json.JSONArray
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.*
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.min
@UnstableApi class WifiSyncService(val context: Context, params: WorkerParameters) : SyncService(context, params), ISyncService {
var loginFail = false
override fun doWork(): Result {
Log.d(TAG, "doWork() called")
SynchronizationSettings.updateLastSynchronizationAttempt()
setCurrentlyActive(true)
login()
if (socket != null && !loginFail) {
if (isGuest) {
Thread.sleep(1000)
// TODO: not using lastSync
val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp
val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis())
SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "50"))
sendToPeer("AllSent", "AllSent")
var receivedBye = false
while (!receivedBye) {
try {
receivedBye = receiveFromPeer()
} catch (e: SocketTimeoutException) {
Log.e("Guest", getString(context, R.string.sync_error_host_not_respond))
logout()
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100"))
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_host_not_respond)))
SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
return Result.failure()
}
}
} else {
var receivedBye = false
while (!receivedBye) {
try {
receivedBye = receiveFromPeer()
} catch (e: SocketTimeoutException) {
Log.e("Host", getString(context, R.string.sync_error_guest_not_respond))
logout()
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100"))
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_guest_not_respond)))
SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
return Result.failure()
}
}
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "50"))
// TODO: not using lastSync
val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp
val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis())
SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
sendToPeer("AllSent", "AllSent")
}
} else {
logout()
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100"))
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_error, "Login failure"))
SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
return Result.failure()
}
logout()
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "100"))
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_success))
SynchronizationSettings.setLastSynchronizationAttemptSuccess(true)
return Result.success()
}
private var socket: Socket? = null
@OptIn(UnstableApi::class) override fun login() {
Log.d(TAG, "serverIp: $hostIp serverPort: $hostPort $isGuest")
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "2"))
if (!isPortInUse(hostPort)) {
if (isGuest) {
val maxTries = 120
var numTries = 0
while (numTries < maxTries) {
try {
socket = Socket(hostIp, hostPort)
break
} catch (e: ConnectException) {
Thread.sleep(1000)
}
numTries++
}
if (numTries >= maxTries) loginFail = true
if (socket != null) {
sendToPeer("Hello", "Hello, Server!")
receiveFromPeer()
}
} else {
try {
if (serverSocket == null) serverSocket = ServerSocket(hostPort)
serverSocket!!.soTimeout = 120000
try {
socket = serverSocket!!.accept()
while (true) {
Log.d(TAG, "waiting for guest message")
try {
receiveFromPeer()
sendToPeer("Hello", "Hello, Client")
break
} catch (e: SocketTimeoutException) {
Log.e("Server", "Guest not responding in 120 seconds, giving up")
loginFail = true
break
}
}
} catch (e: Exception) {
Log.e("Server", "No guest connecing in 120 seconds, giving up")
loginFail = true
}
} catch (e: BindException) {
Log.e("Server", "Failed to start server: Port $hostPort already in use")
loginFail = true
}
}
} else {
Log.w(TAG, "port $hostPort in use, ignored")
loginFail = true
}
EventBus.getDefault().post(SyncServiceEvent(R.string.sync_status_in_progress, "5"))
}
@OptIn(UnstableApi::class) private fun isPortInUse(port: Int): Boolean {
val command = "netstat -tlnp"
val process = Runtime.getRuntime().exec(command)
val output = process.inputStream.bufferedReader().use { it.readText() }
// Log.d(TAG, "isPortInUse: $output")
return output.contains(":$port") // Check if output contains the port
}
private fun sendToPeer(messageType: String, message: String) {
val writer = PrintWriter(socket!!.getOutputStream(), true)
writer.println("$messageType|$message")
}
@Throws(SocketTimeoutException::class)
private fun receiveFromPeer() : Boolean {
val reader = BufferedReader(InputStreamReader(socket!!.getInputStream()))
val message: String?
socket!!.soTimeout = 120000
try {
message = reader.readLine()
} catch (e: SocketTimeoutException) {
throw e
}
if (message != null) {
val parts = message.split("|")
if (parts.size == 2) {
val messageType = parts[0]
val messageData = parts[1]
// Process the message based on the type
when (messageType) {
"Hello" -> Log.d(TAG, "Received Hello message: $messageData")
"EpisodeActions" -> {
val remoteActions = mutableListOf<EpisodeAction>()
val jsonArray = JSONArray(messageData)
for (i in 0 until jsonArray.length()) {
val jsonAction = jsonArray.getJSONObject(i)
// TODO: this conversion shouldn't be needed, check about the uploader
// val timeStr = jsonAction.getString("timestamp")
// val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
// val date = format.parse(timeStr)
// jsonAction.put("timestamp", date?.time?:0L)
Log.d(TAG, "Received EpisodeActions message: $i $jsonAction")
val action = readFromJsonObject(jsonAction)
if (action != null) remoteActions.add(action)
}
processEpisodeActions(remoteActions)
}
"AllSent" -> {
Log.d(TAG, "Received AllSent message: $messageData")
return true
}
else -> Log.d(TAG, "Received unknown message: $messageData")
}
}
}
return false
}
@Throws(SyncServiceException::class)
override fun getSubscriptionChanges(lastSync: Long): SubscriptionChanges? {
Log.d(TAG, "getSubscriptionChanges does nothing")
return null
}
@Throws(WifiSynchronizationServiceException::class)
override fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse? {
Log.d(TAG, "uploadSubscriptionChanges does nothing")
return null
}
@Throws(SyncServiceException::class)
override fun getEpisodeActionChanges(timestamp: Long): EpisodeActionChanges? {
Log.d(TAG, "getEpisodeActionChanges does nothing")
return null
}
override fun pushEpisodeActions(syncServiceImpl: ISyncService, lastSync: Long, newTimeStamp_: Long): Long {
var newTimeStamp = newTimeStamp_
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_episodes_upload))
val queuedEpisodeActions: MutableList<EpisodeAction> = synchronizationQueueStorage.queuedEpisodeActions
Log.d(TAG, "pushEpisodeActions queuedEpisodeActions: ${queuedEpisodeActions.size}")
if (lastSync == 0L) {
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_upload_played))
// only push downloaded items
val pausedItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PAUSED), SortOrder.DATE_NEW_OLD)
val readItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD)
val comItems = mutableSetOf<FeedItem>()
comItems.addAll(pausedItems)
comItems.addAll(readItems)
Log.d(TAG, "First sync. Upload state for all " + comItems.size + " played episodes")
for (item in comItems) {
val media = item.media ?: continue
val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
.timestamp(Date(media.getLastPlayedTime()))
.started(media.getPosition() / 1000)
.position(media.getPosition() / 1000)
.total(media.getDuration() / 1000)
.build()
queuedEpisodeActions.add(played)
}
}
if (queuedEpisodeActions.isNotEmpty()) {
LockingAsyncExecutor.lock.lock()
try {
Log.d(TAG, "Uploading ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
val postResponse = uploadEpisodeActions(queuedEpisodeActions)
newTimeStamp = postResponse.timestamp
Log.d(TAG, "Upload episode response: $postResponse")
synchronizationQueueStorage.clearEpisodeActionQueue()
} finally {
LockingAsyncExecutor.lock.unlock()
}
}
return newTimeStamp
}
@Throws(WifiSynchronizationServiceException::class)
override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction>): UploadChangesResponse {
// Log.d(TAG, "uploadEpisodeActions called")
var i = 0
while (i < queuedEpisodeActions.size) {
uploadEpisodeActionsPartial(queuedEpisodeActions, i, min(queuedEpisodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble()).toInt())
i += UPLOAD_BULK_SIZE
Thread.sleep(1000)
}
return WifiEpisodeActionPostResponse(System.currentTimeMillis() / 1000)
}
@Throws(WifiSynchronizationServiceException::class)
private fun uploadEpisodeActionsPartial(queuedEpisodeActions: List<EpisodeAction>, from: Int, to: Int) {
// Log.d(TAG, "uploadEpisodeActionsPartial called")
try {
val list = JSONArray()
for (i in from until to) {
val episodeAction = queuedEpisodeActions[i]
val obj = episodeAction.writeToJsonObject()
if (obj != null) {
Log.d(TAG, "sending EpisodeAction: $obj")
list.put(obj)
}
}
sendToPeer("EpisodeActions", list.toString())
} catch (e: Exception) {
e.printStackTrace()
throw WifiSynchronizationServiceException(e)
}
}
override fun processEpisodeAction(action: EpisodeAction): Pair<Long, FeedItem>? {
val guid = if (isValidGuid(action.guid)) action.guid else null
val feedItem = getFeedItemByGuidOrEpisodeUrl(guid, action.episode?:"")
if (feedItem == null) {
Log.i(TAG, "Unknown feed item: $action")
return null
}
if (feedItem.media == null) {
Log.i(TAG, "Feed item has no media: $action")
return null
}
feedItem.media = getFeedMedia(feedItem.media!!.id)
var idRemove = 0L
Log.d(TAG, "processEpisodeAction ${feedItem.media!!.getLastPlayedTime()} ${(action.timestamp?.time?:0L)} ${action.position} ${feedItem.title}")
if (feedItem.media!!.getLastPlayedTime() < (action.timestamp?.time?:0L)) {
feedItem.media!!.setPosition(action.position * 1000)
feedItem.media!!.setLastPlayedTime(action.timestamp!!.time)
if (hasAlmostEnded(feedItem.media!!)) {
Log.d(TAG, "Marking as played")
feedItem.setPlayed(true)
feedItem.media!!.setPosition(0)
idRemove = feedItem.id
} else Log.d(TAG, "Setting position")
persistFeedMediaPlaybackInfo(feedItem.media)
} else Log.d(TAG, "local is newer, no change")
return Pair(idRemove, feedItem)
}
override fun logout() {
socket?.close()
}
private class WifiEpisodeActionPostResponse(epochSecond: Long) : UploadChangesResponse(epochSecond)
companion object {
const val TAG: String = "WifiSyncService"
private const val WORK_ID_SYNC = "SyncServiceWorkId"
private const val UPLOAD_BULK_SIZE = 30
var serverSocket: ServerSocket? = null
var isGuest = false
var hostIp : String = ""
var hostPort: Int = 54628
private var isCurrentlyActive = false
internal fun setCurrentlyActive(active: Boolean) {
isCurrentlyActive = active
}
fun startInstantSync(context: Context, hostPort_: Int = 54628, hostIp_: String="", isGuest_: Boolean = false) {
hostIp = hostIp_
isGuest = isGuest_
hostPort = hostPort_
executeLockedAsync {
SynchronizationSettings.resetTimestamps()
val builder: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(WifiSyncService::class.java)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES)
// Give it some time, so other possible actions can be queued.
builder.setInitialDelay(20L, TimeUnit.SECONDS)
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_started))
val workRequest: OneTimeWorkRequest = builder.setInitialDelay(0L, TimeUnit.SECONDS).build()
WorkManager.getInstance(context).enqueueUniqueWork(hostIp_, ExistingWorkPolicy.REPLACE, workRequest)
}
}
}
}

View File

@ -0,0 +1,5 @@
package ac.mdiq.podcini.net.sync.wifi
import ac.mdiq.podcini.net.sync.model.SyncServiceException
class WifiSynchronizationServiceException(e: Throwable?) : SyncServiceException(e)

View File

@ -1,34 +0,0 @@
package ac.mdiq.podcini.playback
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.Playable
/**
* Provides utility methods for Playable objects.
*/
object PlayableUtils {
/**
* Saves the current position of this object.
*
* @param newPosition new playback position in ms
* @param timestamp current time in ms
*/
@UnstableApi @JvmStatic
fun saveCurrentPosition(playable: Playable, newPosition: Int, timestamp: Long) {
playable.setPosition(newPosition)
playable.setLastPlayedTime(timestamp)
if (playable is FeedMedia) {
val item = playable.item
if (item != null && item.isNew) DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.id)
if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition)
playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition)
DBWriter.persistFeedMediaPlaybackInformation(playable)
}
}
}

View File

@ -1,19 +1,21 @@
package ac.mdiq.podcini.playback
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils.getCurrentPlaybackSpeed
import ac.mdiq.podcini.preferences.PlaybackPreferences
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.LocalBinder
import ac.mdiq.podcini.playback.service.PlaybackServiceConstants
import ac.mdiq.podcini.preferences.PlaybackPreferences
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import ac.mdiq.podcini.util.event.playback.PlaybackServiceEvent
import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent
import android.content.*
import android.os.Build
import android.os.IBinder
@ -33,11 +35,6 @@ import org.greenrobot.eventbus.ThreadMode
@UnstableApi
abstract class PlaybackController(private val activity: FragmentActivity) {
private var playbackService: PlaybackService? = null
var status: PlayerStatus = PlayerStatus.STOPPED
private set
private var mediaInfoLoaded = false
private var released = false
private var initialized = false
@ -45,6 +42,22 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
private var loadedFeedMedia: Long = -1
val position: Int
get() = playbackService?.currentPosition ?: getMedia()?.getPosition() ?: Playable.INVALID_TIME
val duration: Int
get() = playbackService?.duration ?: getMedia()?.getDuration() ?: Playable.INVALID_TIME
val currentPlaybackSpeedMultiplier: Float
get() = playbackService?.currentPlaybackSpeed ?: getCurrentPlaybackSpeed(getMedia())
val isPlayingVideoLocally: Boolean
get() = when {
PlaybackService.isCasting -> false
playbackService != null -> PlaybackService.currentMediaType == MediaType.VIDEO
else -> getMedia()?.getMediaType() == MediaType.VIDEO
}
/**
* Creates a new connection to the playbackService.
*/
@ -87,7 +100,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
* example in the activity's onStop() method.
*/
fun release() {
Log.d(TAG, "Releasing PlaybackController")
Logd(TAG, "Releasing PlaybackController")
try {
activity.unregisterReceiver(statusUpdate)
@ -110,10 +123,6 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
}
}
fun isPlaybackServiceReady() : Boolean {
return playbackService != null && playbackService!!.isServiceReady()
}
private fun unbind() {
try {
activity.unbindService(mConnection)
@ -127,7 +136,8 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
* Should be called in the activity's onPause() method.
*/
fun pause() {
mediaInfoLoaded = false
// TODO: why set it to false
// mediaInfoLoaded = false
}
/**
@ -136,10 +146,10 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
* as the arguments of the launch intent.
*/
private fun bindToService() {
Log.d(TAG, "Trying to connect to service")
Logd(TAG, "Trying to connect to service")
check(PlaybackService.isRunning) { "Trying to bind but service is not running" }
val bound = activity.bindService(Intent(activity, PlaybackService::class.java), mConnection, 0)
Log.d(TAG, "Result for service binding: $bound")
Logd(TAG, "Result for service binding: $bound")
}
private val mConnection: ServiceConnection = object : ServiceConnection {
@ -149,7 +159,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
onPlaybackServiceConnected()
if (!released) {
queryService()
Log.d(TAG, "Connection to Service established")
Logd(TAG, "Connection to Service established")
} else Log.i(TAG, "Connection to playback service has been established, but controller has already been released")
}
}
@ -157,26 +167,27 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
override fun onServiceDisconnected(name: ComponentName) {
playbackService = null
initialized = false
Log.d(TAG, "Disconnected from Service")
Logd(TAG, "Disconnected from Service")
}
}
var prevStatus = PlayerStatus.STOPPED
private val statusUpdate: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d(TAG, "Received statusUpdate Intent.")
if (playbackService != null) {
val info = playbackService!!.pSMPInfo
if (status != info.playerStatus || media != info.playable) {
status = info.playerStatus
val info = playbackService!!.mPlayerInfo
if (prevStatus != info.playerStatus || media == null || media!!.getIdentifier() != info.playable?.getIdentifier()) {
// Log.d(TAG, "statusUpdate onReceive $prevStatus ${MediaPlayerBase.status} ${info.playerStatus} ${media?.getIdentifier()} ${info.playable?.getIdentifier()}.")
MediaPlayerBase.status = info.playerStatus
prevStatus = MediaPlayerBase.status
media = info.playable
handleStatus()
}
} else {
Log.w(TAG, "Couldn't receive status update: playbackService was null")
if (PlaybackService.isRunning) {
bindToService()
} else {
status = PlayerStatus.STOPPED
Log.w(TAG, "statusUpdate onReceive: Couldn't receive status update: playbackService was null")
if (PlaybackService.isRunning) bindToService()
else {
MediaPlayerBase.status = PlayerStatus.STOPPED
handleStatus()
}
}
@ -188,7 +199,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
val type = intent.getIntExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_TYPE, -1)
val code = intent.getIntExtra(PlaybackServiceConstants.EXTRA_NOTIFICATION_CODE, -1)
if (code == -1 || type == -1) {
Log.d(TAG, "Bad arguments. Won't handle intent")
Logd(TAG, "Bad arguments. Won't handle intent")
return
}
when (type) {
@ -212,9 +223,9 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
* should be used to update the GUI or start/cancel background threads.
*/
private fun handleStatus() {
Log.d(TAG, "handleStatus() called status: $status")
// Log.d(TAG, "handleStatus() called status: ${MediaPlayerBase.status}")
checkMediaInfoLoaded()
when (status) {
when (MediaPlayerBase.status) {
PlayerStatus.PLAYING -> updatePlayButtonShowsPlay(false)
PlayerStatus.PREPARING -> updatePlayButtonShowsPlay(!(playbackService?.isStartWhenPrepared ?: false))
PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
@ -242,10 +253,10 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
* information has to be refreshed
*/
private fun queryService() {
Log.d(TAG, "Querying service info")
Logd(TAG, "Querying service info")
if (playbackService != null) {
val info = playbackService!!.pSMPInfo
status = info.playerStatus
val info = playbackService!!.mPlayerInfo
MediaPlayerBase.status = info.playerStatus
media = info.playable
// make sure that new media is loaded if it's available
mediaInfoLoaded = false
@ -270,7 +281,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
Log.w(TAG, "playbackservice was null, restarted!")
return
}
when (status) {
when (MediaPlayerBase.status) {
PlayerStatus.FALLBACK -> fallbackSpeed(1.0f)
PlayerStatus.PLAYING -> playbackService?.pause(abandonAudioFocus = true, reinit = false)
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> playbackService?.resume()
@ -286,39 +297,13 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
}
}
val position: Int
get() = playbackService?.currentPosition ?: getMedia()?.getPosition()?:Playable.INVALID_TIME
val duration: Int
get() = playbackService?.duration ?: getMedia()?.getDuration()?:Playable.INVALID_TIME
fun getMedia(): Playable? {
if (media == null && playbackService != null) media = playbackService!!.pSMPInfo.playable
if (media == null && playbackService != null) media = playbackService!!.mPlayerInfo.playable
if (media == null) media = PlaybackPreferences.createInstanceFromPreferences(activity)
return media
}
fun sleepTimerActive(): Boolean {
return playbackService?.sleepTimerActive() ?: false
}
fun disableSleepTimer() {
playbackService?.disableSleepTimer()
}
val sleepTimerTimeLeft: Long
get() = playbackService?.sleepTimerTimeLeft ?: Playable.INVALID_TIME.toLong()
fun extendSleepTimer(extendTime: Long) {
val timeLeft = sleepTimerTimeLeft
if (playbackService != null && timeLeft != Playable.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
}
fun setSleepTimer(time: Long) {
playbackService?.setSleepTimer(time)
}
fun seekTo(time: Int) {
if (playbackService != null) playbackService!!.seekTo(time)
else {
@ -333,80 +318,91 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
}
}
fun setVideoSurface(holder: SurfaceHolder?) {
playbackService?.setVideoSurface(holder)
}
fun setPlaybackSpeed(speed: Float, codeArray: BooleanArray? = null) {
if (playbackService != null) playbackService!!.setSpeed(speed, codeArray)
else {
UserPreferences.setPlaybackSpeed(speed)
EventBus.getDefault().post(SpeedChangedEvent(speed))
}
}
fun speedForward(speed: Float) {
playbackService?.speedForward(speed)
}
fun fallbackSpeed(speed: Float) {
if (playbackService != null) {
when (status) {
PlayerStatus.PLAYING -> {
status = PlayerStatus.FALLBACK
playbackService!!.fallbackSpeed(speed)
}
PlayerStatus.FALLBACK -> {
status = PlayerStatus.PLAYING
playbackService!!.fallbackSpeed(speed)
}
else -> {}
}
}
}
fun setSkipSilence(skipSilence: Boolean) {
playbackService?.skipSilence(skipSilence)
}
val currentPlaybackSpeedMultiplier: Float
get() = playbackService?.currentPlaybackSpeed ?: getCurrentPlaybackSpeed(getMedia())
val audioTracks: List<String>
get() {
if (playbackService?.audioTracks.isNullOrEmpty()) return emptyList()
return playbackService!!.audioTracks.filterNotNull().map { it }
}
val selectedAudioTrack: Int
get() {
return playbackService?.selectedAudioTrack?: -1
}
fun setAudioTrack(track: Int) {
playbackService?.setAudioTrack(track)
}
val isPlayingVideoLocally: Boolean
get() = when {
PlaybackService.isCasting -> false
playbackService != null -> PlaybackService.currentMediaType == MediaType.VIDEO
else -> getMedia()?.getMediaType() == MediaType.VIDEO
}
val videoSize: Pair<Int, Int>?
get() = playbackService?.videoSize
fun notifyVideoSurfaceAbandoned() {
playbackService?.notifyVideoSurfaceAbandoned()
}
val isStreaming: Boolean
get() = playbackService != null && playbackService!!.isStreaming
companion object {
private const val TAG = "PlaybackController"
private var playbackService: PlaybackService? = null
private var media: Playable? = null
val sleepTimerTimeLeft: Long
get() = playbackService?.sleepTimerTimeLeft ?: Playable.INVALID_TIME.toLong()
val audioTracks: List<String>
get() {
if (playbackService?.audioTracks.isNullOrEmpty()) return emptyList()
return playbackService!!.audioTracks.filterNotNull().map { it }
}
val selectedAudioTrack: Int
get() = playbackService?.selectedAudioTrack?: -1
val videoSize: Pair<Int, Int>?
get() = playbackService?.videoSize
fun isPlaybackServiceReady() : Boolean {
return playbackService != null && playbackService!!.isServiceReady()
}
fun speedForward(speed: Float) {
playbackService?.speedForward(speed)
}
fun fallbackSpeed(speed: Float) {
if (playbackService != null) {
when (MediaPlayerBase.status) {
PlayerStatus.PLAYING -> {
MediaPlayerBase.status = PlayerStatus.FALLBACK
playbackService!!.fallbackSpeed(speed)
}
PlayerStatus.FALLBACK -> {
MediaPlayerBase.status = PlayerStatus.PLAYING
playbackService!!.fallbackSpeed(speed)
}
else -> {}
}
}
}
fun setSkipSilence(skipSilence: Boolean) {
playbackService?.skipSilence(skipSilence)
}
fun setAudioTrack(track: Int) {
playbackService?.setAudioTrack(track)
}
fun notifyVideoSurfaceAbandoned() {
playbackService?.notifyVideoSurfaceAbandoned()
}
fun setVideoSurface(holder: SurfaceHolder?) {
playbackService?.setVideoSurface(holder)
}
fun setPlaybackSpeed(speed: Float, codeArray: BooleanArray? = null) {
if (playbackService != null) playbackService!!.setSpeed(speed, codeArray)
else {
UserPreferences.setPlaybackSpeed(speed)
EventBus.getDefault().post(SpeedChangedEvent(speed))
}
}
fun disableSleepTimer() {
playbackService?.disableSleepTimer()
}
fun extendSleepTimer(extendTime: Long) {
val timeLeft = sleepTimerTimeLeft
if (playbackService != null && timeLeft != Playable.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
}
fun setSleepTimer(time: Long) {
playbackService?.setSleepTimer(time)
}
fun sleepTimerActive(): Boolean {
return playbackService?.sleepTimerActive() ?: false
}
}
}

View File

@ -1,14 +1,14 @@
package ac.mdiq.podcini.playback.base
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.media.AudioManager
import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import kotlin.concurrent.Volatile
/*
@ -31,10 +31,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
* could result in nonsensical results (like a status of PLAYING, but a null playable)
* @return the current player status
*/
@get:Synchronized
@Volatile
var playerStatus: PlayerStatus
protected set
// @get:Synchronized
// @Volatile
// var playerStatus: PlayerStatus
// protected set
/**
* A wifi-lock that is acquired if the media file is being streamed.
@ -42,7 +42,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
private var wifiLock: WifiLock? = null
init {
playerStatus = PlayerStatus.STOPPED
status = PlayerStatus.STOPPED
}
abstract fun isStartWhenPrepared(): Boolean
@ -211,13 +211,13 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
*/
@get:Synchronized
val pSMPInfo: PSMPInfo
val pSMPInfo: MediaPlayerInfo
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
*
* @return The PSMPInfo object.
*/
get() = PSMPInfo(oldPlayerStatus, playerStatus, getPlayable())
get() = MediaPlayerInfo(oldPlayerStatus, status, getPlayable())
/**
* Returns the current media, if you need the media and the player status together, you should
@ -236,7 +236,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
fun skip() {
if (getPosition() < 1000) {
Log.d(TAG, "Ignoring skip, is in first second of playback")
Logd(TAG, "Ignoring skip, is in first second of playback")
return
}
endPlayback(hasEnded = false, wasSkipped = true, shouldContinue = true, toStoppedState = true)
@ -322,10 +322,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
*/
@Synchronized
protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int) {
Log.d(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
Logd(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
this.oldPlayerStatus = playerStatus
this.playerStatus = newStatus
this.oldPlayerStatus = status
status = newStatus
setPlayable(newMedia)
if (newMedia != null && newStatus != PlayerStatus.INDETERMINATE) {
@ -335,7 +335,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
}
callback.statusChanged(PSMPInfo(oldPlayerStatus, playerStatus, getPlayable()))
callback.statusChanged(MediaPlayerInfo(oldPlayerStatus, status, getPlayable()))
}
val isAudioChannelInUse: Boolean
@ -352,7 +352,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
interface PSMPCallback {
fun statusChanged(newInfo: PSMPInfo?)
fun statusChanged(newInfo: MediaPlayerInfo?)
fun shouldStop()
@ -376,9 +376,15 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
/**
* Holds information about a PSMP object.
*/
class PSMPInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
companion object {
private const val TAG = "MediaPlayerBase"
@get:Synchronized
@Volatile
@JvmStatic
var status: PlayerStatus = PlayerStatus.STOPPED
// protected set
}
}

View File

@ -11,6 +11,7 @@ import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.model.feed.FeedMedia
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podcini.util.config.ClientConfig
import ac.mdiq.podcini.util.event.PlayerErrorEvent
@ -19,19 +20,13 @@ import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.media.audiofx.LoudnessEnhancer
import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import androidx.core.util.Consumer
import androidx.media.AudioAttributesCompat
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
import androidx.media3.common.*
import androidx.media3.common.Player.*
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
@ -70,7 +65,7 @@ import kotlin.concurrent.Volatile
@UnstableApi
class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBase(context, callback) {
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
// private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
@Volatile
private var statusBeforeSeeking: PlayerStatus? = null
@ -85,13 +80,13 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
private var mediaType: MediaType
private val startWhenPrepared = AtomicBoolean(false)
@Volatile
private var pausedBecauseOfTransientAudiofocusLoss = false
// @Volatile
// private var pausedBecauseOfTransientAudiofocusLoss = false
@Volatile
private var videoSize: Pair<Int, Int>? = null
private val audioFocusRequest: AudioFocusRequestCompat
private val audioFocusCanceller = Handler(Looper.getMainLooper())
// private val audioFocusRequest: AudioFocusRequestCompat
// private val audioFocusCanceller = Handler(Looper.getMainLooper())
private var isShutDown = false
private var seekLatch: CountDownLatch? = null
@ -130,12 +125,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
return exoPlayer?.videoFormat?.height ?: 0
}
private fun setupExoPlayer() {
if (exoPlayer == null) {
createStaticPlayer(context)
}
}
@Throws(IllegalStateException::class)
private fun prepareWR() {
if (mediaSource == null) return
@ -147,6 +136,8 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
bufferingUpdateDisposable.dispose()
// exoplayerListener = null
exoPlayer?.stop()
exoPlayer?.seekTo(0L)
audioSeekCompleteListener = null
audioCompletionListener = null
audioErrorListener = null
@ -159,7 +150,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
b.setContentType(i)
b.setFlags(a.flags)
b.setUsage(a.usage)
exoPlayer?.setAudioAttributes(b.build(), false)
exoPlayer?.setAudioAttributes(b.build(), true)
}
private fun metadata(p: Playable): MediaMetadata {
@ -175,7 +166,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
@Throws(IllegalArgumentException::class, IllegalStateException::class)
private fun setDataSource(m: MediaMetadata, s: String, user: String?, password: String?) {
Log.d(TAG, "setDataSource: $s")
Logd(TAG, "setDataSource: $s")
val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
.setUserAgent(ClientConfig.USER_AGENT)
@ -236,7 +227,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
*/
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) {
Log.d(TAG, "playMediaObject(...)")
Logd(TAG, "playMediaObject(...)")
try {
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately)
} catch (e: RuntimeException) {
@ -255,19 +246,19 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* @see .playMediaObject
*/
private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) {
Log.d(TAG, "playMediaObject ${playable.getEpisodeTitle()} $forceReset $stream $startWhenPrepared $prepareImmediately")
Logd(TAG, "playMediaObject ${playable.getEpisodeTitle()} $forceReset $stream $startWhenPrepared $prepareImmediately")
if (this.playable != null) {
if (!forceReset && this.playable!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) {
if (!forceReset && this.playable!!.getIdentifier() == playable.getIdentifier() && status == PlayerStatus.PLAYING) {
// episode is already playing -> ignore method call
Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.")
Logd(TAG, "Method call to playMediaObject was ignored: media file already playing.")
return
} else {
// stop playback of this episode
if (playerStatus == PlayerStatus.PAUSED || (playerStatus == PlayerStatus.PLAYING) || playerStatus == PlayerStatus.PREPARED)
if (status == PlayerStatus.PAUSED || (status == PlayerStatus.PLAYING) || status == PlayerStatus.PREPARED)
exoPlayer?.stop()
// set temporarily to pause in order to update list with current position
if (playerStatus == PlayerStatus.PLAYING) callback.onPlaybackPause(this.playable, getPosition())
if (status == PlayerStatus.PLAYING) callback.onPlaybackPause(this.playable, getPosition())
if (this.playable!!.getIdentifier() != playable.getIdentifier()) {
val oldMedia: Playable = this.playable!!
callback.onPostPlayback(oldMedia, ended = false, skipped = false, true)
@ -331,26 +322,26 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* This method is executed on an internal executor service.
*/
override fun resume() {
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
val focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
// val focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.d(TAG, "Audiofocus successfully requested")
Log.d(TAG, "Resuming/Starting playback")
// if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Logd(TAG, "Audiofocus successfully requested")
Logd(TAG, "Resuming/Starting playback")
acquireWifiLockIfNecessary()
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(playable), UserPreferences.isSkipSilence)
setVolume(1.0f, 1.0f)
if (playable != null && playerStatus == PlayerStatus.PREPARED && playable!!.getPosition() > 0) {
if (playable != null && status == PlayerStatus.PREPARED && playable!!.getPosition() > 0) {
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(playable!!.getPosition(), playable!!.getLastPlayedTime())
seekTo(newPosition)
}
play()
setPlayerStatus(PlayerStatus.PLAYING, playable)
pausedBecauseOfTransientAudiofocusLoss = false
} else Log.e(TAG, "Failed to request audio focus")
} else Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is $playerStatus")
// pausedBecauseOfTransientAudiofocusLoss = false
// } else Log.e(TAG, "Failed to request audio focus")
} else Logd(TAG, "Call to resume() was ignored because current state of PSMP object is $status")
}
/**
@ -366,23 +357,23 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun pause(abandonFocus: Boolean, reinit: Boolean) {
releaseWifiLockIfNecessary()
if (playerStatus == PlayerStatus.PLAYING) {
Log.d(TAG, "Pausing playback.")
if (status == PlayerStatus.PLAYING) {
Logd(TAG, "Pausing playback.")
exoPlayer?.pause()
setPlayerStatus(PlayerStatus.PAUSED, playable, getPosition())
if (abandonFocus) {
abandonAudioFocus()
pausedBecauseOfTransientAudiofocusLoss = false
}
// if (abandonFocus) {
// abandonAudioFocus()
// pausedBecauseOfTransientAudiofocusLoss = false
// }
if (isStreaming && reinit) reinit()
} else {
Log.d(TAG, "Ignoring call to pause: Player is in $playerStatus state")
Logd(TAG, "Ignoring call to pause: Player is in $status state")
}
}
private fun abandonAudioFocus() {
AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest)
}
// private fun abandonAudioFocus() {
// AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest)
// }
/**
* Prepares media player for playback if the service is in the INITALIZED
@ -392,8 +383,8 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* This method is executed on an internal executor service.
*/
override fun prepare() {
if (playerStatus == PlayerStatus.INITIALIZED) {
Log.d(TAG, "Preparing media player")
if (status == PlayerStatus.INITIALIZED) {
Logd(TAG, "Preparing media player")
setPlayerStatus(PlayerStatus.PREPARING, playable)
prepareWR()
onPrepared(startWhenPrepared.get())
@ -404,14 +395,15 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* Called after media player has been prepared. This method is executed on the caller's thread.
*/
private fun onPrepared(startWhenPrepared: Boolean) {
check(playerStatus == PlayerStatus.PREPARING) { "Player is not in PREPARING state" }
Log.d(TAG, "Resource prepared")
// TODO: appears the check is not needed
// check(status == PlayerStatus.PREPARING) { "Player is not in PREPARING state" }
Logd(TAG, "Resource prepared")
if (mediaType == MediaType.VIDEO) videoSize = Pair(videoWidth, videoHeight)
if (playable != null) {
val pos = playable!!.getPosition()
if (pos > 0) seekTo(pos)
if (playable!!.getDuration() <= 0) {
Log.d(TAG, "Setting duration of media")
Logd(TAG, "Setting duration of media")
playable!!.setDuration(if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt())
}
}
@ -426,15 +418,13 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* This method is executed on an internal executor service.
*/
override fun reinit() {
Log.d(TAG, "reinit()")
Logd(TAG, "reinit()")
releaseWifiLockIfNecessary()
when {
playable != null -> playMediaObject(playable!!, true, isStreaming, startWhenPrepared.get(), false)
// TODO:
// playerWrapper != null -> playerWrapper!!.reset()
else -> {
setupExoPlayer() // TODO
Log.d(TAG, "Call to reinit: media and mediaPlayer were null")
// if (exoPlayer == null) createStaticPlayer(context)
Logd(TAG, "Call to reinit: media and mediaPlayer were null")
}
}
}
@ -451,14 +441,14 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
if (t < 0) t = 0
if (t >= getDuration()) {
Log.d(TAG, "Seek reached end of file, skipping to next episode")
Logd(TAG, "Seek reached end of file, skipping to next episode")
exoPlayer?.seekTo(t.toLong())
audioSeekCompleteListener?.run()
endPlayback(true, wasSkipped = true, true, toStoppedState = true)
// return
}
when (playerStatus) {
when (status) {
PlayerStatus.PLAYING, PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
if (seekLatch != null && seekLatch!!.count > 0) {
try {
@ -468,7 +458,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
}
}
seekLatch = CountDownLatch(1)
statusBeforeSeeking = playerStatus
statusBeforeSeeking = status
setPlayerStatus(PlayerStatus.SEEKING, playable, getPosition())
exoPlayer?.seekTo(t.toLong())
audioSeekCompleteListener?.run()
@ -504,9 +494,9 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun getDuration(): Int {
var retVal = Playable.INVALID_TIME
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
if (status == PlayerStatus.PLAYING || status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED)
retVal = if (exoPlayer?.duration == C.TIME_UNSET) Playable.INVALID_TIME else exoPlayer!!.duration.toInt()
}
if (retVal <= 0 && playable != null && playable!!.getDuration() > 0) retVal = playable!!.getDuration()
return retVal
}
@ -516,9 +506,11 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun getPosition(): Int {
var retVal = Playable.INVALID_TIME
if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
// Log.d(TAG, "getPosition() ${playable?.getIdentifier()} $status")
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
if (retVal <= 0 && playable != null && playable!!.getPosition() >= 0) retVal = playable!!.getPosition()
val playablePos = playable?.getPosition() ?: -1
if (retVal <= 0 && playablePos >= 0) retVal = playablePos
return retVal
}
@ -536,7 +528,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun setPlaybackParams(speed: Float, skipSilence: Boolean) {
EventBus.getDefault().post(SpeedChangedEvent(speed))
Log.d(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence")
Logd(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence")
playbackParameters = PlaybackParameters(speed, playbackParameters.pitch)
exoPlayer!!.skipSilenceEnabled = skipSilence
exoPlayer!!.playbackParameters = playbackParameters
@ -547,8 +539,8 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun getPlaybackSpeed(): Float {
var retVal = 1f
if (playerStatus == PlayerStatus.PLAYING|| playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED
|| playerStatus == PlayerStatus.PREPARED) retVal = playbackParameters.speed
if (status == PlayerStatus.PLAYING|| status == PlayerStatus.PAUSED || status == PlayerStatus.INITIALIZED
|| status == PlayerStatus.PREPARED) retVal = playbackParameters.speed
return retVal
}
@ -581,7 +573,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
loudnessEnhancer?.setEnabled(false)
}
Log.d(TAG, "Media player volume was set to $volumeLeft $volumeRight")
Logd(TAG, "Media player volume was set to $volumeLeft $volumeRight")
}
override fun getCurrentMediaType(): MediaType {
@ -604,10 +596,10 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
e.printStackTrace()
}
release()
playerStatus = PlayerStatus.STOPPED
status = PlayerStatus.STOPPED
isShutDown = true
abandonAudioFocus()
// abandonAudioFocus()
releaseWifiLockIfNecessary()
}
@ -617,7 +609,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
override fun resetVideoSurface() {
if (mediaType == MediaType.VIDEO) {
Log.d(TAG, "Resetting video surface")
Logd(TAG, "Resetting video surface")
exoPlayer?.setVideoSurfaceHolder(null)
reinit()
} else Log.e(TAG, "Resetting video surface for media of Audio type")
@ -631,7 +623,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
* invalid values.
*/
override fun getVideoSize(): Pair<Int, Int>? {
if (playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO)
if (status != PlayerStatus.ERROR && mediaType == MediaType.VIDEO)
videoSize = Pair(videoWidth, videoHeight)
return videoSize
}
@ -671,7 +663,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
override fun getSelectedAudioTrack(): Int {
val trackSelections = exoPlayer!!.currentTrackSelections
val availableFormats = formats
Log.d(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}")
Logd(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}")
for (i in 0 until trackSelections.length) {
val track = trackSelections[i] as? ExoTrackSelection ?: continue
if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat)
@ -682,69 +674,72 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
override fun createMediaPlayer() {
release()
if (playable == null) {
playerStatus = PlayerStatus.STOPPED
status = PlayerStatus.STOPPED
return
}
setAudioStreamType(AudioManager.STREAM_MUSIC)
setAudioStreamType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
setMediaPlayerListeners()
}
private val audioFocusChangeListener = OnAudioFocusChangeListener { focusChange ->
if (isShutDown) return@OnAudioFocusChangeListener
when {
!PlaybackService.isRunning -> {
abandonAudioFocus()
Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running")
return@OnAudioFocusChangeListener
}
focusChange == AudioManager.AUDIOFOCUS_LOSS -> {
Log.d(TAG, "Lost audio focus")
pause(true, reinit = false)
// callback.shouldStop()
}
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK && !UserPreferences.shouldPauseForFocusLoss() -> {
if (playerStatus == PlayerStatus.PLAYING) {
Log.d(TAG, "Lost audio focus temporarily. Ducking...")
setVolume(0.25f, 0.25f)
pausedBecauseOfTransientAudiofocusLoss = false
}
}
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
if (playerStatus == PlayerStatus.PLAYING) {
Log.d(TAG, "Lost audio focus temporarily. Pausing...")
exoPlayer?.pause() // Pause without telling the PlaybackService
pausedBecauseOfTransientAudiofocusLoss = true
audioFocusCanceller.removeCallbacksAndMessages(null)
// Still did not get back the audio focus. Now actually pause.
audioFocusCanceller.postDelayed({ if (pausedBecauseOfTransientAudiofocusLoss) pause(abandonFocus = true, reinit = false) },
30000)
}
}
focusChange == AudioManager.AUDIOFOCUS_GAIN -> {
Log.d(TAG, "Gained audio focus")
audioFocusCanceller.removeCallbacksAndMessages(null)
if (pausedBecauseOfTransientAudiofocusLoss) play() // we paused => play now
else setVolume(1.0f, 1.0f) // we ducked => raise audio level back
pausedBecauseOfTransientAudiofocusLoss = false
}
}
}
// private val audioFocusChangeListener = OnAudioFocusChangeListener { focusChange ->
// if (isShutDown) return@OnAudioFocusChangeListener
//
// when {
// !PlaybackService.isRunning -> {
// abandonAudioFocus()
// Log.d(TAG, "onAudioFocusChange: PlaybackService is no longer running")
// return@OnAudioFocusChangeListener
// }
// focusChange == AudioManager.AUDIOFOCUS_LOSS -> {
// Log.d(TAG, "Lost audio focus")
// pause(true, reinit = false)
//// callback.shouldStop()
// }
// focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK && !UserPreferences.shouldPauseForFocusLoss() -> {
// if (playerStatus == PlayerStatus.PLAYING) {
// Log.d(TAG, "Lost audio focus temporarily. Ducking...")
// setVolume(0.25f, 0.25f)
// pausedBecauseOfTransientAudiofocusLoss = false
// }
// }
// focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
// if (playerStatus == PlayerStatus.PLAYING) {
// Log.d(TAG, "Lost audio focus temporarily. Pausing...")
// exoPlayer?.pause() // Pause without telling the PlaybackService
// pausedBecauseOfTransientAudiofocusLoss = true
// audioFocusCanceller.removeCallbacksAndMessages(null)
// // Still did not get back the audio focus. Now actually pause.
// audioFocusCanceller.postDelayed({ if (pausedBecauseOfTransientAudiofocusLoss) pause(abandonFocus = true, reinit = false) },
// 30000)
// }
// }
// focusChange == AudioManager.AUDIOFOCUS_GAIN -> {
// Log.d(TAG, "Gained audio focus")
// audioFocusCanceller.removeCallbacksAndMessages(null)
// if (pausedBecauseOfTransientAudiofocusLoss) play() // we paused => play now
// else setVolume(1.0f, 1.0f) // we ducked => raise audio level back
// pausedBecauseOfTransientAudiofocusLoss = false
// }
// }
// }
init {
mediaType = MediaType.UNKNOWN
val audioAttributes = AudioAttributesCompat.Builder()
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
.build()
audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(audioFocusChangeListener)
.setWillPauseWhenDucked(true)
.build()
// val audioAttributes = AudioAttributesCompat.Builder()
// .setUsage(AudioAttributesCompat.USAGE_MEDIA)
// .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH)
// .build()
// audioFocusRequest = AudioFocusRequestCompat.Builder(AudioManagerCompat.AUDIOFOCUS_GAIN)
// .setAudioAttributes(audioAttributes)
// .setOnAudioFocusChangeListener(audioFocusChangeListener)
// .setWillPauseWhenDucked(true)
// .build()
setupExoPlayer()
if (exoPlayer == null) {
setupPlayerListener()
createStaticPlayer(context)
}
playbackParameters = exoPlayer!!.playbackParameters
bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
@ -756,15 +751,15 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) {
releaseWifiLockIfNecessary()
val isPlaying = playerStatus == PlayerStatus.PLAYING
val isPlaying = status == PlayerStatus.PLAYING
// we're relying on the position stored in the Playable object for post-playback processing
val position = getPosition()
if (position >= 0) playable?.setPosition(position)
setupExoPlayer()
abandonAudioFocus()
// if (exoPlayer == null) createStaticPlayer(context)
// abandonAudioFocus()
Log.d(TAG, "endPlayback $hasEnded $wasSkipped $shouldContinue $toStoppedState")
Logd(TAG, "endPlayback $hasEnded $wasSkipped $shouldContinue $toStoppedState")
// printStackTrace()
val currentMedia = playable
@ -775,7 +770,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
// Start playback immediately if continuous playback is enabled
nextMedia = callback.getNextInQueue(currentMedia)
if (nextMedia != null) {
Log.d(TAG, "has nextMedia. call callback.onPlaybackEnded false")
Logd(TAG, "has nextMedia. call callback.onPlaybackEnded false")
callback.onPlaybackEnded(nextMedia.getMediaType(), false)
// setting media to null signals to playMediaObject() that
// we're taking care of post-playback processing
@ -786,7 +781,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
when {
shouldContinue || toStoppedState -> {
if (nextMedia == null) {
Log.d(TAG, "nextMedia is null. call callback.onPlaybackEnded true")
Logd(TAG, "nextMedia is null. call callback.onPlaybackEnded true")
callback.onPlaybackEnded(null, true)
playable = null
exoPlayer?.stop()
@ -807,8 +802,8 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
private fun stop() {
releaseWifiLockIfNecessary()
if (playerStatus == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
else Log.d(TAG, "Ignored call to stop: Current player state is: $playerStatus")
if (status == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
else Logd(TAG, "Ignored call to stop: Current player state is: $status")
}
override fun shouldLockWifi(): Boolean {
@ -819,7 +814,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
if (playable == null) return
audioCompletionListener = Runnable {
Log.d(TAG, "audioCompletionListener called")
Logd(TAG, "audioCompletionListener called")
endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true)
}
audioSeekCompleteListener = Runnable { this.genericSeekCompleteListener() }
@ -844,17 +839,60 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
}
private fun genericSeekCompleteListener() {
Log.d(TAG, "genericSeekCompleteListener")
Logd(TAG, "genericSeekCompleteListener $status ${exoPlayer?.isPlaying} $statusBeforeSeeking")
seekLatch?.countDown()
if (playerStatus == PlayerStatus.PLAYING && playable != null) callback.onPlaybackStart(playable!!, getPosition())
if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) setPlayerStatus(statusBeforeSeeking!!, playable, getPosition())
if ((status == PlayerStatus.PLAYING || exoPlayer?.isPlaying != true) && playable != null) callback.onPlaybackStart(playable!!, getPosition())
if (status == PlayerStatus.SEEKING && statusBeforeSeeking != null) setPlayerStatus(statusBeforeSeeking!!, playable, getPosition())
}
override fun isCasting(): Boolean {
return false
}
private fun setupPlayerListener() {
exoplayerListener = object : Listener {
override fun onPlaybackStateChanged(playbackState: @State Int) {
Logd(TAG, "onPlaybackStateChanged $playbackState")
when (playbackState) {
STATE_ENDED -> {
exoPlayer?.seekTo(C.TIME_UNSET)
if (audioCompletionListener != null) audioCompletionListener?.run()
}
STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
else -> bufferingUpdateListener?.accept(BUFFERING_ENDED)
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
val stat = if (isPlaying) PlayerStatus.PLAYING else PlayerStatus.PAUSED
setPlayerStatus(stat, playable)
Logd(TAG, "onIsPlayingChanged $isPlaying")
}
override fun onPlayerError(error: PlaybackException) {
Logd(TAG, "onPlayerError ${error.message}")
if (wasDownloadBlocked(error)) audioErrorListener?.accept(context.getString(R.string.download_error_blocked))
else {
var cause = error.cause
if (cause is HttpDataSourceException && cause.cause != null) cause = cause.cause
if (cause != null && "Source error" == cause.message) cause = cause.cause
audioErrorListener?.accept(if (cause != null) cause.message else error.message)
}
}
override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) {
Logd(TAG, "onPositionDiscontinuity $oldPosition $newPosition $reason")
if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run()
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Logd(TAG, "onAudioSessionIdChanged $audioSessionId")
initLoudnessEnhancer(audioSessionId)
}
}
}
companion object {
private const val TAG = "LocalMediaPlayer"
@ -884,7 +922,7 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
.setIsGaplessSupportRequired(true)
.setIsSpeedChangeSupportRequired(true)
.build()
Log.d(TAG, "createStaticPlayer creating exoPlayer_")
Logd(TAG, "createStaticPlayer creating exoPlayer_")
exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context))
.setTrackSelector(trackSelector!!)
@ -897,39 +935,48 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
.setAudioOffloadPreferences(audioOffloadPreferences)
.build()
exoplayerListener = object : Listener {
override fun onPlaybackStateChanged(playbackState: @State Int) {
Log.d(TAG, "onPlaybackStateChanged $playbackState")
when (playbackState) {
STATE_ENDED -> {
exoPlayer?.seekTo(C.TIME_UNSET)
if (audioCompletionListener != null) audioCompletionListener?.run()
}
STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
else -> bufferingUpdateListener?.accept(BUFFERING_ENDED)
}
}
override fun onPlayerError(error: PlaybackException) {
Log.d(TAG, "onPlayerError ${error.message}")
if (wasDownloadBlocked(error)) audioErrorListener?.accept(context.getString(R.string.download_error_blocked))
else {
var cause = error.cause
if (cause is HttpDataSourceException && cause.cause != null) cause = cause.cause
if (cause != null && "Source error" == cause.message) cause = cause.cause
audioErrorListener?.accept(if (cause != null) cause.message else error.message)
}
}
override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) {
Log.d(TAG, "onPositionDiscontinuity $oldPosition $newPosition $reason")
if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run()
}
override fun onAudioSessionIdChanged(audioSessionId: Int) {
Log.d(TAG, "onAudioSessionIdChanged $audioSessionId")
initLoudnessEnhancer(audioSessionId)
}
}
// exoplayerListener = object : Listener {
// override fun onPlaybackStateChanged(playbackState: @State Int) {
// Log.d(TAG, "onPlaybackStateChanged $playbackState")
// when (playbackState) {
// STATE_ENDED -> {
// exoPlayer?.seekTo(C.TIME_UNSET)
// if (audioCompletionListener != null) audioCompletionListener?.run()
// }
// STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
// else -> bufferingUpdateListener?.accept(BUFFERING_ENDED)
// }
// }
// override fun onIsPlayingChanged(isPlaying: Boolean) {
// val status = if (isPlaying) PlayerStatus.PLAYING else PlayerStatus.PAUSED
// setPlayerStatus(status, )
// Log.d(TAG, "onIsPlayingChanged $isPlaying")
//// if (!isPlaying) context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
// }
// override fun onPlayerError(error: PlaybackException) {
// Log.d(TAG, "onPlayerError ${error.message}")
// if (wasDownloadBlocked(error)) audioErrorListener?.accept(context.getString(R.string.download_error_blocked))
// else {
// var cause = error.cause
// if (cause is HttpDataSourceException && cause.cause != null) cause = cause.cause
// if (cause != null && "Source error" == cause.message) cause = cause.cause
// audioErrorListener?.accept(if (cause != null) cause.message else error.message)
// }
// }
// override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) {
// Log.d(TAG, "onPositionDiscontinuity $oldPosition $newPosition $reason")
// if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run()
// }
// override fun onAudioSessionIdChanged(audioSessionId: Int) {
// Log.d(TAG, "onAudioSessionIdChanged $audioSessionId")
// initLoudnessEnhancer(audioSessionId)
// }
// }
exoPlayer?.addListener(exoplayerListener!!)
if (exoplayerListener != null) {
exoPlayer?.removeListener(exoplayerListener!!)
exoPlayer?.addListener(exoplayerListener!!)
}
initLoudnessEnhancer(exoPlayer!!.audioSessionId)
}

View File

@ -2,11 +2,10 @@ package ac.mdiq.podcini.playback.service
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.playback.PlayableUtils.saveCurrentPosition
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPInfo
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastPsmp
import ac.mdiq.podcini.playback.cast.CastStateListener
@ -56,6 +55,7 @@ import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.util.event.MessageEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
@ -99,7 +99,6 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.Volatile
import kotlin.math.max
@ -129,19 +128,61 @@ class PlaybackService : MediaSessionService() {
private val mBinder: IBinder = LocalBinder()
val mPlayerInfo: MediaPlayerInfo
get() = mediaPlayer!!.pSMPInfo
val status: PlayerStatus
get() = MediaPlayerBase.status
val playable: Playable?
get() = mediaPlayer?.getPlayable()
val sleepTimerTimeLeft: Long
get() = taskManager.sleepTimerTimeLeft
val currentPlaybackSpeed: Float
get() = mediaPlayer?.getPlaybackSpeed() ?: 1.0f
var isStartWhenPrepared: Boolean
get() = mediaPlayer?.isStartWhenPrepared() ?: false
set(s) {
mediaPlayer?.setStartWhenPrepared(s)
}
val duration: Int
get() = mediaPlayer?.getDuration() ?: Playable.INVALID_TIME
val currentPosition: Int
get() = mediaPlayer?.getPosition() ?: Playable.INVALID_TIME
var previousPosition: Int = -1
val audioTracks: List<String?>
get() = mediaPlayer?.getAudioTracks() ?: listOf()
val selectedAudioTrack: Int
get() = mediaPlayer?.getSelectedAudioTrack() ?: -1
val isStreaming: Boolean
get() = mediaPlayer?.isStreaming() ?: false
val videoSize: Pair<Int, Int>?
get() = mediaPlayer?.getVideoSize()
inner class LocalBinder : Binder() {
val service: PlaybackService
get() = this@PlaybackService
}
override fun onUnbind(intent: Intent): Boolean {
Log.d(TAG, "Received onUnbind event")
Logd(TAG, "Received onUnbind event")
return super.onUnbind(intent)
}
override fun onCreate() {
super.onCreate()
Log.d(TAG, "Service created.")
Logd(TAG, "Service created.")
isRunning = true
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
@ -171,17 +212,17 @@ class PlaybackService : MediaSessionService() {
fun recreateMediaSessionIfNeeded() {
if (mediaSession != null) return
Log.d(TAG, "recreateMediaSessionIfNeeded")
Logd(TAG, "recreateMediaSessionIfNeeded")
customMediaNotificationProvider = CustomMediaNotificationProvider(applicationContext)
setMediaNotificationProvider(customMediaNotificationProvider)
recreateMediaPlayer()
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
.setCallback(MyCallback())
.setCustomLayout(notificationCustomButtons)
.build()
recreateMediaPlayer()
}
fun recreateMediaPlayer() {
@ -189,7 +230,7 @@ class PlaybackService : MediaSessionService() {
var wasPlaying = false
if (mediaPlayer != null) {
media = mediaPlayer!!.getPlayable()
wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK
wasPlaying = MediaPlayerBase.status == PlayerStatus.PLAYING || MediaPlayerBase.status == PlayerStatus.FALLBACK
mediaPlayer!!.pause(abandonFocus = true, reinit = false)
mediaPlayer!!.shutdown()
}
@ -201,7 +242,7 @@ class PlaybackService : MediaSessionService() {
}
override fun onTaskRemoved(rootIntent: Intent?) {
Log.d(TAG, "onTaskRemoved")
Logd(TAG, "onTaskRemoved")
val player = mediaSession?.player
if (player != null) {
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
@ -214,13 +255,13 @@ class PlaybackService : MediaSessionService() {
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Service is about to be destroyed")
Logd(TAG, "Service is about to be destroyed")
isRunning = false
currentMediaType = MediaType.UNKNOWN
castStateListener.destroy()
cancelPositionObserver()
// cancelPositionObserver()
LocalMediaPlayer.cleanup()
mediaSession?.run {
player.release()
@ -245,7 +286,7 @@ class PlaybackService : MediaSessionService() {
private inner class MyCallback : MediaSession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
Log.d(TAG, "in onConnect")
Logd(TAG, "in onConnect")
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
// .add(NotificationCustomButton.REWIND)
// .add(NotificationCustomButton.FORWARD)
@ -273,7 +314,7 @@ class PlaybackService : MediaSessionService() {
/* Registering custom player command buttons for player notification. */
notificationCustomButtons.forEach { commandButton ->
Log.d(TAG, "onConnect commandButton ${commandButton.displayName}")
Logd(TAG, "onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
}
@ -468,7 +509,7 @@ class PlaybackService : MediaSessionService() {
// }
override fun onBind(intent: Intent?): IBinder? {
Log.d(TAG, "Received onBind event")
Logd(TAG, "Received onBind event")
return if (intent?.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) {
super.onBind(intent)
} else {
@ -484,7 +525,7 @@ class PlaybackService : MediaSessionService() {
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceConstants.EXTRA_PLAYABLE)
Log.d(TAG, "OnStartCommand flags=$flags startId=$startId $keycode $customAction $hardwareButton ${playable?.getEpisodeTitle()}")
Logd(TAG, "OnStartCommand flags=$flags startId=$startId $keycode $customAction $hardwareButton ${playable?.getEpisodeTitle()}")
if (keycode == -1 && playable == null && customAction == null) {
Log.e(TAG, "PlaybackService was started with no arguments")
@ -492,16 +533,16 @@ class PlaybackService : MediaSessionService() {
}
if ((flags and START_FLAG_REDELIVERY) != 0) {
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.")
Logd(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.")
} else {
when {
keycode != -1 -> {
val notificationButton: Boolean
if (hardwareButton) {
Log.d(TAG, "Received hardware button event")
Logd(TAG, "Received hardware button event")
notificationButton = false
} else {
Log.d(TAG, "Received media button event")
Logd(TAG, "Received media button event")
notificationButton = true
}
val handled = handleKeycode(keycode, notificationButton)
@ -520,7 +561,7 @@ class PlaybackService : MediaSessionService() {
.subscribe(
{ loadedPlayable: Playable? -> startPlaying(loadedPlayable, allowStreamThisTime) },
{ error: Throwable ->
Log.d(TAG, "Playable was not found. Stopping service.")
Logd(TAG, "Playable was not found. Stopping service.")
error.printStackTrace()
})
return START_NOT_STICKY
@ -544,7 +585,7 @@ class PlaybackService : MediaSessionService() {
if (skipIntro > 0 && playable.getPosition() < skipIntroMS) {
val duration = duration
if (skipIntroMS < duration || duration <= 0) {
Log.d(TAG, "skipIntro " + playable.getEpisodeTitle())
Logd(TAG, "skipIntro " + playable.getEpisodeTitle())
mediaPlayer?.seekTo(skipIntroMS)
val skipIntroMesg = applicationContext.getString(R.string.pref_feed_skip_intro_toast, skipIntro)
val toast = Toast.makeText(applicationContext, skipIntroMesg, Toast.LENGTH_LONG)
@ -563,20 +604,20 @@ class PlaybackService : MediaSessionService() {
val intentAllowThisTime = Intent(originalIntent)
intentAllowThisTime.setAction(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME)
intentAllowThisTime.putExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_THIS_TIME, true)
val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
val pendingIntentAllowThisTime =
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time,
intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
else PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
else PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
val intentAlwaysAllow = Intent(intentAllowThisTime)
intentAlwaysAllow.setAction(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS)
intentAlwaysAllow.putExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS, true)
val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
val pendingIntentAlwaysAllow =
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always,
intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_USER_ACTION)
.setSmallIcon(R.drawable.ic_notification_stream)
@ -598,7 +639,7 @@ class PlaybackService : MediaSessionService() {
* return: keycode was handled
*/
private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean {
Log.d(TAG, "Handling keycode: $keycode")
Logd(TAG, "Handling keycode: $keycode")
val info = mediaPlayer?.pSMPInfo
val status = info?.playerStatus
when (keycode) {
@ -679,7 +720,7 @@ class PlaybackService : MediaSessionService() {
return true
}
else -> {
Log.d(TAG, "Unhandled key code: $keycode")
Logd(TAG, "Unhandled key code: $keycode")
if (info?.playable != null && info.playerStatus == PlayerStatus.PLAYING) {
// only notify the user about an unknown key event if it is actually doing something
val message = String.format(resources.getString(R.string.unknown_media_key), keycode)
@ -697,7 +738,7 @@ class PlaybackService : MediaSessionService() {
.subscribe(
{ playable: Playable? -> startPlaying(playable, false) },
{ error: Throwable ->
Log.d(TAG, "Playable was not loaded from preferences. Stopping service.")
Logd(TAG, "Playable was not loaded from preferences. Stopping service.")
error.printStackTrace()
})
}
@ -727,7 +768,7 @@ class PlaybackService : MediaSessionService() {
* mediaplayer.
*/
fun setVideoSurface(sh: SurfaceHolder?) {
Log.d(TAG, "Setting display")
Logd(TAG, "Setting display")
mediaPlayer?.setVideoSurface(sh)
}
@ -737,9 +778,16 @@ class PlaybackService : MediaSessionService() {
// updateNotificationAndMediaSession(playable)
}
// TODO: positionEventTimer also monitors position, should be combined?
private val taskManagerCallback: PSTMCallback = object : PSTMCallback {
override fun positionSaverTick() {
saveCurrentPosition(true, null, Playable.INVALID_TIME)
if (currentPosition != previousPosition) {
// Log.d(TAG, "positionSaverTick currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed")
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
skipEndingIfNecessary()
saveCurrentPosition(true, null, Playable.INVALID_TIME)
previousPosition = currentPosition
}
}
override fun requestWidgetState(): WidgetState {
@ -749,14 +797,14 @@ class PlaybackService : MediaSessionService() {
override fun onChapterLoaded(media: Playable?) {
sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0)
// updateMediaSession(mediaPlayer?.playerStatus)
// updateMediaSession(MediaPlayerBase.status)
}
}
private val mediaPlayerCallback: PSMPCallback = object : PSMPCallback {
override fun statusChanged(newInfo: PSMPInfo?) {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN
Log.d(TAG, "statusChanged called ${newInfo?.playerStatus}")
Logd(TAG, "statusChanged called ${newInfo?.playerStatus}")
// updateMediaSession(newInfo?.playerStatus)
if (newInfo != null) {
when (newInfo.playerStatus) {
@ -770,16 +818,16 @@ class PlaybackService : MediaSessionService() {
}
PlayerStatus.PAUSED -> {
// updateNotificationAndMediaSession(newInfo.playable)
cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
// cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(MediaPlayerBase.status)
}
PlayerStatus.STOPPED -> {}
PlayerStatus.PLAYING -> {
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
if (mediaPlayer != null) writePlayerStatus(MediaPlayerBase.status)
saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded()
// updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver()
// setupPositionObserver()
// set sleep timer if auto-enabled
var autoEnableByTime = true
val fromSetting = autoEnableFrom()
@ -815,7 +863,7 @@ class PlaybackService : MediaSessionService() {
override fun shouldStop() {}
override fun onMediaChanged(reloadUI: Boolean) {
Log.d(TAG, "reloadUI callback reached")
Logd(TAG, "reloadUI callback reached")
if (reloadUI) sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0)
// updateNotificationAndMediaSession(this@PlaybackService.playable)
}
@ -834,12 +882,11 @@ class PlaybackService : MediaSessionService() {
override fun onPlaybackPause(playable: Playable?, position: Int) {
taskManager.cancelPositionSaver()
cancelPositionObserver()
// cancelPositionObserver()
saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position)
taskManager.cancelWidgetUpdater()
if (playable != null) {
if (playable is FeedMedia)
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false)
if (playable is FeedMedia) SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false)
playable.onPlaybackPause(applicationContext)
}
}
@ -865,7 +912,7 @@ class PlaybackService : MediaSessionService() {
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun playerError(event: PlayerErrorEvent?) {
if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK)
if (MediaPlayerBase.status == PlayerStatus.PLAYING || MediaPlayerBase.status == PlayerStatus.FALLBACK)
mediaPlayer!!.pause(abandonFocus = true, reinit = false)
}
@ -895,7 +942,7 @@ class PlaybackService : MediaSessionService() {
val multiplicators = floatArrayOf(0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f)
val multiplicator = multiplicators[max(0.0, (event.getTimeLeft().toInt() / 1000).toDouble())
.toInt()]
Log.d(TAG, "onSleepTimerAlmostExpired: $multiplicator")
Logd(TAG, "onSleepTimerAlmostExpired: $multiplicator")
mediaPlayer?.setVolume(multiplicator, multiplicator)
}
event.isCancelled -> mediaPlayer?.setVolume(1.0f, 1.0f)
@ -903,9 +950,9 @@ class PlaybackService : MediaSessionService() {
}
private fun getNextInQueue(currentMedia: Playable?): Playable? {
Log.d(TAG, "getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}")
Logd(TAG, "getNextInQueue currentMedia: ${currentMedia?.getEpisodeTitle()}")
if (currentMedia !is FeedMedia) {
Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding")
Logd(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding")
writeNoMediaPlaying()
return null
}
@ -919,13 +966,13 @@ class PlaybackService : MediaSessionService() {
val nextItem = DBReader.getNextInQueue(item)
if (nextItem?.media == null) {
Log.d(TAG, "getNextInQueue nextItem: $nextItem media is null")
Logd(TAG, "getNextInQueue nextItem: $nextItem media is null")
writeNoMediaPlaying()
return null
}
if (!isFollowQueue) {
Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.")
Logd(TAG, "getNextInQueue(), but follow queue is not enabled.")
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED, currentitem)
// updateNotificationAndMediaSession(nextItem.media)
return null
@ -943,11 +990,11 @@ class PlaybackService : MediaSessionService() {
* Set of instructions to be performed when playback ends.
*/
private fun onPlaybackEnded(mediaType: MediaType?, stopPlaying: Boolean) {
Log.d(TAG, "onPlaybackEnded mediaType: $mediaType stopPlaying: $stopPlaying")
Logd(TAG, "onPlaybackEnded mediaType: $mediaType stopPlaying: $stopPlaying")
clearCurrentlyPlayingTemporaryPlaybackSpeed()
if (stopPlaying) {
taskManager.cancelPositionSaver()
cancelPositionObserver()
// cancelPositionObserver()
}
if (mediaType == null) {
sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_PLAYBACK_END, 0)
@ -986,18 +1033,19 @@ class PlaybackService : MediaSessionService() {
Log.e(TAG, "Cannot do post-playback processing: media was null")
return
}
Log.d(TAG, "onPostPlayback(): ended=$ended skipped=$skipped playingNext=$playingNext media=${playable.getEpisodeTitle()} ")
Logd(TAG, "onPostPlayback(): ended=$ended skipped=$skipped playingNext=$playingNext media=${playable.getEpisodeTitle()} ")
if (playable !is FeedMedia) {
Log.d(TAG, "Not doing post-playback processing: media not of type FeedMedia")
Logd(TAG, "Not doing post-playback processing: media not of type FeedMedia")
if (ended) playable.onPlaybackCompleted(applicationContext)
else playable.onPlaybackPause(applicationContext)
// TODO: test
// return
}
val media = playable
val item = (media as? FeedMedia)?.item ?: currentitem
val smartMarkAsPlayed = hasAlmostEnded(media)
if (!ended && smartMarkAsPlayed) Log.d(TAG, "smart mark as played")
if (!ended && smartMarkAsPlayed) Logd(TAG, "smart mark as played")
var autoSkipped = false
if (autoSkippedFeedMediaId != null && autoSkippedFeedMediaId == item?.identifyingValue) {
@ -1017,7 +1065,7 @@ class PlaybackService : MediaSessionService() {
if (item != null) {
if (ended || smartMarkAsPlayed || autoSkipped || (skipped && !shouldSkipKeepEpisode())) {
Log.d(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped")
Logd(TAG, "onPostPlayback ended: $ended smartMarkAsPlayed: $smartMarkAsPlayed autoSkipped: $autoSkipped skipped: $skipped")
// only mark the item as played if we're not keeping it anyways
DBWriter.markItemPlayed(item, FeedItem.PLAYED, ended || (skipped && smartMarkAsPlayed))
// don't know if it actually matters to not autodownload when smart mark as played is triggered
@ -1028,7 +1076,7 @@ class PlaybackService : MediaSessionService() {
|| (action == AutoDeleteAction.GLOBAL && item.feed != null && shouldAutoDeleteItemsOnThatFeed(item.feed!!)))
if (media is FeedMedia && shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) {
DBWriter.deleteFeedMediaOfItem(this@PlaybackService, media.id)
Log.d(TAG, "Episode Deleted")
Logd(TAG, "Episode Deleted")
}
// notifyChildrenChanged(getString(R.string.queue_label))
}
@ -1038,7 +1086,7 @@ class PlaybackService : MediaSessionService() {
}
fun setSleepTimer(waitingTime: Long) {
Log.d(TAG, "Setting sleep timer to $waitingTime milliseconds")
Logd(TAG, "Setting sleep timer to $waitingTime milliseconds")
taskManager.setSleepTimer(waitingTime)
}
@ -1068,7 +1116,7 @@ class PlaybackService : MediaSessionService() {
val skipEndMS = skipEnd * 1000
// Log.d(TAG, "skipEndingIfNecessary: checking " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed)
if (skipEnd > 0 && skipEndMS < this.duration && (remainingTime - skipEndMS < 0)) {
Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining $remainingTime $skipEndMS speed $currentPlaybackSpeed")
Logd(TAG, "skipEndingIfNecessary: Skipping the remaining $remainingTime $skipEndMS speed $currentPlaybackSpeed")
val context = applicationContext
val skipMesg = context.getString(R.string.pref_feed_skip_ending_toast, skipEnd)
val toast = Toast.makeText(context, skipMesg, Toast.LENGTH_LONG)
@ -1172,6 +1220,7 @@ class PlaybackService : MediaSessionService() {
if (position != Playable.INVALID_TIME && duration != Playable.INVALID_TIME && playable != null) {
// Log.d(TAG, "Saving current position to $position $duration")
saveCurrentPosition(playable, position, System.currentTimeMillis())
previousPosition = position
}
}
@ -1179,10 +1228,7 @@ class PlaybackService : MediaSessionService() {
return taskManager.isSleepTimerActive
}
val sleepTimerTimeLeft: Long
get() = taskManager.sleepTimerTimeLeft
private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) {
private fun bluetoothNotifyChange(info: MediaPlayerInfo?, whatChanged: String) {
var isPlaying = false
if (info?.playerStatus == PlayerStatus.PLAYING || info?.playerStatus == PlayerStatus.FALLBACK) isPlaying = true
@ -1204,11 +1250,11 @@ class PlaybackService : MediaSessionService() {
override fun onReceive(context: Context, intent: Intent) {
val status = intent.getStringExtra("media_connection_status")
val isConnectedToCar = "media_connected" == status
Log.d(TAG, "Received Auto Connection update: $status")
Logd(TAG, "Received Auto Connection update: $status")
if (!isConnectedToCar) {
Log.d(TAG, "Car was unplugged during playback.")
Logd(TAG, "Car was unplugged during playback.")
} else {
val playerStatus = mediaPlayer?.playerStatus
val playerStatus = MediaPlayerBase.status
when (playerStatus) {
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> mediaPlayer?.resume()
PlayerStatus.PREPARING -> mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared())
@ -1238,12 +1284,12 @@ class PlaybackService : MediaSessionService() {
if (TextUtils.equals(intent.action, Intent.ACTION_HEADSET_PLUG)) {
val state = intent.getIntExtra("state", -1)
Log.d(TAG, "Headset plug event. State is $state")
Logd(TAG, "Headset plug event. State is $state")
if (state != -1) {
when (state) {
UNPLUGGED -> Log.d(TAG, "Headset was unplugged during playback.")
UNPLUGGED -> Logd(TAG, "Headset was unplugged during playback.")
PLUGGED -> {
Log.d(TAG, "Headset was plugged in during playback.")
Logd(TAG, "Headset was plugged in during playback.")
unpauseIfPauseOnDisconnect(false)
}
}
@ -1259,7 +1305,7 @@ class PlaybackService : MediaSessionService() {
if (TextUtils.equals(intent.action, BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
val state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1)
if (state == BluetoothA2dp.STATE_CONNECTED) {
Log.d(TAG, "Received bluetooth connection intent")
Logd(TAG, "Received bluetooth connection intent")
unpauseIfPauseOnDisconnect(true)
}
}
@ -1269,7 +1315,7 @@ class PlaybackService : MediaSessionService() {
private val audioBecomingNoisy: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
// sound is about to change, eg. bluetooth -> speaker
Log.d(TAG, "Pausing playback because audio is becoming noisy")
Logd(TAG, "Pausing playback because audio is becoming noisy")
pauseIfPauseOnDisconnect()
}
}
@ -1278,8 +1324,8 @@ class PlaybackService : MediaSessionService() {
* Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true.
*/
private fun pauseIfPauseOnDisconnect() {
Log.d(TAG, "pauseIfPauseOnDisconnect()")
transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK)
Logd(TAG, "pauseIfPauseOnDisconnect()")
transientPause = (MediaPlayerBase.status == PlayerStatus.PLAYING || MediaPlayerBase.status == PlayerStatus.FALLBACK)
if (isPauseOnHeadsetDisconnect && !isCasting) mediaPlayer?.pause(!isPersistNotify, false)
}
@ -1288,7 +1334,7 @@ class PlaybackService : MediaSessionService() {
*/
private fun unpauseIfPauseOnDisconnect(bluetooth: Boolean) {
if (mediaPlayer != null && mediaPlayer!!.isAudioChannelInUse) {
Log.d(TAG, "unpauseIfPauseOnDisconnect() audio is in use")
Logd(TAG, "unpauseIfPauseOnDisconnect() audio is in use")
return
}
if (transientPause) {
@ -1338,7 +1384,7 @@ class PlaybackService : MediaSessionService() {
if (item?.feed?.id == event.feedId) {
val feedPreferences = item.feed?.preferences
if (feedPreferences != null) {
Log.d(TAG, "skipIntroEndingPresetChanged ${event.skipIntro} ${event.skipEnding}")
Logd(TAG, "skipIntroEndingPresetChanged ${event.skipIntro} ${event.skipEnding}")
feedPreferences.feedSkipIntro = event.skipIntro
feedPreferences.feedSkipEnding = event.skipEnding
}
@ -1348,7 +1394,7 @@ class PlaybackService : MediaSessionService() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvenStartPlay(event: StartPlayEvent) {
Log.d(TAG, "onEvenStartPlay ${event.item.title}")
Logd(TAG, "onEvenStartPlay ${event.item.title}")
currentitem = event.item
}
@ -1368,15 +1414,6 @@ class PlaybackService : MediaSessionService() {
isFallbackSpeed = false
}
val pSMPInfo: PSMPInfo
get() = mediaPlayer!!.pSMPInfo
val status: PlayerStatus
get() = mediaPlayer!!.playerStatus
val playable: Playable?
get() = mediaPlayer?.getPlayable()
fun setSpeed(speed: Float, codeArray: BooleanArray? = null) {
isSpeedForward = false
isFallbackSpeed = false
@ -1387,7 +1424,7 @@ class PlaybackService : MediaSessionService() {
mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
} else {
if (codeArray != null && codeArray.size == 3) {
Log.d(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
Logd(TAG, "setSpeed codeArray: ${codeArray[0]} ${codeArray[1]} ${codeArray[2]}")
if (codeArray[2]) setPlaybackSpeed(speed)
if (codeArray[1]) {
var item = (playable as? FeedMedia)?.item ?: currentitem
@ -1403,7 +1440,7 @@ class PlaybackService : MediaSessionService() {
val feedPreferences = feed.preferences
if (feedPreferences != null) {
feedPreferences.feedPlaybackSpeed = speed
Log.d(TAG, "setSpeed ${feed.title} $speed")
Logd(TAG, "setSpeed ${feed.title} $speed")
DBWriter.persistFeedPreferences(feedPreferences)
EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id))
}
@ -1447,68 +1484,33 @@ class PlaybackService : MediaSessionService() {
mediaPlayer?.setPlaybackParams(currentPlaybackSpeed, skipSilence)
}
val currentPlaybackSpeed: Float
get() {
return mediaPlayer?.getPlaybackSpeed() ?: 1.0f
}
var isStartWhenPrepared: Boolean
get() = mediaPlayer?.isStartWhenPrepared() ?: false
set(s) {
mediaPlayer?.setStartWhenPrepared(s)
}
fun seekTo(t: Int) {
mediaPlayer?.seekTo(t)
EventBus.getDefault().post(PlaybackPositionEvent(t, duration))
}
val duration: Int
get() {
return mediaPlayer?.getDuration() ?: Playable.INVALID_TIME
}
val currentPosition: Int
get() {
return mediaPlayer?.getPosition() ?: Playable.INVALID_TIME
}
val audioTracks: List<String?>
get() {
return mediaPlayer?.getAudioTracks() ?: listOf()
}
val selectedAudioTrack: Int
get() {
return mediaPlayer?.getSelectedAudioTrack() ?: -1
}
fun setAudioTrack(track: Int) {
mediaPlayer?.setAudioTrack(track)
}
val isStreaming: Boolean
get() = mediaPlayer?.isStreaming() ?: false
val videoSize: Pair<Int, Int>?
get() = mediaPlayer?.getVideoSize()
private fun setupPositionObserver() {
positionEventTimer?.dispose()
Log.d(TAG, "Setting up position observer")
positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Log.d(TAG, "positionEventTimer currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed")
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
skipEndingIfNecessary()
}
}
private fun cancelPositionObserver() {
positionEventTimer?.dispose()
}
// private fun setupPositionObserver() {
// positionEventTimer?.dispose()
//
// Log.d(TAG, "Setting up position observer")
// positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS)
// .observeOn(AndroidSchedulers.mainThread())
//// .takeWhile { currentPosition != previousPosition }
// .subscribe {
// Log.d(TAG, "positionEventTimer currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed")
// EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
// previousPosition = currentPosition
// skipEndingIfNecessary()
// }
// }
//
// private fun cancelPositionObserver() {
// positionEventTimer?.dispose()
// }
private fun addPlayableToQueue(playable: Playable?) {
if (playable is FeedMedia) {
@ -1518,6 +1520,28 @@ class PlaybackService : MediaSessionService() {
}
}
/**
* Saves the current position of this object.
*
* @param newPosition new playback position in ms
* @param timestamp current time in ms
*/
@UnstableApi
fun saveCurrentPosition(playable: Playable, newPosition: Int, timestamp: Long) {
playable.setPosition(newPosition)
playable.setLastPlayedTime(timestamp)
if (playable is FeedMedia) {
val item = playable.item
if (item != null && item.isNew) DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.id)
if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition)
playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition)
DBWriter.persistFeedMediaPlaybackInfo(playable)
}
}
companion object {
private const val TAG = "PlaybackService"

View File

@ -12,13 +12,12 @@ internal class PlaybackVolumeUpdater {
if (playable is FeedMedia) updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, playable)
}
private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long,
volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia) {
private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: MediaPlayerBase, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia) {
if (feedMedia.item?.feed?.id == feedId) {
val preferences = feedMedia.item!!.feed!!.preferences
if (preferences != null) preferences.volumeAdaptionSetting = volumeAdaptionSetting
if (mediaPlayer.playerStatus == PlayerStatus.PLAYING) forceUpdateVolume(mediaPlayer)
if (MediaPlayerBase.status == PlayerStatus.PLAYING) forceUpdateVolume(mediaPlayer)
}
}

View File

@ -1,19 +1,9 @@
package ac.mdiq.podcini.preferences
import android.content.Context
import android.content.SharedPreferences
import android.view.KeyEvent
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.SleepTimerPreferences.lastTimerValue
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setLastTimer
import ac.mdiq.podcini.util.error.CrashReportWriter.Companion.file
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
import ac.mdiq.podcini.ui.fragment.QueueFragment
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
import ac.mdiq.podcini.preferences.UserPreferences.episodeCleanupValue
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileAutoDownload
@ -24,6 +14,16 @@ import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileSync
import ac.mdiq.podcini.preferences.UserPreferences.isQueueLocked
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
import ac.mdiq.podcini.preferences.UserPreferences.theme
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeAction
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
import ac.mdiq.podcini.ui.fragment.QueueFragment
import ac.mdiq.podcini.util.error.CrashReportWriter.Companion.file
import android.content.Context
import android.content.SharedPreferences
import android.view.KeyEvent
import androidx.core.app.NotificationManagerCompat
import androidx.preference.PreferenceManager
import org.apache.commons.lang3.StringUtils
import java.util.concurrent.TimeUnit

View File

@ -41,25 +41,23 @@ import java.text.SimpleDateFormat
import java.util.*
class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private val chooseOpmlExportPathLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseOpmlExportPathResult(result)
}
private val chooseHtmlExportPathLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseHtmlExportPathResult(result)
}
private val chooseFavoritesExportPathLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseFavoritesExportPathResult(result)
}
private val restoreDatabaseLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.restoreDatabaseResult(result)
}
private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseOpmlExportPathResult(result) }
private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseHtmlExportPathResult(result) }
private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.chooseFavoritesExportPathResult(result) }
private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.restoreDatabaseResult(result) }
private val backupDatabaseLauncher = registerForActivityResult<String, Uri>(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
private val chooseOpmlImportPathLauncher =
registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
this.chooseOpmlImportPathResult(uri) }
// TODO: implement
private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
this.restorePreferencesResult(result) }
private val backupPreferencesLauncher = registerForActivityResult<String, Uri>(BackupPreferences()) { uri: Uri? -> this.backupPreferencesResult(uri) }
private var disposable: Disposable? = null
private var progressDialog: ProgressDialog? = null
@ -110,6 +108,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
exportDatabase()
true
}
findPreference<Preference>(PREF_PREFERENCES_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
importPreferences()
true
}
findPreference<Preference>(PREF_PREFERENCES_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
exportPreferences()
true
}
findPreference<Preference>(PREF_FAVORITE_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
openExportPathPicker(Export.FAVORITES, chooseFavoritesExportPathLauncher, FavoritesWriter())
true
@ -132,14 +139,22 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
disposable = worker.exportObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ output: DocumentFile? ->
showExportSuccessSnackbar(output?.uri, exportType.contentType)
},
.subscribe({ output: DocumentFile? -> showExportSuccessSnackbar(output?.uri, exportType.contentType) },
{ error: Throwable -> this.showExportErrorDialog(error) },
{ progressDialog!!.dismiss() })
}
}
// TODO: implement
private fun exportPreferences() {
// backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
}
// TODO: implement
private fun importPreferences() {
// backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
}
private fun exportDatabase() {
backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
}
@ -173,13 +188,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
fun showExportSuccessSnackbar(uri: Uri?, mimeType: String?) {
Snackbar.make(requireView(), R.string.export_success_title, Snackbar.LENGTH_LONG)
.setAction(R.string.share_label) {
IntentBuilder(requireContext())
.setType(mimeType)
.addStream(uri!!)
.setChooserTitle(R.string.share_label)
.startChooser()
}
.setAction(R.string.share_label) { IntentBuilder(requireContext()).setType(mimeType).addStream(uri!!).setChooserTitle(R.string.share_label).startChooser() }
.show()
}
@ -235,6 +244,31 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}, { error: Throwable -> this.showExportErrorDialog(error) })
}
private fun restorePreferencesResult(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK || result.data == null) return
// val uri = result.data!!.data
// progressDialog!!.show()
// disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) }
// .subscribeOn(Schedulers.io())
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe({
// showDatabaseImportSuccessDialog()
// progressDialog!!.dismiss()
// }, { error: Throwable -> this.showExportErrorDialog(error) })
}
private fun backupPreferencesResult(uri: Uri?) {
if (uri == null) return
// progressDialog!!.show()
// disposable = Completable.fromAction { DatabaseTransporter.exportToDocument(uri, requireContext()) }
// .subscribeOn(Schedulers.io())
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe({
// showExportSuccessSnackbar(uri, "application/x-sqlite3")
// progressDialog!!.dismiss()
// }, { error: Throwable -> this.showExportErrorDialog(error) })
}
private fun chooseOpmlImportPathResult(uri: Uri?) {
if (uri == null) return
val intent = Intent(context, OpmlImportActivity::class.java)
@ -272,6 +306,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
}
private class BackupPreferences : CreateDocument() {
override fun createIntent(context: Context, input: String): Intent {
return super.createIntent(context, input)
// .addCategory(Intent.CATEGORY_OPENABLE)
// .setType("application/x-sqlite3")
}
}
private enum class Export(val contentType: String, val outputNameTemplate: String, @field:StringRes val labelResId: Int) {
OPML(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME, R.string.opml_export_label),
HTML(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME, R.string.html_export_label),
@ -283,6 +326,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
private const val PREF_OPML_EXPORT = "prefOpmlExport"
private const val PREF_OPML_IMPORT = "prefOpmlImport"
private const val PREF_HTML_EXPORT = "prefHtmlExport"
private const val PREF_PREFERENCES_IMPORT = "prefPrefImport"
private const val PREF_PREFERENCES_EXPORT = "prefPrefExport"
private const val PREF_DATABASE_IMPORT = "prefDatabaseImport"
private const val PREF_DATABASE_EXPORT = "prefDatabaseExport"
private const val PREF_FAVORITE_EXPORT = "prefFavoritesExport"

View File

@ -9,6 +9,7 @@ import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.gpoddernet.GpodnetService
import ac.mdiq.podcini.net.sync.gpoddernet.model.GpodnetDevice
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setSelectedSyncProvider
import ac.mdiq.podcini.util.FileNameGenerator.generateFileName
import android.app.Dialog
import android.content.Context
@ -234,7 +235,7 @@ class GpodderAuthenticationFragment : DialogFragment() {
}
STEP_DEVICE -> {
checkNotNull(selectedDevice) { "Device must not be null here" }
SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET)
setSelectedSyncProvider(SynchronizationProviderViewData.GPODDER_NET)
SynchronizationCredentials.username = username
SynchronizationCredentials.password = password
SynchronizationCredentials.deviceID = selectedDevice!!.id

View File

@ -13,6 +13,7 @@ import ac.mdiq.podcini.net.sync.SynchronizationCredentials
import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.databinding.NextcloudAuthDialogBinding
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setSelectedSyncProvider
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow
/**
@ -68,7 +69,7 @@ class NextcloudAuthenticationFragment : DialogFragment(), NextcloudLoginFlow.Aut
}
override fun onNextcloudAuthenticated(server: String, username: String, password: String) {
SynchronizationSettings.setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER)
setSelectedSyncProvider(SynchronizationProviderViewData.NEXTCLOUD_GPODDER)
SynchronizationCredentials.clear(requireContext())
SynchronizationCredentials.password = password
SynchronizationCredentials.hosturl = server

View File

@ -23,6 +23,8 @@ import ac.mdiq.podcini.net.sync.SyncService
import ac.mdiq.podcini.net.sync.SynchronizationCredentials
import ac.mdiq.podcini.net.sync.SynchronizationProviderViewData
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
import ac.mdiq.podcini.util.event.SyncServiceEvent
import org.greenrobot.eventbus.EventBus
@ -30,6 +32,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_synchronization)
setupScreen()
@ -51,7 +54,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun syncStatusChanged(event: SyncServiceEvent) {
if (!SynchronizationSettings.isProviderConnected) return
if (!isProviderConnected && !wifiSyncEnabledKey) return
updateScreen()
if (event.messageResId == R.string.sync_status_error || event.messageResId == R.string.sync_status_success)
@ -89,6 +92,12 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
}
private fun updateScreen() {
val preferenceInstantSync = findPreference<Preference>(PREFERENCE_INSTANT_SYNC)
preferenceInstantSync!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
WifiAuthenticationFragment().show(childFragmentManager, WifiAuthenticationFragment.TAG)
true
}
val loggedIn = SynchronizationSettings.isProviderConnected
val preferenceHeader = findPreference<Preference>(PREFERENCE_SYNCHRONIZATION_DESCRIPTION)
if (loggedIn) {
@ -112,9 +121,9 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
val gpodnetSetLoginPreference = findPreference<Preference>(PREFERENCE_GPODNET_SETLOGIN_INFORMATION)
gpodnetSetLoginPreference!!.isVisible = isProviderSelected(SynchronizationProviderViewData.GPODDER_NET)
gpodnetSetLoginPreference.isEnabled = loggedIn
findPreference<Preference>(PREFERENCE_SYNC)!!.isEnabled = loggedIn
findPreference<Preference>(PREFERENCE_FORCE_FULL_SYNC)!!.isEnabled = loggedIn
findPreference<Preference>(PREFERENCE_LOGOUT)!!.isEnabled = loggedIn
findPreference<Preference>(PREFERENCE_SYNC)!!.isVisible = loggedIn
findPreference<Preference>(PREFERENCE_FORCE_FULL_SYNC)!!.isVisible = loggedIn
findPreference<Preference>(PREFERENCE_LOGOUT)!!.isVisible = loggedIn
if (loggedIn) {
val summary = getString(R.string.synchronization_login_status,
SynchronizationCredentials.username, SynchronizationCredentials.hosturl)
@ -132,8 +141,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
builder.setTitle(R.string.dialog_choose_sync_service_title)
val providers = SynchronizationProviderViewData.entries.toTypedArray()
val adapter: ListAdapter = object : ArrayAdapter<SynchronizationProviderViewData?>(
requireContext(), R.layout.alertdialog_sync_provider_chooser, providers) {
val adapter: ListAdapter = object : ArrayAdapter<SynchronizationProviderViewData?>(requireContext(), R.layout.alertdialog_sync_provider_chooser, providers) {
var holder: ViewHolder? = null
inner class ViewHolder {
@ -164,10 +172,8 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
builder.setAdapter(adapter) { _: DialogInterface?, which: Int ->
when (providers[which]) {
SynchronizationProviderViewData.GPODDER_NET -> GpodderAuthenticationFragment()
.show(childFragmentManager, GpodderAuthenticationFragment.TAG)
SynchronizationProviderViewData.NEXTCLOUD_GPODDER -> NextcloudAuthenticationFragment()
.show(childFragmentManager, NextcloudAuthenticationFragment.TAG)
SynchronizationProviderViewData.GPODDER_NET -> GpodderAuthenticationFragment().show(childFragmentManager, GpodderAuthenticationFragment.TAG)
SynchronizationProviderViewData.NEXTCLOUD_GPODDER -> NextcloudAuthenticationFragment().show(childFragmentManager, NextcloudAuthenticationFragment.TAG)
}
updateScreen()
}
@ -190,6 +196,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
}
companion object {
private const val PREFERENCE_INSTANT_SYNC = "preference_instant_sync"
private const val PREFERENCE_SYNCHRONIZATION_DESCRIPTION = "preference_synchronization_description"
private const val PREFERENCE_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"
private const val PREFERENCE_SYNC = "pref_synchronization_sync"

View File

@ -0,0 +1,119 @@
package ac.mdiq.podcini.preferences.fragments.synchronization
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.WifiSyncDialogBinding
import ac.mdiq.podcini.net.sync.SynchronizationCredentials
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setWifiSyncEnabled
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync
import ac.mdiq.podcini.util.event.SyncServiceEvent
import android.app.Dialog
import android.content.Context.WIFI_SERVICE
import android.net.wifi.WifiManager
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.util.*
@OptIn(UnstableApi::class) class WifiAuthenticationFragment : DialogFragment() {
private var binding: WifiSyncDialogBinding? = null
private var portNum = 0
private var isGuest: Boolean? = null
@OptIn(UnstableApi::class) override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = MaterialAlertDialogBuilder(requireContext())
dialog.setTitle(R.string.connect_to_peer)
dialog.setNegativeButton(R.string.cancel_label, null)
dialog.setPositiveButton(R.string.confirm_label, null)
binding = WifiSyncDialogBinding.inflate(layoutInflater)
dialog.setView(binding!!.root)
binding!!.hostAddressText.setText(SynchronizationCredentials.hosturl?:"")
portNum = SynchronizationCredentials.hostport
if (portNum == 0) portNum = hostPort
binding!!.hostPortText.setText(portNum.toString())
binding!!.guestButton.setOnClickListener {
binding!!.hostAddressText.visibility = View.VISIBLE
binding!!.hostPortText.visibility = View.VISIBLE
binding!!.hostButton.visibility = View.INVISIBLE
SynchronizationCredentials.hosturl = binding!!.hostAddressText.text.toString()
portNum = binding!!.hostPortText.text.toString().toInt()
isGuest = true
SynchronizationCredentials.hostport = portNum
}
binding!!.hostButton.setOnClickListener {
binding!!.hostAddressText.visibility = View.VISIBLE
binding!!.hostPortText.visibility = View.VISIBLE
binding!!.guestButton.visibility = View.INVISIBLE
val wifiManager = requireContext().applicationContext.getSystemService(WIFI_SERVICE) as WifiManager
val ipAddress = wifiManager.connectionInfo.ipAddress
val ipString = String.format(Locale.US, "%d.%d.%d.%d", ipAddress and 0xff, ipAddress shr 8 and 0xff, ipAddress shr 16 and 0xff, ipAddress shr 24 and 0xff)
binding!!.hostAddressText.setText(ipString)
binding!!.hostAddressText.isEnabled = false
portNum = binding!!.hostPortText.text.toString().toInt()
isGuest = false
SynchronizationCredentials.hostport = portNum
}
EventBus.getDefault().register(this)
return dialog.create()
}
@OptIn(UnstableApi::class) override fun onResume() {
super.onResume()
val d = dialog as? AlertDialog
if (d != null) {
val confirmButton = d.getButton(Dialog.BUTTON_POSITIVE) as Button
confirmButton.setOnClickListener {
Log.d(TAG, "confirm button pressed")
if (isGuest == null) {
Toast.makeText(requireContext(), R.string.host_or_guest, Toast.LENGTH_LONG).show()
return@setOnClickListener
}
binding!!.progressContainer.visibility = View.VISIBLE
confirmButton.visibility = View.INVISIBLE
val cancelButton = d.getButton(Dialog.BUTTON_NEGATIVE) as Button
cancelButton.visibility = View.INVISIBLE
portNum = binding!!.hostPortText.text.toString().toInt()
setWifiSyncEnabled(true)
startInstantSync(requireContext(), portNum, binding!!.hostAddressText.text.toString(), isGuest!!)
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun syncStatusChanged(event: SyncServiceEvent) {
when (event.messageResId) {
R.string.sync_status_error -> {
Toast.makeText(requireContext(), event.message, Toast.LENGTH_LONG).show()
dialog?.dismiss()
}
R.string.sync_status_success -> {
Toast.makeText(requireContext(), R.string.sync_status_success, Toast.LENGTH_LONG).show()
dialog?.dismiss()
}
R.string.sync_status_in_progress -> {
binding!!.progressBar.progress = event.message.toInt()
}
else -> {
Log.d(TAG, "Sync result unknow ${event.messageResId}")
// Toast.makeText(context, "Sync result unknow ${event.messageResId}", Toast.LENGTH_LONG).show()
}
}
}
companion object {
const val TAG: String = "WifiAuthenticationFragment"
}
}

View File

@ -13,6 +13,7 @@ import ac.mdiq.podcini.storage.model.feed.*
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter.Companion.unfiltered
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.Companion.TAG_ROOT
import ac.mdiq.podcini.util.FeedItemPermutors.getPermutor
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.LongList
import ac.mdiq.podcini.util.comparator.DownloadResultComparator
import ac.mdiq.podcini.util.comparator.PlaybackCompletionDateComparator
@ -134,7 +135,7 @@ object DBReader {
* @param items The FeedItems whose Feed-objects should be loaded.
*/
private fun loadFeedDataOfFeedItemList(items: List<FeedItem>) {
Log.d(TAG, "loadFeedDataOfFeedItemList called")
Logd(TAG, "loadFeedDataOfFeedItemList called")
val feedIndex: MutableMap<Long, Feed> = ArrayMap(feeds.size)
val feedsCopy = ArrayList(feeds)
for (feed in feedsCopy) {
@ -228,7 +229,7 @@ object DBReader {
@JvmStatic
fun getQueue(adapter: PodDBAdapter?): List<FeedItem> {
Log.d(TAG, "getQueue(adapter)")
Logd(TAG, "getQueue(adapter)")
adapter?.queueCursor.use { cursor ->
val items = extractItemlistFromCursor(adapter, cursor)
loadAdditionalFeedItemListData(items)
@ -238,7 +239,7 @@ object DBReader {
@JvmStatic
fun getQueueIDList(): LongList {
Log.d(TAG, "getQueueIDList() called")
Logd(TAG, "getQueueIDList() called")
// printStackTrace()
val adapter = getInstance()
@ -263,7 +264,7 @@ object DBReader {
@JvmStatic
fun getQueue(): List<FeedItem> {
Log.d(TAG, "getQueue() called")
Logd(TAG, "getQueue() called")
val adapter = getInstance()
adapter.open()
@ -275,7 +276,7 @@ object DBReader {
}
private fun getFavoriteIDList(): LongList {
Log.d(TAG, "getFavoriteIDList() called")
Logd(TAG, "getFavoriteIDList() called")
val adapter = getInstance()
adapter.open()
@ -300,7 +301,7 @@ object DBReader {
*/
@JvmStatic
fun getEpisodes(offset: Int, limit: Int, filter: FeedItemFilter?, sortOrder: SortOrder?): List<FeedItem> {
Log.d(TAG, "getEpisodes called with: offset=$offset, limit=$limit")
Logd(TAG, "getEpisodes called with: offset=$offset, limit=$limit")
val adapter = getInstance()
adapter.open()
try {
@ -316,7 +317,7 @@ object DBReader {
@JvmStatic
fun getTotalEpisodeCount(filter: FeedItemFilter?): Int {
Log.d(TAG, "getTotalEpisodeCount called")
Logd(TAG, "getTotalEpisodeCount called")
val adapter = getInstance()
adapter.open()
try {
@ -353,7 +354,7 @@ object DBReader {
*/
@JvmStatic
fun getPlaybackHistory(offset: Int, limit: Int): List<FeedItem> {
Log.d(TAG, "getPlaybackHistory() called")
Logd(TAG, "getPlaybackHistory() called")
val adapter = getInstance()
adapter.open()
@ -395,7 +396,7 @@ object DBReader {
@JvmStatic
fun getDownloadLog(): List<DownloadResult> {
Log.d(TAG, "getDownloadLog() called")
Logd(TAG, "getDownloadLog() called")
val adapter = getInstance()
adapter.open()
@ -421,7 +422,7 @@ object DBReader {
* newest events first.
*/
fun getFeedDownloadLog(feedId: Long): List<DownloadResult> {
Log.d(TAG, "getFeedDownloadLog() called with: feed = [$feedId]")
Logd(TAG, "getFeedDownloadLog() called with: feed = [$feedId]")
val adapter = getInstance()
adapter.open()
@ -460,7 +461,7 @@ object DBReader {
* database and the items-attribute will be set correctly.
*/
fun getFeed(feedId: Long, filtered: Boolean): Feed? {
Log.d(TAG, "getFeed() called with: feedId = [$feedId]")
Logd(TAG, "getFeed() called with: feedId = [$feedId]")
val adapter = getInstance()
adapter.open()
try {
@ -481,7 +482,7 @@ object DBReader {
}
private fun getFeedItem(itemId: Long, adapter: PodDBAdapter?): FeedItem? {
Log.d(TAG, "Loading feeditem with id $itemId")
Logd(TAG, "Loading feeditem with id $itemId")
var item: FeedItem? = null
adapter?.getFeedItemCursor(itemId.toString())?.use { cursor ->
@ -506,7 +507,7 @@ object DBReader {
*/
@JvmStatic
fun getFeedItem(itemId: Long): FeedItem? {
Log.d(TAG, "getFeedItem() called with: itemId = [$itemId]")
Logd(TAG, "getFeedItem() called with: itemId = [$itemId]")
val adapter = getInstance()
adapter.open()
@ -524,7 +525,7 @@ object DBReader {
* @return The FeedItem next in queue or null if the FeedItem could not be found.
*/
fun getNextInQueue(item: FeedItem): FeedItem? {
Log.d(TAG, "getNextInQueue() called with: itemId = [${item.id}]")
Logd(TAG, "getNextInQueue() called with: itemId = [${item.id}]")
val adapter = getInstance()
adapter.open()
try {
@ -539,7 +540,7 @@ object DBReader {
return nextItem
}
} catch (e: Exception) {
Log.d(TAG, "getNextInQueue error: ${e.message}")
Logd(TAG, "getNextInQueue error: ${e.message}")
return null
}
} finally {
@ -548,7 +549,7 @@ object DBReader {
}
fun getPausedQueue(limit: Int): List<FeedItem> {
Log.d(TAG, "getPausedQueue() called ")
Logd(TAG, "getPausedQueue() called ")
val adapter = getInstance()
adapter.open()
try {
@ -587,7 +588,7 @@ object DBReader {
* @return Credentials in format "Username:Password", empty String if no authorization given
*/
fun getImageAuthentication(imageUrl: String): String {
Log.d(TAG, "getImageAuthentication() called with: imageUrl = [$imageUrl]")
Logd(TAG, "getImageAuthentication() called with: imageUrl = [$imageUrl]")
val adapter = getInstance()
adapter.open()
try {
@ -619,7 +620,7 @@ object DBReader {
*/
@JvmStatic
fun getFeedItemByGuidOrEpisodeUrl(guid: String?, episodeUrl: String): FeedItem? {
Log.d(TAG, "getFeedItemByGuidOrEpisodeUrl called")
Logd(TAG, "getFeedItemByGuidOrEpisodeUrl called")
val adapter = getInstance()
adapter.open()
try {
@ -635,7 +636,7 @@ object DBReader {
* @param item The FeedItem
*/
fun loadTextDetailsOfFeedItem(item: FeedItem) {
Log.d(TAG, "loadTextOfFeedItem() called with: item = [$item]")
Logd(TAG, "loadTextOfFeedItem() called with: item = [$item]")
// TODO: need to find out who are often calling this
// printStackTrace()
val adapter = getInstance()
@ -665,7 +666,7 @@ object DBReader {
*/
@JvmStatic
fun loadChaptersOfFeedItem(item: FeedItem): List<Chapter>? {
Log.d(TAG, "loadChaptersOfFeedItem() called with: item = [${item.title}]")
Logd(TAG, "loadChaptersOfFeedItem() called with: item = [${item.title}]")
// TODO: need to find out who are often calling this
// val stackTraceElements = Thread.currentThread().stackTrace
// stackTraceElements.forEach { element ->
@ -704,7 +705,7 @@ object DBReader {
*/
@JvmStatic
fun getFeedMedia(mediaId: Long): FeedMedia? {
Log.d(TAG, "getFeedMedia called")
Logd(TAG, "getFeedMedia called")
val adapter = getInstance()
adapter.open()
try {
@ -727,7 +728,7 @@ object DBReader {
}
fun getFeedItemsWithUrl(urls: List<String?>?): List<FeedItem> {
Log.d(TAG, "getFeedItemsWithUrl() called ")
Logd(TAG, "getFeedItemsWithUrl() called ")
val adapter = getInstance()
adapter.open()
try {
@ -822,7 +823,7 @@ object DBReader {
*/
@JvmStatic
fun getNavDrawerData(subscriptionsFilter: SubscriptionsFilter?): NavDrawerData {
Log.d(TAG, "getNavDrawerData() called with: " + "")
Logd(TAG, "getNavDrawerData() called with: " + "")
val adapter = getInstance()
adapter.open()

View File

@ -151,7 +151,7 @@ import java.util.concurrent.*
* Get a FeedItem by its identifying value.
*/
private fun searchFeedItemByIdentifyingValue(items: List<FeedItem>?, searchItem: FeedItem): FeedItem? {
if (items == null) return null
if (items.isNullOrEmpty()) return null
for (item in items) {
if (TextUtils.equals(item.identifyingValue, searchItem.identifyingValue)) return item
}
@ -205,7 +205,6 @@ import java.util.concurrent.*
resultFeed = newFeed
} else {
Log.d(TAG, "Feed with title " + newFeed.title + " already exists. Syncing new with existing one.")
newFeed.items.sortWith(FeedItemPubdateComparator())
if (newFeed.pageNr == savedFeed.pageNr) {

View File

@ -81,6 +81,15 @@ import java.util.concurrent.TimeUnit
}
}
fun deleteItemsMedia(items: List<FeedItem>) {
runOnDbThread {
val adapter = getInstance()
adapter.open()
adapter.removeItemMedia(items)
adapter.close()
}
}
/**
* Deletes a downloaded FeedMedia file from the storage device.
*
@ -767,7 +776,7 @@ import java.util.concurrent.TimeUnit
* @param media The FeedMedia object.
*/
fun persistFeedMedia(media: FeedMedia): Future<*> {
Log.d(TAG, "setFeedMedia called")
Log.d(TAG, "persistFeedMedia called")
return runOnDbThread {
val adapter = getInstance()
adapter.open()
@ -782,8 +791,8 @@ import java.util.concurrent.TimeUnit
* @param media The FeedMedia object.
*/
@JvmStatic
fun persistFeedMediaPlaybackInformation(media: FeedMedia?): Future<*> {
Log.d(TAG, "setFeedMediaPlaybackInformation called")
fun persistFeedMediaPlaybackInfo(media: FeedMedia?): Future<*> {
Log.d(TAG, "persistFeedMediaPlaybackInfo called")
return runOnDbThread {
if (media != null) {
val adapter = getInstance()
@ -912,7 +921,7 @@ import java.util.concurrent.TimeUnit
* @param filterValues Values that represent properties to filter by
*/
fun persistFeedItemsFilter(feedId: Long, filterValues: Set<String>): Future<*> {
Log.d(TAG, "setFeedItemsFilter() called with: feedId = [$feedId], filterValues = [$filterValues]")
Log.d(TAG, "persistFeedItemsFilter() called with: feedId = [$feedId], filterValues = [$filterValues]")
return runOnDbThread {
val adapter = getInstance()
adapter.open()

View File

@ -1152,7 +1152,7 @@ class PodDBAdapter private constructor() {
private const val JOIN_FEED_ITEM_AND_MEDIA = (" LEFT JOIN $TABLE_NAME_FEED_MEDIA ON $TABLE_NAME_FEED_ITEMS.$KEY_ID=$TABLE_NAME_FEED_MEDIA.$KEY_FEEDITEM ")
private const val SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION = ("SELECT $KEYS_FEED_ITEM_WITHOUT_DESCRIPTION, $KEYS_FEED_MEDIA, $TABLE_NAME_FEED_ITEMS.$KEY_DESCRIPTION.$KEY_TRANSCRIPT FROM $TABLE_NAME_FEED_ITEMS$JOIN_FEED_ITEM_AND_MEDIA")
private const val SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION = ("SELECT $KEYS_FEED_ITEM_WITHOUT_DESCRIPTION, $KEYS_FEED_MEDIA, $TABLE_NAME_FEED_ITEMS.$KEY_DESCRIPTION, $TABLE_NAME_FEED_ITEMS.$KEY_TRANSCRIPT FROM $TABLE_NAME_FEED_ITEMS$JOIN_FEED_ITEM_AND_MEDIA")
private const val SELECT_FEED_ITEMS_AND_MEDIA = ("SELECT $KEYS_FEED_ITEM_WITHOUT_DESCRIPTION, $KEYS_FEED_MEDIA FROM $TABLE_NAME_FEED_ITEMS$JOIN_FEED_ITEM_AND_MEDIA")
private lateinit var context: Context

View File

@ -1,8 +1,8 @@
package ac.mdiq.podcini.storage.export.opml
import android.util.Log
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.storage.export.CommonSymbols
import ac.mdiq.podcini.util.Logd
import android.util.Log
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
import org.xmlpull.v1.XmlPullParserFactory
@ -33,14 +33,14 @@ class OpmlReader {
while (eventType != XmlPullParser.END_DOCUMENT) {
when (eventType) {
XmlPullParser.START_DOCUMENT -> if (BuildConfig.DEBUG) Log.d(TAG, "Reached beginning of document")
XmlPullParser.START_DOCUMENT -> Logd(TAG, "Reached beginning of document")
XmlPullParser.START_TAG -> when {
xpp.name == OpmlSymbols.OPML -> {
isInOpml = true
if (BuildConfig.DEBUG) Log.d(TAG, "Reached beginning of OPML tree.")
Logd(TAG, "Reached beginning of OPML tree.")
}
isInOpml && xpp.name == OpmlSymbols.OUTLINE -> {
if (BuildConfig.DEBUG) Log.d(TAG, "Found new Opml element")
Logd(TAG, "Found new Opml element")
val element = OpmlElement()
val title = xpp.getAttributeValue(null, CommonSymbols.TITLE)
@ -61,7 +61,7 @@ class OpmlReader {
}
elementList!!.add(element)
} else {
if (BuildConfig.DEBUG) Log.d(TAG, "Skipping element because of missing xml url")
Logd(TAG, "Skipping element because of missing xml url")
}
}
}
@ -69,7 +69,7 @@ class OpmlReader {
eventType = xpp.next()
}
if (BuildConfig.DEBUG) Log.d(TAG, "Parsing finished.")
Logd(TAG, "Parsing finished.")
return elementList!!
}

View File

@ -118,8 +118,7 @@ class Feed : FeedFile {
description: String?, paymentLinks: String?, author: String?, language: String?,
type: String?, feedIdentifier: String?, imageUrl: String?, fileUrl: String?,
downloadUrl: String?, downloaded: Boolean, paged: Boolean, nextPageLink: String?,
filter: String?, sortOrder: SortOrder?, lastUpdateFailed: Boolean
) : super(fileUrl, downloadUrl, downloaded) {
filter: String?, sortOrder: SortOrder?, lastUpdateFailed: Boolean) : super(fileUrl, downloadUrl, downloaded) {
this.id = id
this.feedTitle = title
this.customTitle = customTitle

View File

@ -1,5 +1,10 @@
package ac.mdiq.podcini.storage.model.feed
import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.storage.model.playback.RemoteMedia
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
@ -7,11 +12,6 @@ import android.os.Parcel
import android.os.Parcelable
import android.support.v4.media.MediaBrowserCompat
import android.support.v4.media.MediaDescriptionCompat
import ac.mdiq.podcini.storage.model.MediaMetadataRetrieverCompat
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.storage.model.playback.RemoteMedia
import android.util.Log
import java.util.*
import kotlin.concurrent.Volatile
import kotlin.math.max
@ -276,7 +276,7 @@ class FeedMedia : FeedFile, Playable {
}
override fun onPlaybackPause(context: Context) {
Log.d("FeedMedia", "onPlaybackPause $position $duration")
Logd("FeedMedia", "onPlaybackPause $position $duration")
if (position > startPosition) {
playedDuration = playedDurationWhenStarted + position - startPosition
playedDurationWhenStarted = playedDuration

View File

@ -1,13 +1,13 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import ac.mdiq.podcini.R
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.PlaybackStatus.isCurrentlyPlaying
import android.content.Context
import android.view.KeyEvent
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
import ac.mdiq.podcini.util.PlaybackStatus.isCurrentlyPlaying
import ac.mdiq.podcini.storage.model.feed.FeedItem
import android.util.Log
class PauseActionButton(item: FeedItem) : ItemActionButton(item) {
override fun getLabel(): Int {
@ -17,7 +17,7 @@ class PauseActionButton(item: FeedItem) : ItemActionButton(item) {
return R.drawable.ic_pause
}
@UnstableApi override fun onClick(context: Context) {
Log.d("PauseActionButton", "onClick called")
Logd("PauseActionButton", "onClick called")
val media = item.media ?: return
if (isCurrentlyPlaying(media)) context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))

View File

@ -1,16 +1,16 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import android.content.Context
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.DBTasks
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.storage.DBTasks
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.playback.MediaType
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.playback.StartPlayEvent
import android.util.Log
import android.content.Context
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
import org.greenrobot.eventbus.EventBus
class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
@ -21,7 +21,7 @@ class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
return R.drawable.ic_play_24dp
}
@UnstableApi override fun onClick(context: Context) {
Log.d("PlayActionButton", "onClick called")
Logd("PlayActionButton", "onClick called")
val media = item.media
if (media == null) {
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()

View File

@ -1,14 +1,14 @@
package ac.mdiq.podcini.ui.actions.actionbutton
import android.content.Context
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.PlaybackServiceStarter
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.playback.MediaType
import android.util.Log
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.widget.Toast
import androidx.media3.common.util.UnstableApi
class PlayLocalActionButton(item: FeedItem) : ItemActionButton(item) {
override fun getLabel(): Int {
@ -18,7 +18,7 @@ class PlayLocalActionButton(item: FeedItem) : ItemActionButton(item) {
return R.drawable.ic_play_24dp
}
@UnstableApi override fun onClick(context: Context) {
Log.d("PlayLocalActionButton", "onClick called")
Logd("PlayLocalActionButton", "onClick called")
val media = item.media
if (media == null) {
Toast.makeText(context, R.string.no_media_label, Toast.LENGTH_LONG).show()

View File

@ -2,6 +2,8 @@ package ac.mdiq.podcini.ui.actions.menuhandler
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.SynchronizationSettings.isProviderConnected
import ac.mdiq.podcini.net.sync.SynchronizationSettings.wifiSyncEnabledKey
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
import ac.mdiq.podcini.preferences.PlaybackPreferences
@ -139,7 +141,7 @@ object FeedItemMenuHandler {
R.id.mark_read_item -> {
selectedItem.setPlayed(true)
DBWriter.markItemPlayed(selectedItem, FeedItem.PLAYED, true)
if (selectedItem.feed?.isLocalFeed != true && SynchronizationSettings.isProviderConnected) {
if (selectedItem.feed?.isLocalFeed != true && (isProviderConnected || wifiSyncEnabledKey)) {
val media: FeedMedia? = selectedItem.media
// not all items have media, Gpodder only cares about those that do
if (media != null) {

View File

@ -1,5 +1,6 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.MainActivityBinding
import ac.mdiq.podcini.net.download.FeedUpdateManager
@ -18,11 +19,12 @@ import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.model.download.DownloadStatus
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr
import ac.mdiq.podcini.ui.dialog.RatingDialog
import ac.mdiq.podcini.ui.fragment.*
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr
import ac.mdiq.podcini.ui.view.LockableBottomSheetBehavior
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
import ac.mdiq.podcini.util.event.FeedUpdateRunningEvent
import ac.mdiq.podcini.util.event.MessageEvent
@ -38,6 +40,7 @@ import android.media.AudioManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.StrictMode
import android.util.DisplayMetrics
import android.util.Log
import android.view.KeyEvent
@ -60,7 +63,6 @@ import androidx.media3.session.SessionToken
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.bumptech.glide.Glide
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
@ -84,6 +86,7 @@ class MainActivity : CastEnabledActivity() {
private val binding get() = _binding!!
private lateinit var mainView: View
private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerFragmentView: View
private lateinit var navDrawer: View
private lateinit var dummyView : View
@ -101,6 +104,13 @@ class MainActivity : CastEnabledActivity() {
lastTheme = getNoTitleTheme(this)
setTheme(lastTheme)
if (BuildConfig.DEBUG) {
val builder = StrictMode.ThreadPolicy.Builder()
.detectAll() // Enable all detections
.penaltyLog() // Log violations to the console
StrictMode.setThreadPolicy(builder.build())
}
DBReader.updateFeedList()
if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0))
@ -158,7 +168,7 @@ class MainActivity : CastEnabledActivity() {
val transaction = fm.beginTransaction()
val navDrawerFragment = NavDrawerFragment()
transaction.replace(R.id.navDrawerFragment, navDrawerFragment, NavDrawerFragment.TAG)
val audioPlayerFragment = AudioPlayerFragment()
audioPlayerFragment = AudioPlayerFragment()
transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG)
transaction.commit()
navDrawer = findViewById(R.id.navDrawerFragment)
@ -210,7 +220,7 @@ class MainActivity : CastEnabledActivity() {
DownloadStatus.STATE_COMPLETED
}
WorkInfo.State.CANCELLED -> {
Log.d(TAG, "download cancelled $downloadUrl")
Logd(TAG, "download cancelled $downloadUrl")
DownloadStatus.STATE_COMPLETED
}
}
@ -265,10 +275,13 @@ class MainActivity : CastEnabledActivity() {
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
override fun onStateChanged(view: View, state: Int) {
Log.d(TAG, "bottomSheet onStateChanged $state")
Logd(TAG, "bottomSheet onStateChanged $state")
when (state) {
BottomSheetBehavior.STATE_COLLAPSED -> onSlide(view,0.0f)
BottomSheetBehavior.STATE_EXPANDED -> onSlide(view, 1.0f)
BottomSheetBehavior.STATE_EXPANDED -> {
audioPlayerFragment.initDetailedView()
onSlide(view, 1.0f)
}
else -> {}
}
}
@ -282,7 +295,7 @@ class MainActivity : CastEnabledActivity() {
}
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
Log.d(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
// Tablet layout does not have a drawer
when {
drawerLayout != null -> {
@ -352,7 +365,7 @@ class MainActivity : CastEnabledActivity() {
fun loadFragment(tag: String?, args: Bundle?) {
var tag = tag
var args = args
Log.d(TAG, "loadFragment(tag: $tag, args: $args)")
Logd(TAG, "loadFragment(tag: $tag, args: $args)")
val fragment: Fragment
when (tag) {
QueueFragment.TAG -> fragment = QueueFragment()
@ -447,7 +460,7 @@ class MainActivity : CastEnabledActivity() {
val maxWidth = resources.getDimension(R.dimen.nav_drawer_max_screen_size).toInt()
navDrawer.layoutParams.width = min(width.toDouble(), maxWidth.toDouble()).toInt()
Log.d(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
}
private val screenWidth: Int
@ -503,16 +516,16 @@ class MainActivity : CastEnabledActivity() {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).trimMemory(level)
// Glide.get(this).trimMemory(level)
}
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).clearMemory()
// Glide.get(this).clearMemory()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
Log.d(TAG, "onOptionsItemSelected ${item.title}")
Logd(TAG, "onOptionsItemSelected ${item.title}")
when {
drawerToggle != null && drawerToggle!!.onOptionsItemSelected(item) -> return true // Tablet layout does not have a drawer
item.itemId == android.R.id.home -> {
@ -541,14 +554,14 @@ class MainActivity : CastEnabledActivity() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: MessageEvent) {
Log.d(TAG, "onEvent($event)")
Logd(TAG, "onEvent($event)")
val snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG)
if (event.action != null) snackbar.setAction(event.actionText) { event.action.accept(this) }
}
private fun handleNavIntent() {
Log.d(TAG, "handleNavIntent()")
Logd(TAG, "handleNavIntent()")
val intent = intent
when {
intent.hasExtra(EXTRA_FEED_ID) -> {
@ -623,7 +636,7 @@ class MainActivity : CastEnabledActivity() {
private fun handleDeeplink(uri: Uri?) {
if (uri?.path == null) return
Log.d(TAG, "Handling deeplink: $uri")
Logd(TAG, "Handling deeplink: $uri")
when (uri.path) {
"/deeplink/search" -> {
val query = uri.getQueryParameter("query") ?: return

View File

@ -1,6 +1,7 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.util.Logd
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
@ -36,7 +37,7 @@ class OnlineFeedViewActivity : AppCompatActivity() {
Log.e(TAG, "feedUrl is null.")
showNoPodcastFoundError()
} else {
Log.d(TAG, "Activity was started with url $feedUrl")
Logd(TAG, "Activity was started with url $feedUrl")
val intent = MainActivity.showOnlineFeed(this, feedUrl)
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false))

View File

@ -1,11 +1,19 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.OpmlSelectionBinding
import ac.mdiq.podcini.net.download.FeedUpdateManager.runOnce
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.storage.DBTasks
import ac.mdiq.podcini.storage.export.opml.OpmlElement
import ac.mdiq.podcini.storage.export.opml.OpmlReader
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import android.Manifest
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.Spannable
import android.text.SpannableString
@ -24,14 +32,6 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.media3.common.util.UnstableApi
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.export.opml.OpmlElement
import ac.mdiq.podcini.storage.export.opml.OpmlReader
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.storage.DBTasks
import ac.mdiq.podcini.net.download.FeedUpdateManager.runOnce
import ac.mdiq.podcini.databinding.OpmlSelectionBinding
import ac.mdiq.podcini.storage.model.feed.Feed
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
@ -116,9 +116,8 @@ class OpmlImportActivity : AppCompatActivity() {
}
var uri = intent.data
if (uri != null && uri.toString().startsWith("/")) {
uri = Uri.parse("file://$uri")
} else {
if (uri != null && uri.toString().startsWith("/")) uri = Uri.parse("file://$uri")
else {
val extraText = intent.getStringExtra(Intent.EXTRA_TEXT)
if (extraText != null) uri = Uri.parse(extraText)
}
@ -190,9 +189,8 @@ class OpmlImportActivity : AppCompatActivity() {
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
startImport()
} else {
if (isGranted) startImport()
else {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.opml_import_ask_read_permission)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
@ -221,12 +219,12 @@ class OpmlImportActivity : AppCompatActivity() {
.subscribe(
{ result: ArrayList<OpmlElement>? ->
binding.progressBar.visibility = View.GONE
Log.d(TAG, "Parsing was successful")
Logd(TAG, "Parsing was successful")
readElements = result
listAdapter = ArrayAdapter(this@OpmlImportActivity, android.R.layout.simple_list_item_multiple_choice, titleList)
binding.feedlist.adapter = listAdapter
}, { e: Throwable ->
Log.d(TAG, Log.getStackTraceString(e))
Logd(TAG, Log.getStackTraceString(e))
val message = if (e.message == null) "" else e.message!!
if (message.lowercase().contains("permission")) {
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)

View File

@ -5,13 +5,13 @@ import ac.mdiq.podcini.databinding.SettingsActivityBinding
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.preferences.fragments.*
import ac.mdiq.podcini.preferences.fragments.synchronization.SynchronizationPreferencesFragment
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.MessageEvent
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
@ -158,7 +158,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: MessageEvent) {
Log.d(FRAGMENT_TAG, "onEvent($event)")
Logd(FRAGMENT_TAG, "onEvent($event)")
val s = Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG)
if (event.action != null) {
s.setAction(event.actionText) { event.action.accept(this) }

View File

@ -1,33 +1,33 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.MainActivity.Companion.EXTRA_FEED_ID
import ac.mdiq.podcini.databinding.SubscriptionSelectionActivityBinding
import ac.mdiq.podcini.preferences.ThemeSwitcher
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.NavDrawerData
import ac.mdiq.podcini.databinding.SubscriptionSelectionActivityBinding
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.ui.activity.MainActivity.Companion.EXTRA_FEED_ID
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ListView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.SuccessResult
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -100,21 +100,36 @@ class SelectSubscriptionActivity : AppCompatActivity() {
private fun getBitmapFromUrl(feed: Feed) {
val iconSize = (128 * resources.displayMetrics.density).toInt()
if (!feed.imageUrl.isNullOrBlank()) Glide.with(this)
.asBitmap()
.load(feed.imageUrl)
.apply(RequestOptions.overrideOf(iconSize, iconSize))
.listener(object : RequestListener<Bitmap?> {
@UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap?>, isFirstResource: Boolean): Boolean {
addShortcut(feed, null)
return true
}
@UnstableApi override fun onResourceReady(resource: Bitmap, model: Any, target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
addShortcut(feed, resource)
return true
// if (!feed.imageUrl.isNullOrBlank()) Glide.with(this)
// .asBitmap()
// .load(feed.imageUrl)
// .apply(RequestOptions.overrideOf(iconSize, iconSize))
// .listener(object : RequestListener<Bitmap?> {
// @UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Bitmap?>, isFirstResource: Boolean): Boolean {
// addShortcut(feed, null)
// return true
// }
// @UnstableApi override fun onResourceReady(resource: Bitmap, model: Any, target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean): Boolean {
// addShortcut(feed, resource)
// return true
// }
// }).submit()
val request = ImageRequest.Builder(this)
.data(feed.imageUrl)
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
@OptIn(UnstableApi::class) override fun onError(request: ImageRequest, throwable: ErrorResult) {
addShortcut(feed, null)
}
}).submit()
@OptIn(UnstableApi::class) override fun onSuccess(request: ImageRequest, result: SuccessResult) {
addShortcut(feed, (result.drawable as BitmapDrawable).bitmap)
}
})
.size(iconSize, iconSize)
.build()
imageLoader.enqueue(request)
}
private fun loadSubscriptions() {

View File

@ -2,6 +2,8 @@ package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
import ac.mdiq.podcini.playback.PlaybackController.Companion.audioTracks
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
@ -16,6 +18,7 @@ import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
import ac.mdiq.podcini.util.FeedItemUtil.getLinkWithFallback
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
import ac.mdiq.podcini.util.event.MessageEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
@ -34,7 +37,6 @@ import android.view.*
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.widget.EditText
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
@ -151,12 +153,12 @@ class VideoplayerActivity : CastEnabledActivity() {
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
Glide.get(this).trimMemory(level)
// Glide.get(this).trimMemory(level)
}
override fun onLowMemory() {
super.onLowMemory()
Glide.get(this).clearMemory()
// Glide.get(this).clearMemory()
}
fun toggleViews() {
@ -184,7 +186,7 @@ class VideoplayerActivity : CastEnabledActivity() {
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(event: MessageEvent) {
Log.d(TAG, "onEvent($event)")
Logd(TAG, "onEvent($event)")
val errorDialog = MaterialAlertDialogBuilder(this)
errorDialog.setMessage(event.message)
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
@ -226,11 +228,11 @@ class VideoplayerActivity : CastEnabledActivity() {
menu.findItem(R.id.remove_from_favorites_item).setVisible(videoEpisodeFragment.isFavorite)
}
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller.sleepTimerActive())
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller.sleepTimerActive())
menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
menu.findItem(R.id.audio_controls).setVisible(controller.audioTracks.size >= 2)
menu.findItem(R.id.audio_controls).setVisible(audioTracks.size >= 2)
menu.findItem(R.id.playback_speed).setVisible(true)
menu.findItem(R.id.player_show_chapters).setVisible(true)

View File

@ -1,5 +1,15 @@
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SimplechapterItemBinding
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.adapter.ChaptersListAdapter.ChapterHolder
import ac.mdiq.podcini.ui.view.CircularProgressBar
import ac.mdiq.podcini.util.Converter.getDurationStringLocalized
import ac.mdiq.podcini.util.Converter.getDurationStringLong
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@ -8,21 +18,10 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import coil.ImageLoader
import coil.load
import coil.request.ImageRequest
import com.google.android.material.elevation.SurfaceColors
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SimplechapterItemBinding
import ac.mdiq.podcini.ui.adapter.ChaptersListAdapter.ChapterHolder
import ac.mdiq.podcini.util.Converter.getDurationStringLocalized
import ac.mdiq.podcini.util.Converter.getDurationStringLong
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.storage.model.feed.Chapter
import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage
import ac.mdiq.podcini.storage.model.playback.Playable
import ac.mdiq.podcini.ui.view.CircularProgressBar
import kotlin.math.max
import kotlin.math.min
@ -83,16 +82,19 @@ class ChaptersListAdapter(private val context: Context, private val callback: Ca
if (hasImages) {
holder.image.visibility = View.VISIBLE
if (sc.imageUrl.isNullOrEmpty()) {
Glide.with(context).clear(holder.image)
// Glide.with(context).clear(holder.image)
val imageLoader = ImageLoader.Builder(context).build()
imageLoader.enqueue(ImageRequest.Builder(context).data(null).target(holder.image).build())
} else {
if (media != null) {
val imgUrl = EmbeddedChapterImage.getModelFor(media!!,position)
if (imgUrl != null) Glide.with(context)
.load(imgUrl)
.apply(RequestOptions()
.dontAnimate()
.transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
.into(holder.image)
// if (imgUrl != null) Glide.with(context)
// .load(imgUrl)
// .apply(RequestOptions()
// .dontAnimate()
// .transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
// .into(holder.image)
holder.image.load(imgUrl)
}
}
} else holder.image.visibility = View.GONE

View File

@ -1,18 +1,19 @@
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.MainActivity
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomViewTarget
import com.bumptech.glide.request.transition.Transition
import coil.ImageLoader
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.target.Target
import java.lang.ref.WeakReference
class CoverLoader(activity: MainActivity) {
class CoverLoader(private val activity: MainActivity) {
private var resource = 0
private var uri: String? = null
private var fallbackUri: String? = null
@ -59,55 +60,107 @@ class CoverLoader(activity: MainActivity) {
fun load() {
if (imgvCover == null) return
val coverTarget = CoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
// val coverTarget = CoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
val coverTargetCoil = CoilCoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
if (resource != 0) {
Glide.with(imgvCover!!).clear(coverTarget)
// Glide.with(imgvCover!!).clear(coverTarget)
val imageLoader = ImageLoader.Builder(activity).build()
imageLoader.enqueue(ImageRequest.Builder(activity).data(null).target(coverTargetCoil).build())
imgvCover!!.setImageResource(resource)
CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined)
// CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined)
CoilCoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined)
return
}
val options: RequestOptions = RequestOptions()
.fitCenter()
.dontAnimate()
// val options: RequestOptions = RequestOptions()
// .fitCenter()
// .dontAnimate()
//
// var builder: RequestBuilder<Drawable?> = Glide.with(imgvCover!!)
// .`as`(Drawable::class.java)
// .load(uri)
// .apply(options)
//
// if (!fallbackUri.isNullOrBlank()) {
// builder = builder.error(Glide.with(imgvCover!!)
// .`as`(Drawable::class.java)
// .load(fallbackUri)
// .apply(options))
// }
//
// builder.into<CoverTarget>(coverTarget)
var builder: RequestBuilder<Drawable?> = Glide.with(imgvCover!!)
.`as`(Drawable::class.java)
.load(uri)
.apply(options)
val request = ImageRequest.Builder(activity)
.data(uri)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(activity)
.data(fallbackUri)
.error(R.mipmap.ic_launcher)
.target(coverTargetCoil)
.build()
activity.imageLoader.enqueue(fallbackImageRequest)
}
})
.target(coverTargetCoil)
.build()
activity.imageLoader.enqueue(request)
if (!fallbackUri.isNullOrBlank()) {
builder = builder.error(Glide.with(imgvCover!!)
.`as`(Drawable::class.java)
.load(fallbackUri)
.apply(options))
}
builder.into<CoverTarget>(coverTarget)
}
internal class CoverTarget(fallbackTitle: TextView?, coverImage: ImageView, private val textAndImageCombined: Boolean)
: CustomViewTarget<ImageView, Drawable>(coverImage) {
// internal class CoverTarget(fallbackTitle: TextView?, coverImage: ImageView, private val textAndImageCombined: Boolean)
// : CustomViewTarget<ImageView, Drawable>(coverImage) {
//
// private val fallbackTitle: WeakReference<TextView?> = WeakReference<TextView?>(fallbackTitle)
// private val cover: WeakReference<ImageView> = WeakReference(coverImage)
//
// override fun onLoadFailed(errorDrawable: Drawable?) {
// setTitleVisibility(fallbackTitle.get(), true)
// }
//
// override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable?>?) {
// val ivCover = cover.get()
// ivCover!!.setImageDrawable(resource)
// setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
// }
//
// override fun onResourceCleared(placeholder: Drawable?) {
// val ivCover = cover.get()
// ivCover!!.setImageDrawable(placeholder)
// setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
// }
//
// companion object {
// fun setTitleVisibility(fallbackTitle: TextView?, textAndImageCombined: Boolean) {
// fallbackTitle?.visibility = if (textAndImageCombined) View.VISIBLE else View.GONE
// }
// }
// }
internal class CoilCoverTarget(fallbackTitle: TextView?, coverImage: ImageView, private val textAndImageCombined: Boolean) : Target {
private val fallbackTitle: WeakReference<TextView?> = WeakReference<TextView?>(fallbackTitle)
private val cover: WeakReference<ImageView> = WeakReference(coverImage)
override fun onLoadFailed(errorDrawable: Drawable?) {
override fun onStart(placeholder: Drawable?) {
}
override fun onError(errorDrawable: Drawable?) {
setTitleVisibility(fallbackTitle.get(), true)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable?>?) {
override fun onSuccess(resource: Drawable) {
val ivCover = cover.get()
ivCover!!.setImageDrawable(resource)
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
}
override fun onResourceCleared(placeholder: Drawable?) {
val ivCover = cover.get()
ivCover!!.setImageDrawable(placeholder)
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
}
// override fun onResourceCleared(placeholder: Drawable?) {
// val ivCover = cover.get()
// ivCover!!.setImageDrawable(placeholder)
// setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
// }
companion object {
fun setTitleVisibility(fallbackTitle: TextView?, textAndImageCombined: Boolean) {
@ -115,4 +168,5 @@ class CoverLoader(activity: MainActivity) {
}
}
}
}

View File

@ -2,21 +2,19 @@ package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.fragment.EpisodeInfoFragment
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
import android.R.color
import android.app.Activity
import android.os.Build
import android.util.Log
import android.view.*
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import java.lang.ref.WeakReference
/**
* List adapter for the list of new episodes.
*/
@ -35,7 +33,8 @@ open class EpisodeItemListAdapter(mainActivity: MainActivity) :
fun setDummyViews(dummyViews: Int) {
this.dummyViews = dummyViews
notifyDataSetChanged()
// TODO: test : what is this?
// notifyDataSetChanged()
}
fun updateItems(items: List<FeedItem>) {
@ -63,7 +62,7 @@ open class EpisodeItemListAdapter(mainActivity: MainActivity) :
// Reset state of recycled views
holder.coverHolder.visibility = View.VISIBLE
holder.dragHandle.setVisibility(View.GONE)
holder.dragHandle.visibility = View.GONE
beforeBindViewHolder(holder, pos)
@ -83,7 +82,6 @@ open class EpisodeItemListAdapter(mainActivity: MainActivity) :
// val ids: LongArray = FeedItemUtil.getIds(episodes)
// val position = ArrayUtils.indexOf(ids, item.id)
activity?.loadChildFragment(EpisodeInfoFragment.newInstance(episodes[pos]))
Log.d("infoCard", "setOnClickListener starting EpisodeInfoFragment")
} else {
toggleSelection(holder.bindingAdapterPosition)
}
@ -159,7 +157,8 @@ open class EpisodeItemListAdapter(mainActivity: MainActivity) :
}
protected fun getItem(index: Int): FeedItem? {
return if (index in episodes.indices) episodes[index] else null
val item = if (index in episodes.indices) episodes[index] else null
return item
}
protected val activity: Activity?

View File

@ -1,17 +1,14 @@
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
import ac.mdiq.podcini.ui.activity.MainActivity
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
import coil.load
import java.lang.ref.WeakReference
class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
@ -51,14 +48,18 @@ class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
val podcast: PodcastSearchResult? = getItem(position)
holder.imageView!!.contentDescription = podcast?.title
if (!podcast?.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
.load(podcast?.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.transform(FitCenter(), RoundedCorners((8 * mainActivityRef.get()!!.resources.displayMetrics.density).toInt()))
.dontAnimate())
.into(holder.imageView!!)
// if (!podcast?.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
// .load(podcast?.imageUrl)
// .apply(RequestOptions()
// .placeholder(R.color.light_gray)
// .transform(FitCenter(), RoundedCorners((8 * mainActivityRef.get()!!.resources.displayMetrics.density).toInt()))
// .dontAnimate())
// .into(holder.imageView!!)
holder.imageView?.load(podcast?.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
return convertView!!
}

View File

@ -1,6 +1,11 @@
package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.HorizontalFeedItemBinding
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment
import ac.mdiq.podcini.ui.view.SquareImageView
import android.view.ContextMenu
import android.view.MenuInflater
import android.view.View
@ -10,13 +15,9 @@ import androidx.annotation.StringRes
import androidx.cardview.widget.CardView
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.HorizontalFeedItemBinding
import ac.mdiq.podcini.ui.fragment.FeedItemlistFragment
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.ui.view.SquareImageView
import coil.ImageLoader
import coil.load
import coil.request.ImageRequest
import java.lang.ref.WeakReference
open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
@ -58,7 +59,9 @@ open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
holder.actionButton.visibility = View.GONE
if (position >= data.size) {
holder.itemView.alpha = 0.1f
Glide.with(mainActivityRef.get()!!).clear(holder.imageView)
// Glide.with(mainActivityRef.get()!!).clear(holder.imageView)
val imageLoader = ImageLoader.Builder(mainActivityRef.get()!!).build()
imageLoader.enqueue(ImageRequest.Builder(mainActivityRef.get()!!).data(null).target(holder.imageView).build())
holder.imageView.setImageResource(R.color.medium_gray)
return
}
@ -77,13 +80,17 @@ open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
false
}
if (!podcast.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
.load(podcast.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.fitCenter()
.dontAnimate())
.into(holder.imageView)
// if (!podcast.imageUrl.isNullOrBlank()) Glide.with(mainActivityRef.get()!!)
// .load(podcast.imageUrl)
// .apply(RequestOptions()
// .placeholder(R.color.light_gray)
// .fitCenter()
// .dontAnimate())
// .into(holder.imageView)
holder.imageView.load(podcast.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
}
override fun getItemId(position: Int): Long {

View File

@ -17,7 +17,6 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.os.Build
import android.view.*
import android.view.ContextMenu.ContextMenuInfo
import android.view.View.OnCreateContextMenuListener
@ -30,10 +29,7 @@ import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import coil.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.apache.commons.lang3.ArrayUtils
import java.lang.ref.WeakReference
@ -258,15 +254,20 @@ class NavListAdapter(private val itemAccess: ItemAccess, context: Activity) :
val feed = drawerItem.feed
val context = activity.get() ?: return
if (!feed.imageUrl.isNullOrBlank()) Glide.with(context)
.load(feed.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.transform(FitCenter(),
RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
.dontAnimate())
.into(holder.image)
// if (!feed.imageUrl.isNullOrBlank()) Glide.with(context)
// .load(feed.imageUrl)
// .apply(RequestOptions()
// .placeholder(R.color.light_gray)
// .error(R.color.light_gray)
// .transform(FitCenter(),
// RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
// .dontAnimate())
// .into(holder.image)
holder.image.load(feed.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
if (feed.hasLastUpdateFailed()) {
val p = holder.title.layoutParams as RelativeLayout.LayoutParams

View File

@ -11,14 +11,10 @@ import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import coil.load
class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSearchResult>) :
ArrayAdapter<PodcastSearchResult?>(context, 0, objects) {
class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSearchResult>)
: ArrayAdapter<PodcastSearchResult?>(context, 0, objects) {
// List holding the podcasts found in the search
private val data: List<PodcastSearchResult> = objects
@ -63,16 +59,20 @@ class OnlineFeedsAdapter(private val context: Context, objects: List<PodcastSear
} else viewHolder.updateView.visibility = View.INVISIBLE
//Update the empty imageView with the image from the feed
if (!podcast.imageUrl.isNullOrBlank()) Glide.with(context)
.load(podcast.imageUrl)
.apply(RequestOptions()
.placeholder(R.color.light_gray)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transform(FitCenter(),
RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
.dontAnimate())
.into(viewHolder.coverView)
// if (!podcast.imageUrl.isNullOrBlank()) Glide.with(context)
// .load(podcast.imageUrl)
// .apply(RequestOptions()
// .placeholder(R.color.light_gray)
// .diskCacheStrategy(DiskCacheStrategy.NONE)
// .transform(FitCenter(),
// RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
// .dontAnimate())
// .into(viewHolder.coverView)
viewHolder.coverView.load(podcast.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
return view
}

View File

@ -6,6 +6,7 @@ import android.view.Menu
import android.view.MenuItem
import androidx.recyclerview.widget.RecyclerView
import ac.mdiq.podcini.R
import android.util.Log
/**
* Used by Recyclerviews that need to provide ability to select items.

View File

@ -6,9 +6,7 @@ import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import coil.load
/**
* Displays a list of items that have a subtitle and an icon.
@ -24,13 +22,14 @@ class SimpleIconListAdapter<T : SimpleIconListAdapter.ListItem>(private val cont
val binding = SimpleIconListItemBinding.bind(view!!)
binding.title.text = item.title
binding.subtitle.text = item.subtitle
if (item.imageUrl.isNotBlank()) Glide.with(context)
.load(item.imageUrl)
.apply(RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.fitCenter()
.dontAnimate())
.into(binding.icon)
// if (item.imageUrl.isNotBlank()) Glide.with(context)
// .load(item.imageUrl)
// .apply(RequestOptions()
// .diskCacheStrategy(DiskCacheStrategy.NONE)
// .fitCenter()
// .dontAnimate())
// .into(binding.icon)
binding.icon.load(item.imageUrl)
return view
}

View File

@ -24,9 +24,8 @@ import java.text.NumberFormat
/**
* Adapter for subscriptions
*/
open class SubscriptionsAdapter(mainActivity: MainActivity) :
SelectableAdapter<SubscriptionsAdapter.SubscriptionViewHolder?>(mainActivity),
View.OnCreateContextMenuListener {
open class SubscriptionsAdapter(mainActivity: MainActivity)
: SelectableAdapter<SubscriptionsAdapter.SubscriptionViewHolder?>(mainActivity), View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private var listItems: List<NavDrawerData.FeedDrawerItem>
@ -58,7 +57,7 @@ open class SubscriptionsAdapter(mainActivity: MainActivity) :
holder.selectCheckbox.visibility = View.VISIBLE
holder.selectView.visibility = View.VISIBLE
holder.selectCheckbox.setChecked((isSelected(position)))
holder.selectCheckbox.setChecked(isSelected(position))
holder.selectCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setSelected(holder.bindingAdapterPosition, isChecked)
}
@ -86,8 +85,7 @@ open class SubscriptionsAdapter(mainActivity: MainActivity) :
}
holder.itemView.setOnTouchListener { _: View?, e: MotionEvent ->
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
&& e.buttonState == MotionEvent.BUTTON_SECONDARY) {
if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) {
if (!inActionMode()) {
longPressedPosition = holder.bindingAdapterPosition
selectedItem = drawerItem

View File

@ -1,9 +1,9 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.content.DialogInterface
import android.util.Log
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -17,7 +17,7 @@ abstract class ConfirmationDialog(private val context: Context, private val titl
constructor(context: Context, titleId: Int, messageId: Int) : this(context, titleId, context.getString(messageId))
private fun onCancelButtonPressed(dialog: DialogInterface) {
Log.d(TAG, "Dialog was cancelled")
Logd(TAG, "Dialog was cancelled")
dialog.dismiss()
}

View File

@ -5,10 +5,10 @@ import ac.mdiq.podcini.databinding.FilterDialogBinding
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
import ac.mdiq.podcini.feed.FeedItemFilterGroup
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.util.Logd
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -30,7 +30,7 @@ abstract class ItemFilterDialog : BottomSheetDialogFragment() {
_binding = FilterDialogBinding.bind(layout)
rows = binding.filterRows
val filter = requireArguments().getSerializable(ARGUMENT_FILTER) as FeedItemFilter?
Log.d("ItemFilterDialog", "fragment onCreateView")
Logd("ItemFilterDialog", "fragment onCreateView")
//add filter rows
for (item in FeedItemFilterGroup.entries) {

View File

@ -3,6 +3,9 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.AudioControlsBinding
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.PlaybackController.Companion.audioTracks
import ac.mdiq.podcini.playback.PlaybackController.Companion.selectedAudioTrack
import ac.mdiq.podcini.playback.PlaybackController.Companion.setAudioTrack
import android.app.Dialog
import android.os.Bundle
import android.os.Handler
@ -51,8 +54,6 @@ class PlaybackControlsDialog : DialogFragment() {
}
@UnstableApi private fun setupAudioTracks() {
val audioTracks = controller!!.audioTracks
val selectedAudioTrack = controller!!.selectedAudioTrack
val butAudioTracks = binding.audioTracks
if (audioTracks.size < 2 || selectedAudioTrack < 0) {
butAudioTracks.visibility = View.GONE
@ -62,7 +63,7 @@ class PlaybackControlsDialog : DialogFragment() {
butAudioTracks.visibility = View.VISIBLE
butAudioTracks.text = audioTracks[selectedAudioTrack]
butAudioTracks.setOnClickListener {
controller!!.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
Handler(Looper.getMainLooper()).postDelayed({ this.setupAudioTracks() }, 500)
}
}

View File

@ -1,12 +1,13 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import android.app.ProgressDialog
import android.content.Context
import android.content.DialogInterface
import android.util.Log
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.Feed
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import io.reactivex.Completable
@ -49,7 +50,7 @@ object RemoveFeedDialog {
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
Log.d(TAG, "Feed(s) deleted")
Logd(TAG, "Feed(s) deleted")
progressDialog.dismiss()
}, { error: Throwable? ->
Log.e(TAG, Log.getStackTraceString(error))

View File

@ -5,9 +5,9 @@ import ac.mdiq.podcini.databinding.EditTextDialogBinding
import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.NavDrawerData.FeedDrawerItem
import ac.mdiq.podcini.storage.model.feed.Feed
import ac.mdiq.podcini.util.Logd
import android.app.Activity
import android.content.DialogInterface
import android.util.Log
import android.view.LayoutInflater
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
@ -70,6 +70,6 @@ class RenameItemDialog {
// DBWriter.setFeedPreferences(preferences)
// }
// }
Log.d("RenameDialog", "rename tag not needed here")
Logd("RenameDialog", "rename tag not needed here")
}
}

View File

@ -19,6 +19,9 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.vibrate
import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.util.Converter.getDurationStringLong
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.PlaybackController.Companion.disableSleepTimer
import ac.mdiq.podcini.playback.PlaybackController.Companion.extendSleepTimer
import ac.mdiq.podcini.playback.PlaybackController.Companion.setSleepTimer
import android.app.Activity
import android.app.Dialog
import android.content.Context
@ -83,13 +86,13 @@ class SleepTimerDialog : DialogFragment() {
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
extendSleepFiveMinutesButton.setOnClickListener {
controller.extendSleepTimer((5 * 1000 * 60).toLong())
extendSleepTimer((5 * 1000 * 60).toLong())
}
extendSleepTenMinutesButton.setOnClickListener {
controller.extendSleepTimer((10 * 1000 * 60).toLong())
extendSleepTimer((10 * 1000 * 60).toLong())
}
extendSleepTwentyMinutesButton.setOnClickListener {
controller.extendSleepTimer((20 * 1000 * 60).toLong())
extendSleepTimer((20 * 1000 * 60).toLong())
}
etxtTime.setText(lastTimerValue())
@ -128,7 +131,7 @@ class SleepTimerDialog : DialogFragment() {
val disableButton = binding.disableSleeptimerButton
disableButton.setOnClickListener {
controller.disableSleepTimer()
disableSleepTimer()
}
val setButton = binding.setSleeptimerButton
setButton.setOnClickListener {
@ -141,7 +144,7 @@ class SleepTimerDialog : DialogFragment() {
if (time == 0L) throw NumberFormatException("Timer must not be zero")
setLastTimer(etxtTime.getText().toString())
controller.setSleepTimer(timerMillis())
setSleepTimer(timerMillis())
closeKeyboard(content)
} catch (e: NumberFormatException) {

View File

@ -3,11 +3,15 @@ package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SpeedSelectDialogBinding
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlaybackServiceReady
import ac.mdiq.podcini.playback.PlaybackController.Companion.setPlaybackSpeed
import ac.mdiq.podcini.playback.PlaybackController.Companion.setSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray
import ac.mdiq.podcini.ui.view.ItemOffsetDecoration
import ac.mdiq.podcini.ui.view.PlaybackSpeedSeekBar
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.playback.SpeedChangedEvent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@ -120,7 +124,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
skipSilence.isChecked = isSkipSilence
skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
isSkipSilence = isChecked
controller?.setSkipSilence(isChecked)
setSkipSilence(isChecked)
}
return binding.root
@ -128,7 +132,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
@OptIn(UnstableApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (controller == null || !controller!!.isPlaybackServiceReady()) {
if (!isPlaybackServiceReady()) {
binding.currentAudio.visibility = View.INVISIBLE
binding.currentPodcast.visibility = View.INVISIBLE
} else {
@ -172,15 +176,15 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
true
}
holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({
Log.d("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
Logd("VariableSpeedDialog", "holder.chip settingCode0: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
settingCode[0] = binding.currentAudio.isChecked
settingCode[1] = binding.currentPodcast.isChecked
settingCode[2] = binding.global.isChecked
Log.d("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
Logd("VariableSpeedDialog", "holder.chip settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (controller != null) {
dismiss()
controller!!.setPlaybackSpeed(speed, settingCode)
setPlaybackSpeed(speed, settingCode)
}
}, 200) }
}
@ -203,7 +207,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
*/
fun newInstance(settingCode_: BooleanArray? = null, index_default: Int? = null): VariableSpeedDialog? {
val settingCode = settingCode_ ?: BooleanArray(3){true}
Log.d("VariableSpeedDialog", "newInstance settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
Logd("VariableSpeedDialog", "newInstance settingCode: ${settingCode[0]} ${settingCode[1]} ${settingCode[2]}")
if (settingCode.size != 3) {
Log.e("VariableSpeedDialog", "wrong settingCode dimension")
return null

View File

@ -6,6 +6,11 @@ import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding
import ac.mdiq.podcini.feed.util.ImageResourceUtils
import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils
import ac.mdiq.podcini.playback.PlaybackController
import ac.mdiq.podcini.playback.PlaybackController.Companion.fallbackSpeed
import ac.mdiq.podcini.playback.PlaybackController.Companion.isPlaybackServiceReady
import ac.mdiq.podcini.playback.PlaybackController.Companion.sleepTimerActive
import ac.mdiq.podcini.playback.PlaybackController.Companion.speedForward
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
import ac.mdiq.podcini.playback.service.PlaybackService
@ -31,6 +36,7 @@ import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
import ac.mdiq.podcini.util.ChapterUtils
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.TimeSpeedConverter
import ac.mdiq.podcini.util.event.FavoritesEvent
import ac.mdiq.podcini.util.event.PlayerErrorEvent
@ -52,8 +58,9 @@ import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.media3.common.util.UnstableApi
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.elevation.SurfaceColors
@ -75,10 +82,11 @@ import kotlin.math.min
*/
@UnstableApi
class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar.OnMenuItemClickListener {
var _binding: AudioplayerFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var playerDetailsFragment: PlayerDetailsFragment
private var playerDetailsFragment: PlayerDetailsFragment? = null
private lateinit var toolbar: MaterialToolbar
private var playerFragment1: InternalPlayerFragment? = null
@ -103,7 +111,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
_binding = AudioplayerFragmentBinding.inflate(inflater)
binding.root.setOnTouchListener { _: View?, _: MotionEvent? -> true } // Avoid clicks going through player to fragments below
Log.d(TAG, "fragment onCreateView")
Logd(TAG, "fragment onCreateView")
toolbar = binding.toolbar
toolbar.title = ""
toolbar.setNavigationOnClickListener {
@ -137,22 +145,26 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
cardViewSeek = binding.cardViewSeek
txtvSeek = binding.txtvSeek
val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction()
playerDetailsFragment = PlayerDetailsFragment()
transaction.replace(R.id.itemDescription, playerDetailsFragment).commit()
EventBus.getDefault().register(this)
return binding.root
}
fun initDetailedView() {
if (playerDetailsFragment == null) {
val fm = requireActivity().supportFragmentManager
val transaction = fm.beginTransaction()
playerDetailsFragment = PlayerDetailsFragment()
transaction.replace(R.id.itemDescription, playerDetailsFragment!!).commit()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
controller?.release()
controller = null
EventBus.getDefault().unregister(this)
Log.d(TAG, "Fragment destroyed")
Logd(TAG, "Fragment destroyed")
}
private fun setChapterDividers(media: Playable?) {
@ -181,13 +193,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
private fun loadMediaInfo(includingChapters: Boolean) {
Log.d(TAG, "loadMediaInfo called")
Logd(TAG, "loadMediaInfo called")
val theMedia = controller?.getMedia() ?: return
Log.d(TAG, "loadMediaInfo $theMedia")
Logd(TAG, "loadMediaInfo $theMedia")
if (currentMedia == null || theMedia.getIdentifier() != currentMedia?.getIdentifier()) {
Log.d(TAG, "loadMediaInfo loading details")
Logd(TAG, "loadMediaInfo loading details")
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
val media: Playable? = theMedia
@ -229,7 +241,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
}
private fun updateUi(media: Playable?) {
Log.d(TAG, "updateUi called")
Logd(TAG, "updateUi called")
setChapterDividers(media)
setupOptionsMenu(media)
}
@ -287,10 +299,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvenStartPlay(event: StartPlayEvent) {
Log.d(TAG, "onEvenStartPlay ${event.item.title}")
Logd(TAG, "onEvenStartPlay ${event.item.title}")
currentitem = event.item
if (currentMedia?.getIdentifier() == null || currentitem!!.media?.getIdentifier() != currentMedia?.getIdentifier())
playerDetailsFragment.setItem(currentitem!!)
playerDetailsFragment?.setItem(currentitem!!)
(activity as MainActivity).setPlayerVisible(true)
}
@ -362,8 +374,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
if (controller != null) {
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive())
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive())
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!sleepTimerActive())
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(sleepTimerActive())
}
(activity as? CastEnabledActivity)?.requestCastButton(toolbar.menu)
}
@ -379,7 +391,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val itemId = menuItem.itemId
when (itemId) {
R.id.show_home_reader_view -> {
playerDetailsFragment.buildHomeReaderText()
playerDetailsFragment?.buildHomeReaderText()
return true
}
R.id.show_video -> {
@ -399,7 +411,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
return true
}
R.id.share_notes -> {
val notes = if (playerDetailsFragment.showHomeText) playerDetailsFragment.readerhtml else feedItem?.description
val notes = if (playerDetailsFragment?.showHomeText == true) playerDetailsFragment!!.readerhtml else feedItem?.description
if (!notes.isNullOrEmpty()) {
val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
val context = requireContext()
@ -418,7 +430,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@JvmOverloads
fun scrollToTop() {
playerDetailsFragment.scrollToTop()
playerDetailsFragment?.scrollToTop()
}
fun fadePlayerToToolbar(slideOffset: Float) {
@ -462,7 +474,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@UnstableApi
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = InternalPlayerFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
Logd(TAG, "fragment onCreateView")
episodeTitle = binding.titleView
butPlaybackSpeed = binding.butPlaybackSpeed
@ -487,7 +499,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
sbPosition.setOnSeekBarChangeListener(this)
binding.internalPlayerFragment.setOnClickListener {
Log.d(TAG, "internalPlayerFragment was clicked")
Logd(TAG, "internalPlayerFragment was clicked")
val media = controller?.getMedia()
if (media != null) {
val mediaType = media.getMediaType()
@ -522,7 +534,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val media = controller!!.getMedia()
if (media != null) {
if (media.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) {
if (media.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING) {
controller!!.playPause()
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media.getMediaType()))
} else controller!!.playPause()
@ -537,7 +549,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
@OptIn(UnstableApi::class) private fun setupControlButtons() {
butRev.setOnClickListener {
if (controller != null && controller!!.isPlaybackServiceReady()) {
if (controller != null && isPlaybackServiceReady()) {
val curr: Int = controller!!.position
controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000)
sbPosition.visibility = View.VISIBLE
@ -548,14 +560,14 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
true
}
butPlay.setOnLongClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed)
if (fallbackSpeed > 0.1f) fallbackSpeed(fallbackSpeed)
}
true
}
butFF.setOnClickListener {
if (controller != null && controller!!.isPlaybackServiceReady()) {
if (controller != null && isPlaybackServiceReady()) {
val curr: Int = controller!!.position
controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000)
sbPosition.visibility = View.VISIBLE
@ -566,9 +578,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
true
}
butSkip.setOnClickListener {
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
if (controller != null && MediaPlayerBase.status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) controller!!.speedForward(speedForward)
if (speedForward > 0.1f) speedForward(speedForward)
}
}
butSkip.setOnLongClickListener {
@ -622,7 +634,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
txtvLength.text = Converter.getDurationStringLong(duration)
}
if (sbPosition.visibility == View.INVISIBLE && controller != null && controller!!.isPlaybackServiceReady()) {
if (sbPosition.visibility == View.INVISIBLE && isPlaybackServiceReady()) {
sbPosition.visibility = View.VISIBLE
}
if (!sbPosition.isPressed) {
@ -643,7 +655,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "Fragment is about to be destroyed")
Logd(TAG, "Fragment is about to be destroyed")
disposable?.dispose()
}
@ -668,38 +680,40 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
override fun onStartTrackingTouch(seekBar: SeekBar) {}
@OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) {
if (controller != null && controller!!.isPlaybackServiceReady()) {
if (isPlaybackServiceReady()) {
val prog: Float = seekBar.progress / (seekBar.max.toFloat())
controller!!.seekTo((prog * controller!!.duration).toInt())
controller?.seekTo((prog * controller!!.duration).toInt())
}
}
@UnstableApi
fun updateUi(media: Playable?) {
Log.d(TAG, "updateUi called $media")
Logd(TAG, "updateUi called $media")
if (media == null) return
episodeTitle.text = media.getEpisodeTitle()
(activity as MainActivity).setPlayerVisible(true)
onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration()))
val options = RequestOptions()
.placeholder(R.color.light_gray)
.error(R.color.light_gray)
.fitCenter()
.dontAnimate()
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) + "sdfsdf"
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media)
Glide.with(this)
.load(imgLoc)
.error(Glide.with(this)
.load(imgLocFB)
.error(R.mipmap.ic_launcher)
.apply(options))
.apply(options)
.into(imgvCover)
val imageLoader = imgvCover.context.imageLoader
val imageRequest = ImageRequest.Builder(requireContext())
.data(imgLoc)
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(requireContext())
.data(imgLocFB)
.error(R.mipmap.ic_launcher)
.target(imgvCover)
.build()
imageLoader.enqueue(fallbackImageRequest)
}
})
.target(imgvCover)
.build()
imageLoader.enqueue(imageRequest)
if (controller?.isPlayingVideoLocally == true) {
(activity as MainActivity).bottomSheet.setLocked(true)

View File

@ -4,23 +4,24 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.BaseEpisodesListFragmentBinding
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
import ac.mdiq.podcini.net.download.FeedUpdateManager
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectActionHandler
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.adapter.EpisodeItemListAdapter
import ac.mdiq.podcini.ui.adapter.SelectableAdapter
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.actions.EpisodeMultiSelectActionHandler
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.actions.menuhandler.MenuItemUtils
import ac.mdiq.podcini.ui.view.EmptyViewHandler
import ac.mdiq.podcini.ui.view.EpisodeItemListRecyclerView
import ac.mdiq.podcini.ui.view.LiftOnScrollListener
import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder
import ac.mdiq.podcini.util.FeedItemUtil
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.event.*
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import android.content.DialogInterface
import android.os.Bundle
import android.util.Log
@ -82,7 +83,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
super.onCreateView(inflater, container, savedInstanceState)
_binding = BaseEpisodesListFragmentBinding.inflate(inflater)
Log.d(TAG, "fragment onCreateView")
Logd(TAG, "fragment onCreateView")
txtvInformation = binding.txtvInformation
toolbar = binding.toolbar
@ -222,7 +223,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
}
override fun onContextItemSelected(item: MenuItem): Boolean {
Log.d(TAG, "onContextItemSelected() called with: item = [$item]")
Logd(TAG, "onContextItemSelected() called with: item = [$item]")
when {
// The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones.
// Apparently, none of the visibility check method works reliably on its own, so we just use all.
@ -284,7 +285,7 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ data: List<FeedItem> ->
if (data.size < EPISODES_PER_PAGE) hasMoreItems = false
Logd(TAG, "loadMoreItems $page ${data.size}")
episodes.addAll(data)
listAdapter.setDummyViews(0)
listAdapter.updateItems(episodes)
@ -411,7 +412,6 @@ abstract class BaseEpisodesListFragment : Fragment(), SelectableAdapter.OnSelect
listAdapter.updateItems(episodes)
listAdapter.setTotalNumberOfItems(data.second)
if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
updateToolbar()
}, { error: Throwable? ->
listAdapter.setDummyViews(0)

Some files were not shown because too many files have changed in this diff Show More