5.1.0 commit

This commit is contained in:
Xilin Jia 2024-05-13 21:08:13 +01:00
parent 399b4d16eb
commit 6fc3eb58ca
41 changed files with 351 additions and 393 deletions

View File

@ -97,9 +97,10 @@ The project aims to improve efficiency and provide more useful and user-friendly
* 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 sync for subscriptions and media files
### Security
### Security and reliability
* Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure
* Settings/Preferences can now to exported and imported
For more details of the changes, see the [Changelog](changelog.md)

View File

@ -159,8 +159,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020141
versionName "5.0.1"
versionCode 3020142
versionName "5.1.0"
def commit = ""
try {

View File

@ -2,10 +2,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.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPCallback {
class CancelableMediaPlayerCallback(private val originalCallback: MediaPlayerCallback) : MediaPlayerCallback {
private var isCancelled = false
fun cancel() {

View File

@ -2,10 +2,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.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerInfo
open class DefaultPSMPCallback : PSMPCallback {
open class DefaultMediaPlayerCallback : MediaPlayerCallback {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
}

View File

@ -97,7 +97,7 @@ class MediaPlayerBaseTest {
@UiThreadTest
fun testInit() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val psmp: MediaPlayerBase = LocalMediaPlayer(c, DefaultPSMPCallback())
val psmp: MediaPlayerBase = LocalMediaPlayer(c, DefaultMediaPlayerCallback())
psmp.shutdown()
}
@ -125,7 +125,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectStreamNoStartNoPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -155,7 +155,7 @@ class MediaPlayerBaseTest {
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.playerInfo.playerStatus)
Assert.assertFalse(psmp.isStartWhenPrepared())
callback.cancel()
psmp.shutdown()
@ -167,7 +167,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectStreamStartNoPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -198,7 +198,7 @@ class MediaPlayerBaseTest {
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.playerInfo.playerStatus)
Assert.assertTrue(psmp.isStartWhenPrepared())
callback.cancel()
psmp.shutdown()
@ -210,7 +210,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectStreamNoStartPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(4)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -244,7 +244,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.PREPARED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.PREPARED, psmp.playerInfo.playerStatus)
callback.cancel()
psmp.shutdown()
@ -256,7 +256,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectStreamStartPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(5)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -293,7 +293,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.PLAYING, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.PLAYING, psmp.playerInfo.playerStatus)
callback.cancel()
psmp.shutdown()
}
@ -304,7 +304,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectLocalNoStartNoPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -333,7 +333,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.playerInfo.playerStatus)
Assert.assertFalse(psmp.isStartWhenPrepared())
callback.cancel()
psmp.shutdown()
@ -345,7 +345,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectLocalStartNoPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(2)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -374,7 +374,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.INITIALIZED, psmp.playerInfo.playerStatus)
Assert.assertTrue(psmp.isStartWhenPrepared())
callback.cancel()
psmp.shutdown()
@ -386,7 +386,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectLocalNoStartPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(4)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -420,7 +420,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.PREPARED, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.PREPARED, psmp.playerInfo.playerStatus)
callback.cancel()
psmp.shutdown()
}
@ -431,7 +431,7 @@ class MediaPlayerBaseTest {
fun testPlayMediaObjectLocalStartPrepare() {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val countDownLatch = CountDownLatch(5)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
try {
checkPSMPInfo(newInfo)
@ -469,7 +469,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(LATCH_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
if (assertionError != null) throw assertionError!!
Assert.assertTrue(res)
Assert.assertSame(PlayerStatus.PLAYING, psmp.pSMPInfo.playerStatus)
Assert.assertSame(PlayerStatus.PLAYING, psmp.playerInfo.playerStatus)
callback.cancel()
psmp.shutdown()
}
@ -485,7 +485,7 @@ class MediaPlayerBaseTest {
val latchCount = if ((stream && reinit)) 2 else 1
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
when {
@ -606,7 +606,7 @@ class MediaPlayerBaseTest {
}
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
when {
@ -665,7 +665,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val latchCount = 1
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
if (newInfo!!.playerStatus == PlayerStatus.ERROR) {
@ -696,7 +696,7 @@ class MediaPlayerBaseTest {
val res = countDownLatch.await(timeoutSeconds, TimeUnit.SECONDS)
if (initialState != PlayerStatus.INITIALIZED) {
Assert.assertEquals(initialState, psmp.pSMPInfo.playerStatus)
Assert.assertEquals(initialState, psmp.playerInfo.playerStatus)
}
if (assertionError != null) throw assertionError!!
@ -738,7 +738,7 @@ class MediaPlayerBaseTest {
val c = InstrumentationRegistry.getInstrumentation().targetContext
val latchCount = 2
val countDownLatch = CountDownLatch(latchCount)
val callback = CancelablePSMPCallback(object : DefaultPSMPCallback() {
val callback = CancelableMediaPlayerCallback(object : DefaultMediaPlayerCallback() {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
checkPSMPInfo(newInfo)
if (newInfo!!.playerStatus == PlayerStatus.ERROR) {

View File

@ -2,14 +2,14 @@ package ac.mdiq.podcini.playback.cast
import android.content.Context
import ac.mdiq.podcini.playback.base.MediaPlayerBase
import ac.mdiq.podcini.playback.base.MediaPlayerBase.PSMPCallback
import ac.mdiq.podcini.playback.base.MediaPlayerBase.MediaPlayerCallback
/**
* Stub implementation of CastPsmp for Free build flavour
*/
object CastPsmp {
@JvmStatic
fun getInstanceIfConnected(context: Context, callback: PSMPCallback): MediaPlayerBase? {
fun getInstanceIfConnected(context: Context, callback: MediaPlayerCallback): MediaPlayerBase? {
return null
}
}

View File

@ -227,7 +227,7 @@ import kotlin.math.min
return null
}
@Throws(WifiSynchronizationServiceException::class)
@Throws(SyncServiceException::class)
override fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse? {
Log.d(TAG, "uploadSubscriptionChanges does nothing")
return null
@ -280,7 +280,7 @@ import kotlin.math.min
return newTimeStamp
}
@Throws(WifiSynchronizationServiceException::class)
@Throws(SyncServiceException::class)
override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction>): UploadChangesResponse {
// Log.d(TAG, "uploadEpisodeActions called")
var i = 0
@ -292,7 +292,7 @@ import kotlin.math.min
return WifiEpisodeActionPostResponse(System.currentTimeMillis() / 1000)
}
@Throws(WifiSynchronizationServiceException::class)
@Throws(SyncServiceException::class)
private fun uploadEpisodeActionsPartial(queuedEpisodeActions: List<EpisodeAction>, from: Int, to: Int) {
// Log.d(TAG, "uploadEpisodeActionsPartial called")
try {
@ -308,7 +308,7 @@ import kotlin.math.min
sendToPeer("EpisodeActions", list.toString())
} catch (e: Exception) {
e.printStackTrace()
throw WifiSynchronizationServiceException(e)
throw SyncServiceException(e)
}
}

View File

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

View File

@ -20,7 +20,7 @@ import kotlin.concurrent.Volatile
* Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
* and remote (cast devices) playback.
*/
abstract class MediaPlayerBase protected constructor(protected val context: Context, protected val callback: PSMPCallback) {
abstract class MediaPlayerBase protected constructor(protected val context: Context, protected val callback: MediaPlayerCallback) {
@Volatile
private var oldPlayerStatus: PlayerStatus? = null
@ -211,7 +211,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
*/
@get:Synchronized
val pSMPInfo: MediaPlayerInfo
val playerInfo: MediaPlayerInfo
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
*
@ -312,7 +312,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
* as the old one).
*
*
* It will also call [PSMPCallback.onPlaybackPause] or [PSMPCallback.onPlaybackStart]
* It will also call [MediaPlayerCallback.onPlaybackPause] or [MediaPlayerCallback.onPlaybackStart]
* depending on the status change.
*
* @param newStatus The new PlayerStatus. This must not be null.
@ -351,7 +351,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
setPlayerStatus(newStatus, newMedia, Playable.INVALID_TIME)
}
interface PSMPCallback {
interface MediaPlayerCallback {
fun statusChanged(newInfo: MediaPlayerInfo?)
fun shouldStop()

View File

@ -63,9 +63,7 @@ import kotlin.concurrent.Volatile
* Manages the MediaPlayer object of the PlaybackService.
*/
@UnstableApi
class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBase(context, callback) {
// private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPlayerBase(context, callback) {
@Volatile
private var statusBeforeSeeking: PlayerStatus? = null
@ -80,17 +78,11 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
private var mediaType: MediaType
private val startWhenPrepared = AtomicBoolean(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 var isShutDown = false
private var seekLatch: CountDownLatch? = null
// from wrapper
private val bufferUpdateInterval = 5L
private val bufferingUpdateDisposable: Disposable
private var mediaSource: MediaSource? = null
@ -323,24 +315,18 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
*/
override fun resume() {
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) {
// val focusGained = AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
Logd(TAG, "Audiofocus successfully requested")
Logd(TAG, "Resuming/Starting playback")
acquireWifiLockIfNecessary()
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(playable), UserPreferences.isSkipSilence)
setVolume(1.0f, 1.0f)
// 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 && 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")
if (playable != null && status == PlayerStatus.PREPARED && playable!!.getPosition() > 0) {
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(playable!!.getPosition(), playable!!.getLastPlayedTime())
seekTo(newPosition)
}
play()
setPlayerStatus(PlayerStatus.PLAYING, playable)
} else Logd(TAG, "Call to resume() was ignored because current state of PSMP object is $status")
}
@ -361,20 +347,12 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
Logd(TAG, "Pausing playback.")
exoPlayer?.pause()
setPlayerStatus(PlayerStatus.PAUSED, playable, getPosition())
// if (abandonFocus) {
// abandonAudioFocus()
// pausedBecauseOfTransientAudiofocusLoss = false
// }
if (isStreaming && reinit) reinit()
} else {
Logd(TAG, "Ignoring call to pause: Player is in $status state")
}
}
// private fun abandonAudioFocus() {
// AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest)
// }
/**
* Prepares media player for playback if the service is in the INITALIZED
* state.
@ -497,7 +475,10 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
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()
if (retVal <= 0) {
val playableDur = playable?.getDuration() ?: -1
if (playableDur > 0) retVal = playableDur
}
return retVal
}
@ -509,8 +490,10 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
// Log.d(TAG, "getPosition() ${playable?.getIdentifier()} $status")
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
val playablePos = playable?.getPosition() ?: -1
if (retVal <= 0 && playablePos >= 0) retVal = playablePos
if (retVal <= 0) {
val playablePos = playable?.getPosition() ?: -1
if (playablePos >= 0) retVal = playablePos
}
return retVal
}
@ -562,7 +545,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
volumeRight *= adaptionFactor
}
}
// playerWrapper?.setVolume(volumeLeft, volumeRight)
if (volumeLeft > 1) {
exoPlayer!!.volume = 1f
@ -599,7 +581,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
status = PlayerStatus.STOPPED
isShutDown = true
// abandonAudioFocus()
releaseWifiLockIfNecessary()
}
@ -681,61 +662,9 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
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
// }
// }
// }
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()
if (exoPlayer == null) {
setupPlayerListener()
createStaticPlayer(context)
@ -756,9 +685,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
val position = getPosition()
if (position >= 0) playable?.setPosition(position)
// if (exoPlayer == null) createStaticPlayer(context)
// abandonAudioFocus()
Logd(TAG, "endPlayback $hasEnded $wasSkipped $shouldContinue $toStoppedState")
// printStackTrace()
@ -896,7 +822,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
companion object {
private const val TAG = "LocalMediaPlayer"
// from wrapper
const val BUFFERING_STARTED: Int = -1
const val BUFFERING_ENDED: Int = -2
const val ERROR_CODE_OFFSET: Int = 1000
@ -935,44 +860,6 @@ 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 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)
// }
// }
if (exoplayerListener != null) {
exoPlayer?.removeListener(exoplayerListener!!)
exoPlayer?.addListener(exoplayerListener!!)
@ -988,7 +875,6 @@ class LocalMediaPlayer(context: Context, callback: PSMPCallback) : MediaPlayerBa
if (oldEnhancer.enabled) newEnhancer.setTargetGain(oldEnhancer.targetGain.toInt())
oldEnhancer.release()
}
loudnessEnhancer = newEnhancer
}

View File

@ -5,7 +5,7 @@ import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
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.MediaPlayerCallback
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.playback.cast.CastPsmp
import ac.mdiq.podcini.playback.cast.CastStateListener
@ -129,7 +129,7 @@ class PlaybackService : MediaSessionService() {
private val mBinder: IBinder = LocalBinder()
val mPlayerInfo: MediaPlayerInfo
get() = mediaPlayer!!.pSMPInfo
get() = mediaPlayer!!.playerInfo
val status: PlayerStatus
get() = MediaPlayerBase.status
@ -361,14 +361,6 @@ class PlaybackService : MediaSessionService() {
// }
return settable
}
// this is just for testing
// override fun onAddMediaItems(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, mediaItems: MutableList<MediaItem>): ListenableFuture<List<MediaItem>> {
// val updatedMediaItems = mediaItems.map { mediaItem ->
// mediaItem.buildUpon().setUri(mediaItem.requestMetadata.mediaUri).build()
// }
// return Futures.immediateFuture(updatedMediaItems)
// }
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
@ -640,7 +632,7 @@ class PlaybackService : MediaSessionService() {
*/
private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean {
Logd(TAG, "Handling keycode: $keycode")
val info = mediaPlayer?.pSMPInfo
val info = mediaPlayer?.playerInfo
val status = info?.playerStatus
when (keycode) {
KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
@ -801,7 +793,7 @@ class PlaybackService : MediaSessionService() {
}
}
private val mediaPlayerCallback: PSMPCallback = object : PSMPCallback {
private val mediaPlayerCallback: MediaPlayerCallback = object : MediaPlayerCallback {
override fun statusChanged(newInfo: MediaPlayerInfo?) {
currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN
Logd(TAG, "statusChanged called ${newInfo?.playerStatus}")
@ -809,11 +801,11 @@ class PlaybackService : MediaSessionService() {
if (newInfo != null) {
when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.playerInfo.playable, mediaPlayer!!.playerInfo.playerStatus, currentitem)
// updateNotificationAndMediaSession(newInfo.playable)
}
PlayerStatus.PREPARED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.playerInfo.playable, mediaPlayer!!.playerInfo.playerStatus, currentitem)
taskManager.startChapterLoader(newInfo.playable!!)
}
PlayerStatus.PAUSED -> {

View File

@ -306,12 +306,6 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled())
}
// companion object {
// private const val TAG = "SleepTimer"
// private const val UPDATE_INTERVAL = 1000L
// const val NOTIFICATION_THRESHOLD: Long = 10000
// }
}
interface PSTMCallback {

View File

@ -7,9 +7,9 @@ import androidx.preference.ListPreference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class MaterialListPreference : ListPreference {
constructor(context: Context) : super(context!!)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context!!, attrs)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onClick() {
val builder = MaterialAlertDialogBuilder(context)

View File

@ -7,9 +7,10 @@ import androidx.preference.MultiSelectListPreference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
class MaterialMultiSelectListPreference : MultiSelectListPreference {
constructor(context: Context) : super(context!!)
constructor(context: Context, attrs: AttributeSet?) : super(context!!, attrs)
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onClick() {
val builder = MaterialAlertDialogBuilder(context)
@ -22,8 +23,8 @@ class MaterialMultiSelectListPreference : MultiSelectListPreference {
for (i in values.indices) {
selected[i] = getValues().contains(values[i].toString())
}
builder.setMultiChoiceItems(entries, selected) { dialog: DialogInterface?, which: Int, isChecked: Boolean -> selected[which] = isChecked }
builder.setPositiveButton("OK") { dialog: DialogInterface?, which: Int ->
builder.setMultiChoiceItems(entries, selected) { _: DialogInterface?, which: Int, isChecked: Boolean -> selected[which] = isChecked }
builder.setPositiveButton("OK") { _: DialogInterface?, _: Int ->
val selectedValues: MutableSet<String> = HashSet()
for (i in values.indices) {
if (selected[i]) selectedValues.add(entryValues[i].toString())

View File

@ -21,6 +21,7 @@ import org.greenrobot.eventbus.EventBus
* otherwise every public method will throw an Exception when called.
*/
class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListener {
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
if (PREF_CURRENT_PLAYER_STATUS == key) EventBus.getDefault().post(PlayerStatusEvent())
}

View File

@ -103,7 +103,7 @@ object SleepTimerPreferences {
@JvmStatic
fun isInTimeRange(from: Int, to: Int, current: Int): Boolean {
// Range covers one day
if (from < to) return from <= current && current < to
if (from < to) return current in from..<to
// Range covers two days
if (from <= current) return true

View File

@ -154,7 +154,7 @@ object UserPreferences {
}
@JvmStatic
var theme: ThemePreference?
var theme: ThemePreference
get() = when (prefs.getString(PREF_THEME, "system")) {
"0" -> ThemePreference.LIGHT
"1" -> ThemePreference.DARK
@ -175,7 +175,7 @@ object UserPreferences {
get() = Build.VERSION.SDK_INT >= 31 && prefs.getBoolean(PREF_TINTED_COLORS, false)
@JvmStatic
var hiddenDrawerItems: List<String?>
var hiddenDrawerItems: List<String>
get() {
val hiddenItems = prefs.getString(PREF_HIDDEN_DRAWER_ITEMS, "")
return ArrayList(listOf(*TextUtils.split(hiddenItems, ",")))
@ -188,10 +188,9 @@ object UserPreferences {
}
@JvmStatic
var fullNotificationButtons: List<Int>?
var fullNotificationButtons: List<Int>
get() {
val buttons = TextUtils.split(prefs.getString(PREF_FULL_NOTIFICATION_BUTTONS,
"$NOTIFICATION_BUTTON_SKIP,$NOTIFICATION_BUTTON_PLAYBACK_SPEED"), ",")
val buttons = TextUtils.split(prefs.getString(PREF_FULL_NOTIFICATION_BUTTONS, "$NOTIFICATION_BUTTON_SKIP,$NOTIFICATION_BUTTON_PLAYBACK_SPEED"), ",")
val notificationButtons: MutableList<Int> = ArrayList()
for (button in buttons) {
notificationButtons.add(button.toInt())
@ -509,7 +508,7 @@ object UserPreferences {
val defaultValue = HashSet<String>()
defaultValue.add("images")
val getValueStringSet = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue)
val allowed: MutableSet<String> = HashSet(getValueStringSet)
val allowed: MutableSet<String> = HashSet(getValueStringSet!!)
if (allow) allowed.add(type)
else allowed.remove(type)

View File

@ -1,6 +1,18 @@
package ac.mdiq.podcini.preferences.fragments
import android.app.Activity
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.DatabaseTransporter
import ac.mdiq.podcini.storage.PreferencesTransporter
import ac.mdiq.podcini.storage.asynctask.DocumentFileExportWorker
import ac.mdiq.podcini.storage.asynctask.ExportWorker
import ac.mdiq.podcini.storage.export.ExportWriter
import ac.mdiq.podcini.storage.export.favorites.FavoritesWriter
import ac.mdiq.podcini.storage.export.html.HtmlWriter
import ac.mdiq.podcini.storage.export.opml.OpmlWriter
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import android.app.Activity.RESULT_OK
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Context
@ -21,17 +33,6 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.storage.asynctask.DocumentFileExportWorker
import ac.mdiq.podcini.storage.asynctask.ExportWorker
import ac.mdiq.podcini.storage.export.ExportWriter
import ac.mdiq.podcini.storage.export.favorites.FavoritesWriter
import ac.mdiq.podcini.storage.export.html.HtmlWriter
import ac.mdiq.podcini.storage.export.opml.OpmlWriter
import ac.mdiq.podcini.storage.DatabaseTransporter
import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@ -41,6 +42,7 @@ 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 ->
@ -53,10 +55,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
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) }
this.restorePreferencesResult(result)
}
private val backupPreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
val data: Uri? = it.data?.data
if (data != null) PreferencesTransporter.exportToDocument(data, requireContext())
}
}
private var disposable: Disposable? = null
private var progressDialog: ProgressDialog? = null
@ -145,14 +152,29 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
}
// TODO: implement
private fun exportPreferences() {
// backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
backupPreferencesLauncher.launch(intent)
}
// TODO: implement
private fun importPreferences() {
// backupDatabaseLauncher.launch(dateStampFilename(DATABASE_EXPORT_FILENAME))
val builder = MaterialAlertDialogBuilder(requireActivity())
builder.setTitle(R.string.preferences_import_label)
builder.setMessage(R.string.preferences_import_warning)
// add a button
builder.setNegativeButton(R.string.no, null)
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addCategory(Intent.CATEGORY_DEFAULT)
restorePreferencesLauncher.launch(intent)
}
// create and show the alert dialog
builder.show()
}
private fun exportDatabase() {
@ -186,7 +208,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
builder.show()
}
fun showExportSuccessSnackbar(uri: Uri?, mimeType: String?) {
private 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() }
.show()
@ -202,25 +224,25 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}
private fun chooseOpmlExportPathResult(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK || result.data == null) return
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
exportWithWriter(OpmlWriter(), uri, Export.OPML)
}
private fun chooseHtmlExportPathResult(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK || result.data == null) return
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
exportWithWriter(HtmlWriter(), uri, Export.HTML)
}
private fun chooseFavoritesExportPathResult(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK || result.data == null) return
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
exportWithWriter(FavoritesWriter(), uri, Export.FAVORITES)
}
private fun restoreDatabaseResult(result: ActivityResult) {
if (result.resultCode != Activity.RESULT_OK || result.data == null) return
if (result.resultCode != RESULT_OK || result.data == null) return
val uri = result.data!!.data
progressDialog!!.show()
disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) }
@ -232,6 +254,19 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
}, { error: Throwable -> this.showExportErrorDialog(error) })
}
private fun restorePreferencesResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data?.data == null) return
val uri = result.data!!.data!!
progressDialog!!.show()
disposable = Completable.fromAction { PreferencesTransporter.importBackup(uri, requireContext()) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
showDatabaseImportSuccessDialog()
progressDialog!!.dismiss()
}, { error: Throwable -> this.showExportErrorDialog(error) })
}
private fun backupDatabaseResult(uri: Uri?) {
if (uri == null) return
progressDialog!!.show()
@ -244,31 +279,6 @@ 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)
@ -306,19 +316,10 @@ 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),
FAVORITES(CONTENT_TYPE_HTML, DEFAULT_FAVORITES_OUTPUT_NAME, R.string.favorites_export_label)
FAVORITES(CONTENT_TYPE_HTML, DEFAULT_FAVORITES_OUTPUT_NAME, R.string.favorites_export_label),
}
companion object {

View File

@ -6,6 +6,7 @@ import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.activity.PreferenceActivity.Companion.getTitleOfPage
import ac.mdiq.podcini.preferences.fragments.about.AboutFragment
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import ac.mdiq.podcini.util.Logd
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.PorterDuff
@ -18,6 +19,8 @@ import com.bytehamster.lib.preferencesearch.SearchPreference
class MainPreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
Logd("MainPreferencesFragment", "onCreatePreferences")
addPreferencesFromResource(R.xml.preferences)
setupMainScreen()
setupSearch()
@ -128,7 +131,7 @@ class MainPreferencesFragment : PreferenceFragmentCompat() {
private fun setupSearch() {
val searchPreference = findPreference<SearchPreference>("searchPreference")
val config = searchPreference!!.searchConfiguration
config.setActivity((activity as AppCompatActivity?)!!)
config.setActivity((activity as AppCompatActivity))
config.setFragmentContainerViewId(R.id.settingsContainer)
config.setBreadcrumbsEnabled(true)

View File

@ -22,6 +22,7 @@ import com.google.android.material.snackbar.Snackbar
import org.greenrobot.eventbus.EventBus
class UserInterfacePreferencesFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences_user_interface)
setupInterfaceScreen()

View File

@ -21,7 +21,7 @@ class AboutFragment : PreferenceFragmentCompat() {
findPreference<Preference>("about_version")!!.summary = String.format("%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.COMMIT_HASH)
findPreference<Preference>("about_version")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { preference: Preference? ->
Preference.OnPreferenceClickListener {
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(getString(R.string.bug_report_title), findPreference<Preference>("about_version")!!.summary)
clipboard.setPrimaryClip(clip)
@ -29,7 +29,7 @@ class AboutFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>("about_contributors")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { preference: Preference? ->
Preference.OnPreferenceClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.settingsContainer, ContributorsPagerFragment())
.addToBackStack(getString(R.string.contributors))
@ -37,12 +37,12 @@ class AboutFragment : PreferenceFragmentCompat() {
true
}
findPreference<Preference>("about_privacy_policy")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { preference: Preference? ->
Preference.OnPreferenceClickListener {
openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/blob/main/PrivacyPolicy.md")
true
}
findPreference<Preference>("about_licenses")!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { preference: Preference? ->
Preference.OnPreferenceClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.settingsContainer, LicensesFragment())
.addToBackStack(getString(R.string.translators))

View File

@ -60,7 +60,7 @@ class LicensesFragment : ListFragment() {
val items = arrayOf<CharSequence>("View website", "View license")
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setItems(items) { dialog: DialogInterface?, which: Int ->
.setItems(items) { _: DialogInterface?, which: Int ->
when (which) {
0 -> openInBrowser(requireContext(), item.licenseUrl)
1 -> showLicenseText(item.licenseTextFile)

View File

@ -2,14 +2,13 @@ package ac.mdiq.podcini.preferences.fragments.synchronization
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.*
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
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.setSelectedSyncProvider
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

View File

@ -1,20 +1,19 @@
package ac.mdiq.podcini.preferences.fragments.synchronization
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.NextcloudAuthDialogBinding
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
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.setSelectedSyncProvider
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.view.View
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
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.databinding.NextcloudAuthDialogBinding
import ac.mdiq.podcini.net.sync.SynchronizationSettings.setSelectedSyncProvider
import ac.mdiq.podcini.net.sync.nextcloud.NextcloudLoginFlow
/**
* Guides the user through the authentication process.

View File

@ -98,7 +98,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
true
}
val loggedIn = SynchronizationSettings.isProviderConnected
val loggedIn = isProviderConnected
val preferenceHeader = findPreference<Preference>(PREFERENCE_SYNCHRONIZATION_DESCRIPTION)
if (loggedIn) {
val selectedProvider = SynchronizationProviderViewData.fromIdentifier(selectedSyncProviderKey)

View File

@ -636,7 +636,7 @@ object DBReader {
* @param item The FeedItem
*/
fun loadTextDetailsOfFeedItem(item: FeedItem) {
Logd(TAG, "loadTextOfFeedItem() called with: item = [$item]")
Logd(TAG, "loadTextDetailsOfFeedItem() called with: item = [${item.title}]")
// TODO: need to find out who are often calling this
// printStackTrace()
val adapter = getInstance()

View File

@ -0,0 +1,97 @@
package ac.mdiq.podcini.storage
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import java.io.*
object PreferencesTransporter {
private const val TAG = "PreferencesTransporter"
@Throws(IOException::class)
fun exportToDocument(uri: Uri, context: Context) {
try {
val chosenDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Destination directory is not valid")
val exportSubDir = chosenDir.createDirectory("Podcini-Prefs") ?: throw IOException("Error creating subdirectory Podcini-Prefs")
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
file.name.startsWith("shared_prefs")
}?.firstOrNull()
if (sharedPreferencesDir != null) {
sharedPreferencesDir.listFiles()!!.forEach { file ->
val destFile = exportSubDir.createFile("text/xml", file.name)
if (destFile != null) copyFile(file, destFile, context)
}
} else {
Log.e("Error", "shared_prefs directory not found")
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
} finally { }
}
private fun copyFile(sourceFile: File, destFile: DocumentFile, context: Context) {
try {
val inputStream = FileInputStream(sourceFile)
val outputStream = context.contentResolver.openOutputStream(destFile.uri)
if (outputStream != null) copyStream(inputStream, outputStream)
inputStream.close()
outputStream?.close()
} catch (e: IOException) {
Log.e("Error", "Error copying file: $e")
throw e
}
}
private fun copyFile(sourceFile: DocumentFile, destFile: File, context: Context) {
try {
val inputStream = context.contentResolver.openInputStream(sourceFile.uri)
val outputStream = FileOutputStream(destFile)
if (inputStream != null) copyStream(inputStream, outputStream)
inputStream?.close()
outputStream.close()
} catch (e: IOException) {
Log.e("Error", "Error copying file: $e")
throw e
}
}
private fun copyStream(inputStream: InputStream, outputStream: OutputStream) {
val buffer = ByteArray(1024)
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
}
}
@Throws(IOException::class)
fun importBackup(uri: Uri, context: Context) {
try {
val exportedDir = DocumentFile.fromTreeUri(context, uri) ?: throw IOException("Backup directory is not valid")
val sharedPreferencesDir = context.applicationContext.filesDir.parentFile?.listFiles { file ->
file.name.startsWith("shared_prefs")
}?.firstOrNull()
if (sharedPreferencesDir != null) {
sharedPreferencesDir.listFiles()?.forEach { file ->
// val prefName = file.name.substring(0, file.name.lastIndexOf('.'))
file.delete()
}
} else {
Log.e("Error", "shared_prefs directory not found")
}
val files = exportedDir.listFiles()
for (file in files) {
if (file?.isFile == true && file.name?.endsWith(".xml") == true) {
val destFile = File(sharedPreferencesDir, file.name!!)
copyFile(file, destFile, context)
}
}
} catch (e: IOException) {
Log.e(TAG, Log.getStackTraceString(e))
throw e
} finally { }
}
}

View File

@ -38,6 +38,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
setTheme(getTheme(this))
super.onCreate(savedInstanceState)
Logd("PreferenceActivity", "onCreate")
val ab = supportActionBar
ab?.setDisplayHomeAsUpEnabled(true)

View File

@ -118,6 +118,7 @@ class SelectSubscriptionActivity : AppCompatActivity() {
val request = ImageRequest.Builder(this)
.data(feed.imageUrl)
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
@OptIn(UnstableApi::class) override fun onError(request: ImageRequest, throwable: ErrorResult) {

View File

@ -2,10 +2,12 @@ package ac.mdiq.podcini.ui.adapter
import ac.mdiq.podcini.R
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.util.Logd
import android.graphics.drawable.Drawable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import coil.Coil
import coil.ImageLoader
import coil.imageLoader
import coil.request.ErrorResult
@ -60,43 +62,24 @@ class CoverLoader(private val activity: MainActivity) {
fun load() {
if (imgvCover == null) return
// val coverTarget = CoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
val coverTargetCoil = CoilCoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
if (resource != 0) {
// 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)
CoilCoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined)
return
}
// 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)
val request = ImageRequest.Builder(activity)
.data(uri)
.setHeader("User-Agent", "Mozilla/5.0")
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(activity)
.data(fallbackUri)
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.target(coverTargetCoil)
.build()
@ -105,39 +88,11 @@ class CoverLoader(private val activity: MainActivity) {
})
.target(coverTargetCoil)
.build()
activity.imageLoader.enqueue(request)
activity.imageLoader
.enqueue(request)
}
// 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)
@ -156,12 +111,6 @@ class CoverLoader(private val activity: MainActivity) {
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

View File

@ -700,11 +700,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val imageLoader = imgvCover.context.imageLoader
val imageRequest = ImageRequest.Builder(requireContext())
.data(imgLoc)
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(requireContext())
.data(imgLocFB)
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.target(imgvCover)
.build()

View File

@ -15,10 +15,8 @@ import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ShareCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.MenuHost
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
@ -247,9 +245,19 @@ class EpisodeHomeFragment : Fragment() {
updateAppearance()
}
private fun cleatWebview(webview: WebView) {
binding.root.removeView(webview)
webview.clearHistory()
webview.clearCache(true)
webview.clearView()
webview.destroy()
}
@OptIn(UnstableApi::class) override fun onDestroyView() {
super.onDestroyView()
Log.d(TAG, "onDestroyView")
cleatWebview(binding.webView)
cleatWebview(binding.readerView)
_binding = null
disposable?.dispose()
tts?.stop()

View File

@ -99,8 +99,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
item = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) requireArguments().getSerializable(ARG_FEEDITEM, FeedItem::class.java)
else requireArguments().getSerializable(ARG_FEEDITEM) as? FeedItem
// item = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) requireArguments().getSerializable(ARG_FEEDITEM, FeedItem::class.java)
// else requireArguments().getSerializable(ARG_FEEDITEM) as? FeedItem
}
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -260,6 +260,9 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
controller?.release()
disposable?.dispose()
root.removeView(webvDescription)
webvDescription.clearHistory()
webvDescription.clearCache(true)
webvDescription.clearView()
webvDescription.destroy()
}
@ -300,6 +303,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(requireContext())
.data(imgLocFB)
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.target(imgvCover)
.build()
@ -411,6 +415,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
disposable?.dispose()
if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE
Logd(TAG, "load() called")
disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -435,6 +440,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
return feedItem
}
fun setItem(item_: FeedItem) {
item = item_
}
companion object {
private const val TAG = "EpisodeInfoFragment"
private const val ARG_FEEDITEM = "feeditem"
@ -442,9 +451,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@JvmStatic
fun newInstance(item: FeedItem): EpisodeInfoFragment {
val fragment = EpisodeInfoFragment()
val args = Bundle()
args.putSerializable(ARG_FEEDITEM, item)
fragment.arguments = args
fragment.setItem(item)
// val args = Bundle()
// args.putSerializable(ARG_FEEDITEM, item)
// fragment.arguments = args
return fragment
}
}

View File

@ -316,11 +316,13 @@ class PlayerDetailsFragment : Fragment() {
val imageLoader = binding.imgvCover.context.imageLoader
val imageRequest = ImageRequest.Builder(requireContext())
.data(media!!.getImageLocation())
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(requireContext())
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.target(binding.imgvCover)
.build()
@ -342,11 +344,13 @@ class PlayerDetailsFragment : Fragment() {
val imageLoader = binding.imgvCover.context.imageLoader
val imageRequest = ImageRequest.Builder(requireContext())
.data(imgLoc)
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
val fallbackImageRequest = ImageRequest.Builder(requireContext())
.data(ImageResourceUtils.getFallbackImageLocation(media!!))
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.target(binding.imgvCover)
.build()

View File

@ -81,7 +81,6 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
if (elementsWithTimeCodes.size == 0) return // No elements with timecodes
var useHourFormat = true
if (playableDuration != Int.MAX_VALUE) {
// We need to decide if we are going to treat short timecodes as HH:MM or MM:SS. To do
// so we will parse all the short timecodes and see if they fit in the duration. If one
@ -91,10 +90,8 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
val matcherForElement = TIMECODE_REGEX.matcher(element.html())
while (matcherForElement.find()) {
// We only want short timecodes right now.
if (matcherForElement.group(1) == null) {
val time = durationStringShortToMs(matcherForElement.group(0)!!, true)
// If the parsed timecode is greater then the duration then we know we need to
// use the minute format so we are done.
if (time > playableDuration) {
@ -103,7 +100,6 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
}
}
}
if (!useHourFormat) break
}
}
@ -114,13 +110,10 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
while (matcherForElement.find()) {
val group = matcherForElement.group(0) ?: continue
val time = if (matcherForElement.group(1) != null) durationStringLongToMs(group)
else durationStringShortToMs(group, useHourFormat)
var replacementText = group
if (time < playableDuration) replacementText = String.format(Locale.US, TIMECODE_LINK, time, group)
matcherForElement.appendReplacement(buffer, replacementText)
}

View File

@ -20,9 +20,6 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
import ac.mdiq.podcini.ui.adapter.CoverLoader
import ac.mdiq.podcini.feed.util.ImageResourceUtils
import ac.mdiq.podcini.util.DateFormatter
import ac.mdiq.podcini.util.NetworkUtils
import ac.mdiq.podcini.util.PlaybackStatus
import ac.mdiq.podcini.net.download.MediaSizeLoader
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
import ac.mdiq.podcini.storage.model.feed.FeedItem
@ -37,8 +34,10 @@ import ac.mdiq.podcini.ui.actions.actionbutton.ItemActionButton
import ac.mdiq.podcini.ui.actions.actionbutton.TTSActionButton
import ac.mdiq.podcini.ui.view.CircularProgressBar
import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.*
import android.widget.LinearLayout
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat.getDrawable
import io.reactivex.functions.Consumer
import kotlin.math.max
@ -117,32 +116,39 @@ class EpisodeItemViewHolder(private val activity: MainActivity, parent: ViewGrou
}
// Log.d(TAG, "bind called ${item.media}")
if (item.media != null) {
bind(item.media!!)
} else if (item.playState == BUILDING) {
// for generating TTS files for episode without media
secondaryActionProgress.setPercentage(actionButton!!.processing, item)
secondaryActionProgress.setIndeterminate(false)
} else {
secondaryActionProgress.setPercentage(0f, item)
secondaryActionProgress.setIndeterminate(false)
isVideo.visibility = View.GONE
progressBar.visibility = View.GONE
duration.visibility = View.GONE
position.visibility = View.GONE
itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground))
when {
item.media != null -> {
bind(item.media!!)
}
item.playState == BUILDING -> {
// for generating TTS files for episode without media
secondaryActionProgress.setPercentage(actionButton!!.processing, item)
secondaryActionProgress.setIndeterminate(false)
}
else -> {
secondaryActionProgress.setPercentage(0f, item)
secondaryActionProgress.setIndeterminate(false)
isVideo.visibility = View.GONE
progressBar.visibility = View.GONE
duration.visibility = View.GONE
position.visibility = View.GONE
itemView.setBackgroundResource(ThemeUtils.getDrawableFromAttr(activity, R.attr.selectableItemBackground))
}
}
if (coverHolder.visibility == View.VISIBLE) {
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(item)
// Log.d(TAG, "imgLoc $imgLoc")
Logd(TAG, "imgLoc $imgLoc ${item.feed?.imageUrl} ${item.title}")
if (!imgLoc.isNullOrBlank() && !imgLoc.contains(PREFIX_GENERATIVE_COVER)) CoverLoader(activity)
.withUri(imgLoc)
.withFallbackUri(item.feed?.imageUrl)
.withPlaceholderView(placeholder)
.withCoverView(cover)
.load()
else cover.setImageResource(R.mipmap.ic_launcher)
else {
Logd(TAG, "setting to ic_launcher")
cover.setImageDrawable(AppCompatResources.getDrawable(activity, R.drawable.ic_launcher_foreground))
}
}
}

View File

@ -81,12 +81,14 @@ object WidgetUpdater {
CoroutineScope(Dispatchers.IO).launch {
val request = ImageRequest.Builder(context)
.data(imgLoc)
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
CoroutineScope(Dispatchers.IO).launch {
val fallbackImageRequest = ImageRequest.Builder(context)
.data(imgLoc1)
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.size(iconSize, iconSize)
.build()

View File

@ -609,6 +609,7 @@
<string name="html_export_label">HTML export</string>
<string name="preferences_export_label">Preferences export</string>
<string name="preferences_import_label">Preferences import</string>
<string name="preferences_import_warning">Importing preferences will replace all of your current preferences. If confirmed, choose a previously exported directory with name containing \"Podcini-Prefs\"</string>
<string name="database_export_label">Database export</string>
<string name="database_import_label">Database import</string>
<string name="database_import_warning">Importing a database will replace all of your current subscriptions and playing history. You should export your current database as a backup. Do you want to replace\?</string>

View File

@ -16,18 +16,18 @@
android:summary="@string/database_import_summary"/>
</PreferenceCategory>
<!-- <PreferenceCategory android:title="@string/preferences">-->
<!-- <Preference-->
<!-- android:key="prefPrefExport"-->
<!-- search:keywords="@string/import_export_search_keywords"-->
<!-- android:title="@string/preferences_export_label"-->
<!-- android:summary="@string/preferences_export_summary"/>-->
<!-- <Preference-->
<!-- android:key="prefPrefImport"-->
<!-- search:keywords="@string/import_export_search_keywords"-->
<!-- android:title="@string/preferences_import_label"-->
<!-- android:summary="@string/preferences_import_summary"/>-->
<!-- </PreferenceCategory>-->
<PreferenceCategory android:title="@string/preferences">
<Preference
android:key="prefPrefExport"
search:keywords="@string/import_export_search_keywords"
android:title="@string/preferences_export_label"
android:summary="@string/preferences_export_summary"/>
<Preference
android:key="prefPrefImport"
search:keywords="@string/import_export_search_keywords"
android:title="@string/preferences_import_label"
android:summary="@string/preferences_import_summary"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/opml">
<Preference

View File

@ -1,3 +1,9 @@
## 5.1.0
* properly destroys WebView objects
* fixed issue loading image lacking a header
* preferences now can be exported/imported
## 5.0.1
* fixed crash when opening Import/Export in settings

View File

@ -0,0 +1,6 @@
Version 5.1.0 brings several changes:
* properly destroys WebView objects
* fixed issue loading image lacking a header
* preferences now can be exported/imported