5.0.0 commit
This commit is contained in:
parent
eeea4bfd17
commit
5fcb4c3939
|
@ -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
|
||||
|
||||
|
|
44
README.md
44
README.md
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
@ -28,8 +28,7 @@ object LockingAsyncExecutor {
|
|||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}.subscribeOn(Schedulers.io()).subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!!
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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?
|
||||
|
|
|
@ -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!!
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue