From 6fc3eb58ca794f0d99ae1df16c6d5ecf56278032 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Mon, 13 May 2024 21:08:13 +0100 Subject: [PATCH] 5.1.0 commit --- README.md | 3 +- app/build.gradle | 4 +- ...ck.kt => CancelableMediaPlayerCallback.kt} | 4 +- ...lback.kt => DefaultMediaPlayerCallback.kt} | 4 +- .../service/playback/MediaPlayerBaseTest.kt | 44 ++--- .../ac/mdiq/podcini/playback/cast/CastPsmp.kt | 4 +- .../podcini/net/sync/wifi/WifiSyncService.kt | 8 +- .../WifiSynchronizationServiceException.kt | 5 - .../podcini/playback/base/MediaPlayerBase.kt | 8 +- .../playback/service/LocalMediaPlayer.kt | 154 +++--------------- .../playback/service/PlaybackService.kt | 20 +-- .../service/PlaybackServiceTaskManager.kt | 6 - .../preferences/MaterialListPreference.kt | 4 +- .../MaterialMultiSelectListPreference.kt | 9 +- .../preferences/PlaybackPreferences.kt | 1 + .../preferences/SleepTimerPreferences.kt | 2 +- .../podcini/preferences/UserPreferences.kt | 11 +- .../ImportExportPreferencesFragment.kt | 119 +++++++------- .../fragments/MainPreferencesFragment.kt | 5 +- .../UserInterfacePreferencesFragment.kt | 1 + .../fragments/about/AboutFragment.kt | 8 +- .../fragments/about/LicensesFragment.kt | 2 +- .../GpodderAuthenticationFragment.kt | 5 +- .../NextcloudAuthenticationFragment.kt | 17 +- .../SynchronizationPreferencesFragment.kt | 2 +- .../java/ac/mdiq/podcini/storage/DBReader.kt | 2 +- .../podcini/storage/PreferencesTransporter.kt | 97 +++++++++++ .../podcini/ui/activity/PreferenceActivity.kt | 1 + .../ui/activity/SelectSubscriptionActivity.kt | 1 + .../ac/mdiq/podcini/ui/adapter/CoverLoader.kt | 63 +------ .../ui/fragment/AudioPlayerFragment.kt | 2 + .../ui/fragment/EpisodeHomeFragment.kt | 12 +- .../ui/fragment/EpisodeInfoFragment.kt | 20 ++- .../ui/fragment/PlayerDetailsFragment.kt | 4 + .../mdiq/podcini/ui/utils/ShownotesCleaner.kt | 7 - .../view/viewholder/EpisodeItemViewHolder.kt | 46 +++--- .../mdiq/podcini/ui/widget/WidgetUpdater.kt | 2 + app/src/main/res/values/strings.xml | 1 + .../res/xml/preferences_import_export.xml | 24 +-- changelog.md | 6 + .../android/en-US/changelogs/3020142.txt | 6 + 41 files changed, 351 insertions(+), 393 deletions(-) rename app/src/androidTest/java/ac/test/podcini/service/playback/{CancelablePSMPCallback.kt => CancelableMediaPlayerCallback.kt} (92%) rename app/src/androidTest/java/ac/test/podcini/service/playback/{DefaultPSMPCallback.kt => DefaultMediaPlayerCallback.kt} (87%) delete mode 100644 app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSynchronizationServiceException.kt create mode 100644 app/src/main/java/ac/mdiq/podcini/storage/PreferencesTransporter.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/3020142.txt diff --git a/README.md b/README.md index 66c097ce..6dcb33c6 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/build.gradle b/app/build.gradle index e4ad5067..29761d1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt similarity index 92% rename from app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt rename to app/src/androidTest/java/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt index ff85bd60..657ae4c4 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/CancelablePSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/CancelableMediaPlayerCallback.kt @@ -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() { diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt similarity index 87% rename from app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt rename to app/src/androidTest/java/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt index 9a0e880d..a0cdb468 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultPSMPCallback.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/DefaultMediaPlayerCallback.kt @@ -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?) { } diff --git a/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt b/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt index 3104f174..0818ee42 100644 --- a/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt +++ b/app/src/androidTest/java/ac/test/podcini/service/playback/MediaPlayerBaseTest.kt @@ -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) { diff --git a/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt index 26d70a41..9f3d58ef 100644 --- a/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt +++ b/app/src/free/java/ac/mdiq/podcini/playback/cast/CastPsmp.kt @@ -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 } } diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt index d2a5b552..51e1d717 100644 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt +++ b/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSyncService.kt @@ -227,7 +227,7 @@ import kotlin.math.min return null } - @Throws(WifiSynchronizationServiceException::class) + @Throws(SyncServiceException::class) override fun uploadSubscriptionChanges(added: List, removed: List): 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): 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, 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) } } diff --git a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSynchronizationServiceException.kt b/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSynchronizationServiceException.kt deleted file mode 100644 index 3cce5f64..00000000 --- a/app/src/main/java/ac/mdiq/podcini/net/sync/wifi/WifiSynchronizationServiceException.kt +++ /dev/null @@ -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) diff --git a/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt b/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt index 8097d4ea..f4433fe5 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/base/MediaPlayerBase.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index 02ecfbf6..b0503545 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -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? = 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 } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt index 94e015e3..10e99de3 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -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): ListenableFuture> { -// 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 -> { diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt index 60f63417..7196cd6f 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceTaskManager.kt @@ -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 { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/MaterialListPreference.kt b/app/src/main/java/ac/mdiq/podcini/preferences/MaterialListPreference.kt index b20ed3ca..f012925b 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/MaterialListPreference.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/MaterialListPreference.kt @@ -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) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/MaterialMultiSelectListPreference.kt b/app/src/main/java/ac/mdiq/podcini/preferences/MaterialMultiSelectListPreference.kt index 393dbd86..92f28a8f 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/MaterialMultiSelectListPreference.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/MaterialMultiSelectListPreference.kt @@ -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 = HashSet() for (i in values.indices) { if (selected[i]) selectedValues.add(entryValues[i].toString()) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt b/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt index 3703996e..9f0735d6 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/PlaybackPreferences.kt @@ -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()) } diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt b/app/src/main/java/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt index 7d163dd5..bb1024de 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/SleepTimerPreferences.kt @@ -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.. 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 + var hiddenDrawerItems: List 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? + var fullNotificationButtons: List 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 = ArrayList() for (button in buttons) { notificationButtons.add(button.toInt()) @@ -509,7 +508,7 @@ object UserPreferences { val defaultValue = HashSet() defaultValue.add("images") val getValueStringSet = prefs.getStringSet(PREF_MOBILE_UPDATE, defaultValue) - val allowed: MutableSet = HashSet(getValueStringSet) + val allowed: MutableSet = HashSet(getValueStringSet!!) if (allow) allowed.add(type) else allowed.remove(type) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index c8bc1d7b..3e634c22 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -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(ActivityResultContracts.GetContent()) { uri: Uri? -> this.chooseOpmlImportPathResult(uri) } - // TODO: implement private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - this.restorePreferencesResult(result) } - private val backupPreferencesLauncher = registerForActivityResult(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 { diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt index cf4eb30b..c62456b6 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/MainPreferencesFragment.kt @@ -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") val config = searchPreference!!.searchConfiguration - config.setActivity((activity as AppCompatActivity?)!!) + config.setActivity((activity as AppCompatActivity)) config.setFragmentContainerViewId(R.id.settingsContainer) config.setBreadcrumbsEnabled(true) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt index 2feeda01..b021db36 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/UserInterfacePreferencesFragment.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/AboutFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/AboutFragment.kt index 7ed4c790..786ac48a 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/AboutFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/AboutFragment.kt @@ -21,7 +21,7 @@ class AboutFragment : PreferenceFragmentCompat() { findPreference("about_version")!!.summary = String.format("%s (%s)", BuildConfig.VERSION_NAME, BuildConfig.COMMIT_HASH) findPreference("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("about_version")!!.summary) clipboard.setPrimaryClip(clip) @@ -29,7 +29,7 @@ class AboutFragment : PreferenceFragmentCompat() { true } findPreference("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("about_privacy_policy")!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { preference: Preference? -> + Preference.OnPreferenceClickListener { openInBrowser(requireContext(), "https://github.com/XilinJia/Podcini/blob/main/PrivacyPolicy.md") true } findPreference("about_licenses")!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { preference: Preference? -> + Preference.OnPreferenceClickListener { parentFragmentManager.beginTransaction() .replace(R.id.settingsContainer, LicensesFragment()) .addToBackStack(getString(R.string.translators)) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt index 414bfd6d..1ec57140 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/about/LicensesFragment.kt @@ -60,7 +60,7 @@ class LicensesFragment : ListFragment() { val items = arrayOf("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) diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt index d82f115c..2a05ee9d 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/GpodderAuthenticationFragment.kt @@ -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 diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/NextcloudAuthenticationFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/NextcloudAuthenticationFragment.kt index 59af69f0..8ca52bdf 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/NextcloudAuthenticationFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/NextcloudAuthenticationFragment.kt @@ -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. diff --git a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt index 07f330f8..5e4a1b65 100644 --- a/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/preferences/fragments/synchronization/SynchronizationPreferencesFragment.kt @@ -98,7 +98,7 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() { true } - val loggedIn = SynchronizationSettings.isProviderConnected + val loggedIn = isProviderConnected val preferenceHeader = findPreference(PREFERENCE_SYNCHRONIZATION_DESCRIPTION) if (loggedIn) { val selectedProvider = SynchronizationProviderViewData.fromIdentifier(selectedSyncProviderKey) diff --git a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt index 078c83b6..d902f353 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/DBReader.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/storage/PreferencesTransporter.kt b/app/src/main/java/ac/mdiq/podcini/storage/PreferencesTransporter.kt new file mode 100644 index 00000000..386b7973 --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/storage/PreferencesTransporter.kt @@ -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 { } + + } +} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt index c3d17b07..2755a897 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/PreferenceActivity.kt @@ -38,6 +38,7 @@ class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener { setTheme(getTheme(this)) super.onCreate(savedInstanceState) + Logd("PreferenceActivity", "onCreate") val ab = supportActionBar ab?.setDisplayHomeAsUpEnabled(true) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt index 0c6c5566..d19325af 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/SelectSubscriptionActivity.kt @@ -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) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/adapter/CoverLoader.kt b/app/src/main/java/ac/mdiq/podcini/ui/adapter/CoverLoader.kt index ed0027d9..5752f014 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/adapter/CoverLoader.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/adapter/CoverLoader.kt @@ -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 = 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) - 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(coverImage) { -// -// private val fallbackTitle: WeakReference = WeakReference(fallbackTitle) -// private val cover: WeakReference = WeakReference(coverImage) -// -// override fun onLoadFailed(errorDrawable: Drawable?) { -// setTitleVisibility(fallbackTitle.get(), true) -// } -// -// override fun onResourceReady(resource: Drawable, transition: Transition?) { -// 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 = WeakReference(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 diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 99b69651..881c0c6b 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt index 93353174..48700ec3 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index 2586f5b6..e7f41828 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -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 { 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 } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 00d38c2d..8144d8d2 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -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() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt b/app/src/main/java/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt index dcf0c453..be3682ba 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/utils/ShownotesCleaner.kt @@ -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) } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt index 7e79cfef..7315b1fb 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/view/viewholder/EpisodeItemViewHolder.kt @@ -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)) + } } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt index 4b529b67..5dbad339 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/widget/WidgetUpdater.kt @@ -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() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b981e53a..7e1dc3f3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -609,6 +609,7 @@ HTML export Preferences export Preferences import + Importing preferences will replace all of your current preferences. If confirmed, choose a previously exported directory with name containing \"Podcini-Prefs\" Database export Database import 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\? diff --git a/app/src/main/res/xml/preferences_import_export.xml b/app/src/main/res/xml/preferences_import_export.xml index c9957be0..60c97e87 100644 --- a/app/src/main/res/xml/preferences_import_export.xml +++ b/app/src/main/res/xml/preferences_import_export.xml @@ -16,18 +16,18 @@ android:summary="@string/database_import_summary"/> - - - - - - - - - - - - + + + +