From 25f811a8bd7f9e825448e3705e2bcf21e231267c Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Sat, 7 Sep 2024 13:31:22 +0100 Subject: [PATCH] 6.5.6 commit --- app/build.gradle | 14 +- app/src/main/AndroidManifest.xml | 4 +- .../podcini/playback/ServiceStatusHandler.kt | 30 +- .../mdiq/podcini/playback/base/VideoMode.kt | 19 + .../playback/service/LocalMediaPlayer.kt | 8 +- .../playback/service/PlaybackService.kt | 15 + .../podcini/preferences/UserPreferences.kt | 537 +++++++++--------- .../ImportExportPreferencesFragment.kt | 94 ++- .../storage/algorithms/AutoDownloads.kt | 6 +- .../ac/mdiq/podcini/storage/database/Feeds.kt | 9 +- .../mdiq/podcini/storage/database/RealmDB.kt | 2 +- .../ac/mdiq/podcini/storage/model/Feed.kt | 3 - .../podcini/storage/model/FeedPreferences.kt | 96 ++-- .../actionbutton/EpisodeActionButton.kt | 15 +- .../actions/actionbutton/PlayActionButton.kt | 15 +- .../actionbutton/PlayLocalActionButton.kt | 2 +- .../actionbutton/StreamActionButton.kt | 14 +- .../ui/activity/VideoplayerActivity.kt | 66 +-- .../starter/VideoPlayerActivityStarter.kt | 5 +- .../ui/fragment/AudioPlayerFragment.kt | 17 +- .../ui/fragment/FeedSettingsFragment.kt | 139 +++-- .../mdiq/podcini/ui/view/ShownotesWebView.kt | 4 +- app/src/main/res/values/strings.xml | 5 + changelog.md | 6 + .../android/en-US/changelogs/3020240.txt | 5 + 25 files changed, 598 insertions(+), 532 deletions(-) create mode 100644 app/src/main/kotlin/ac/mdiq/podcini/playback/base/VideoMode.kt create mode 100644 fastlane/metadata/android/en-US/changelogs/3020240.txt diff --git a/app/build.gradle b/app/build.gradle index 27a7c33b..44b3f858 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,8 +31,8 @@ android { testApplicationId "ac.mdiq.podcini.tests" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - versionCode 3020239 - versionName "6.5.5" + versionCode 3020240 + versionName "6.5.6" applicationId "ac.mdiq.podcini.R" def commit = "" @@ -142,7 +142,6 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard.cfg" resValue "string", "app_name", "Podcini.R" resValue "string", "provider_authority", "ac.mdiq.podcini.R.provider" -// debuggable false vcsInfo.include false minifyEnabled true shrinkResources true @@ -180,18 +179,11 @@ dependencies { implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6' -// implementation 'androidx.compose.material3:material3:1.2.0' implementation 'androidx.compose.material:material:1.7.0' -// implementation 'androidx.compose.foundation:foundation:1.6.2' implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0' debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0' - // Optional - Add full set of material icons -// implementation 'androidx.compose.material:material-icons-extended' - // Optional - Add window size utils -// implementation 'androidx.compose.material3:material3-window-size-class' - implementation 'androidx.activity:activity-compose:1.9.2' implementation 'androidx.window:window:1.3.0' @@ -204,7 +196,6 @@ dependencies { implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation "androidx.fragment:fragment-ktx:1.8.3" implementation 'androidx.gridlayout:gridlayout:1.0.0' -// implementation "androidx.media:media:1.7.0" implementation "androidx.media3:media3-exoplayer:1.4.1" implementation "androidx.media3:media3-ui:1.4.1" implementation "androidx.media3:media3-datasource-okhttp:1.4.1" @@ -233,7 +224,6 @@ dependencies { implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" implementation 'com.squareup.okio:okio:3.9.0' -// implementation "io.reactivex.rxjava2:rxandroid:2.1.1" implementation "io.reactivex.rxjava2:rxjava:2.2.21" implementation "io.reactivex.rxjava3:rxjava:3.1.8" implementation "io.reactivex.rxjava3:rxandroid:3.0.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e0af24a..478cbbe8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,8 +45,8 @@ android:supportsRtl="true" android:logo="@mipmap/ic_launcher" android:resizeableActivity="true" - android:allowAudioPlaybackCapture="true"> - + android:allowAudioPlaybackCapture="true" + android:networkSecurityConfig="@xml/network_security_config"> ().firstOrNull { it.code == code } ?: NONE + } + fun fromTag(tag: String): VideoMode { + return enumValues().firstOrNull { it.tag == tag } ?: NONE + } + } +} diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt index 7bc40868..4d6da2b3 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/LocalMediaPlayer.kt @@ -12,12 +12,10 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerCallback import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope -import ac.mdiq.podcini.storage.model.EpisodeMedia -import ac.mdiq.podcini.storage.model.Feed -import ac.mdiq.podcini.storage.model.MediaType -import ac.mdiq.podcini.storage.model.Playable +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent @@ -197,7 +195,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP val audioStream = audioStreamsList[audioIndex] Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate}") val aSource = DefaultMediaSourceFactory(context).createMediaSource(MediaItem.Builder().setTag(metadata).setUri(Uri.parse(audioStream.content)).build()) - if (media.episode?.feed?.preferences?.playAudioOnly != true) { + if (media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) { Logd(TAG, "setDataSource1 result: $streamInfo") Logd(TAG, "setDataSource1 videoStreams: ${streamInfo.videoStreams.size} videoOnlyStreams: ${streamInfo.videoOnlyStreams.size} audioStreams: ${streamInfo.audioStreams.size}") val videoStreamsList = getSortedStreamVideosList(streamInfo.videoStreams, streamInfo.videoOnlyStreams, true, true) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 5fec5816..52c1f420 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -52,6 +52,8 @@ import ac.mdiq.podcini.storage.model.CurrentState.Companion.PLAYER_STATUS_PLAYIN import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded +import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter +import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState import ac.mdiq.podcini.util.EventFlow @@ -1440,5 +1442,18 @@ class PlaybackService : MediaLibraryService() { } } } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played or the medaitype that is provided as an argument. + * If the playbackservice is not running, the type of the last played media will be looked up. + */ + @JvmStatic + fun getPlayerActivityIntent(context: Context, mediaType_: MediaType? = null): Intent { + val mediaType = mediaType_ ?: currentMediaType + val showVideoPlayer = if (isRunning) mediaType == MediaType.VIDEO && !isCasting else curState.curIsVideo + return if (showVideoPlayer) VideoPlayerActivityStarter(context).intent + else MainActivityStarter(context).withOpenPlayer().getIntent() + } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt index b1764b80..615dc049 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/UserPreferences.kt @@ -23,6 +23,269 @@ import java.net.Proxy object UserPreferences { private val TAG: String = UserPreferences::class.simpleName ?: "Anonymous" + // Experimental + const val EPISODE_CLEANUP_QUEUE: Int = -1 + const val EPISODE_CLEANUP_NULL: Int = -2 + const val EPISODE_CLEANUP_EXCEPT_FAVORITE: Int = -3 + const val EPISODE_CLEANUP_DEFAULT: Int = 0 + + const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1 + + const val DEFAULT_PAGE_REMEMBER: String = "remember" + + private lateinit var context: Context + lateinit var appPrefs: SharedPreferences + + var theme: ThemePreference + get() = when (appPrefs.getString(Prefs.prefTheme.name, "system")) { + "0" -> ThemePreference.LIGHT + "1" -> ThemePreference.DARK + else -> ThemePreference.SYSTEM + } + set(theme) { + when (theme) { + ThemePreference.LIGHT -> appPrefs.edit().putString(Prefs.prefTheme.name, "0").apply() + ThemePreference.DARK -> appPrefs.edit().putString(Prefs.prefTheme.name, "1").apply() + else -> appPrefs.edit().putString(Prefs.prefTheme.name, "system").apply() + } + } + + val isBlackTheme: Boolean + get() = appPrefs.getBoolean(Prefs.prefThemeBlack.name, false) + + val isThemeColorTinted: Boolean + get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false) + + var hiddenDrawerItems: List + get() { + val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "") + return hiddenItems?.split(",") ?: listOf() + } + set(items) { + val str = items.joinToString() + appPrefs.edit().putString(Prefs.prefHiddenDrawerItems.name, str).apply() + } + + var fullNotificationButtons: List + get() { + val buttons = appPrefs.getString(Prefs.prefFullNotificationButtons.name, "${NOTIFICATION_BUTTON.SKIP.ordinal},${NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal}")?.split(",") ?: listOf() + val notificationButtons: MutableList = ArrayList() + for (button in buttons) { + notificationButtons.add(button.toInt()) + } + return notificationButtons + } + set(items) { + val str = items.joinToString() + appPrefs.edit().putString(Prefs.prefFullNotificationButtons.name, str).apply() + } + + val isAutoDelete: Boolean + get() = appPrefs.getBoolean(Prefs.prefAutoDelete.name, false) + + val isAutoDeleteLocal: Boolean + get() = appPrefs.getBoolean(Prefs.prefAutoDeleteLocal.name, false) + + val videoPlayMode: Int + get() { + try { return appPrefs.getString(Prefs.prefVideoPlaybackMode.name, "1")!!.toInt() + } catch (e: NumberFormatException) { + Log.e(TAG, Log.getStackTraceString(e)) + setVideoMode(1) + return 1 + } + } + + var videoPlaybackSpeed: Float + get() { + try { return appPrefs.getString(Prefs.prefVideoPlaybackSpeed.name, "1.00")!!.toFloat() + } catch (e: NumberFormatException) { + Log.e(TAG, Log.getStackTraceString(e)) + videoPlaybackSpeed = 1.0f + return 1.0f + } + } + set(speed) { + appPrefs.edit().putString(Prefs.prefVideoPlaybackSpeed.name, speed.toString()).apply() + } + + var isSkipSilence: Boolean + get() = appPrefs.getBoolean(Prefs.prefSkipSilence.name, false) + set(skipSilence) { + appPrefs.edit().putBoolean(Prefs.prefSkipSilence.name, skipSilence).apply() + } + + /** + * Returns the capacity of the episode cache. This method will return the + * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to + * 'unlimited'. + */ + val episodeCacheSize: Int + get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt() + + @set:VisibleForTesting + var isEnableAutodownload: Boolean + get() = appPrefs.getBoolean(Prefs.prefEnableAutoDl.name, false) + set(enabled) { + appPrefs.edit().putBoolean(Prefs.prefEnableAutoDl.name, enabled).apply() + } + + val isEnableAutodownloadOnBattery: Boolean + get() = appPrefs.getBoolean(Prefs.prefEnableAutoDownloadOnBattery.name, true) + + var speedforwardSpeed: Float + get() { + try { return appPrefs.getString(Prefs.prefSpeedforwardSpeed.name, "0.00")!!.toFloat() + } catch (e: NumberFormatException) { + Log.e(TAG, Log.getStackTraceString(e)) + speedforwardSpeed = 0.0f + return 0.0f + } + } + set(speed) { + appPrefs.edit().putString(Prefs.prefSpeedforwardSpeed.name, speed.toString()).apply() + } + + var fallbackSpeed: Float + get() { + try { return appPrefs.getString(Prefs.prefFallbackSpeed.name, "0.00")!!.toFloat() + } catch (e: NumberFormatException) { + Log.e(TAG, Log.getStackTraceString(e)) + fallbackSpeed = 0.0f + return 0.0f + } + } + set(speed) { + appPrefs.edit().putString(Prefs.prefFallbackSpeed.name, speed.toString()).apply() + } + + var fastForwardSecs: Int + get() = appPrefs.getInt(Prefs.prefFastForwardSecs.name, 30) + set(secs) { + appPrefs.edit().putInt(Prefs.prefFastForwardSecs.name, secs).apply() + } + + var rewindSecs: Int + get() = appPrefs.getInt(Prefs.prefRewindSecs.name, 10) + set(secs) { + appPrefs.edit().putInt(Prefs.prefRewindSecs.name, secs).apply() + } + + var proxyConfig: ProxyConfig + get() { + val type = Proxy.Type.valueOf(appPrefs.getString(Prefs.prefProxyType.name, Proxy.Type.DIRECT.name)!!) + val host = appPrefs.getString(Prefs.prefProxyHost.name, null) + val port = appPrefs.getInt(Prefs.prefProxyPort.name, 0) + val username = appPrefs.getString(Prefs.prefProxyUser.name, null) + val password = appPrefs.getString(Prefs.prefProxyPassword.name, null) + return ProxyConfig(type, host, port, username, password) + } + set(config) { + val editor = appPrefs.edit() + editor.putString(Prefs.prefProxyType.name, config.type.name) + if (config.host.isNullOrEmpty()) editor.remove(Prefs.prefProxyHost.name) + else editor.putString(Prefs.prefProxyHost.name, config.host) + + if (config.port <= 0 || config.port > 65535) editor.remove(Prefs.prefProxyPort.name) + else editor.putInt(Prefs.prefProxyPort.name, config.port) + + if (config.username.isNullOrEmpty()) editor.remove(Prefs.prefProxyUser.name) + else editor.putString(Prefs.prefProxyUser.name, config.username) + + if (config.password.isNullOrEmpty()) editor.remove(Prefs.prefProxyPassword.name) + else editor.putString(Prefs.prefProxyPassword.name, config.password) + + editor.apply() + } + + var defaultPage: String? + get() = appPrefs.getString(Prefs.prefDefaultPage.name, "SubscriptionsFragment") + set(defaultPage) { + appPrefs.edit().putString(Prefs.prefDefaultPage.name, defaultPage).apply() + } + + var isStreamOverDownload: Boolean + get() = appPrefs.getBoolean(Prefs.prefStreamOverDownload.name, false) + set(stream) { + appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply() + } + + /** + * Sets up the UserPreferences class. + * @throws IllegalArgumentException if context is null + */ + fun init(context: Context) { + Logd(TAG, "Creating new instance of UserPreferences") + UserPreferences.context = context.applicationContext + FilesUtils.context = context.applicationContext + appPrefs = PreferenceManager.getDefaultSharedPreferences(context) + createNoMediaFile() + } + + /** + * Helper function to return whether the specified button should be shown on full + * notifications. + * @param buttonId Either NOTIFICATION_BUTTON_REWIND, NOTIFICATION_BUTTON_FAST_FORWARD, + * NOTIFICATION_BUTTON.SKIP.ordinal, NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal + * or NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal. + * @return `true` if button should be shown, `false` otherwise + */ + private fun showButtonOnFullNotification(buttonId: Int): Boolean { + return fullNotificationButtons.contains(buttonId) + } + +// only used in test + fun showSkipOnFullNotification(): Boolean { + return showButtonOnFullNotification(NOTIFICATION_BUTTON.SKIP.ordinal) + } + + // only used in test + fun showNextChapterOnFullNotification(): Boolean { + return showButtonOnFullNotification(NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal) + } + + // only used in test + fun showPlaybackSpeedOnFullNotification(): Boolean { + return showButtonOnFullNotification(NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal) + } + + /** + * @return `true` if we should show remaining time or the duration + */ + fun shouldShowRemainingTime(): Boolean { + return appPrefs.getBoolean(Prefs.showTimeLeft.name, false) + } + + /** + * Sets the preference for whether we show the remain time, if not show the duration. This will + * send out events so the current playing screen, queue and the episode list would refresh + * @return `true` if we should show remaining time or the duration + */ + fun setShowRemainTimeSetting(showRemain: Boolean?) { + appPrefs.edit().putBoolean(Prefs.showTimeLeft.name, showRemain!!).apply() + } + +// only used in test + fun shouldPauseForFocusLoss(): Boolean { + return appPrefs.getBoolean(Prefs.prefPauseForFocusLoss.name, true) + } + + fun backButtonOpensDrawer(): Boolean { + return appPrefs.getBoolean(Prefs.prefBackButtonOpensDrawer.name, false) + } + + fun timeRespectsSpeed(): Boolean { + return appPrefs.getBoolean(Prefs.prefPlaybackTimeRespectsSpeed.name, false) + } + + fun setPlaybackSpeed(speed: Float) { + appPrefs.edit().putString(Prefs.prefPlaybackSpeed.name, speed.toString()).apply() + } + + fun setVideoMode(mode: Int) { + appPrefs.edit().putString(Prefs.prefVideoPlaybackMode.name, mode.toString()).apply() + } + @Suppress("EnumEntryName") enum class Prefs { prefOPMLBackup, @@ -109,12 +372,6 @@ object UserPreferences { prefVideoPlaybackMode, } - // Experimental - const val EPISODE_CLEANUP_QUEUE: Int = -1 - const val EPISODE_CLEANUP_NULL: Int = -2 - const val EPISODE_CLEANUP_EXCEPT_FAVORITE: Int = -3 - const val EPISODE_CLEANUP_DEFAULT: Int = 0 - // Constants enum class NOTIFICATION_BUTTON { REWIND, @@ -124,274 +381,6 @@ object UserPreferences { PLAYBACK_SPEED, } - const val EPISODE_CACHE_SIZE_UNLIMITED: Int = -1 - - const val DEFAULT_PAGE_REMEMBER: String = "remember" - - private lateinit var context: Context - lateinit var appPrefs: SharedPreferences - - var theme: ThemePreference - get() = when (appPrefs.getString(Prefs.prefTheme.name, "system")) { - "0" -> ThemePreference.LIGHT - "1" -> ThemePreference.DARK - else -> ThemePreference.SYSTEM - } - set(theme) { - when (theme) { - ThemePreference.LIGHT -> appPrefs.edit().putString(Prefs.prefTheme.name, "0").apply() - ThemePreference.DARK -> appPrefs.edit().putString(Prefs.prefTheme.name, "1").apply() - else -> appPrefs.edit().putString(Prefs.prefTheme.name, "system").apply() - } - } - - val isBlackTheme: Boolean - get() = appPrefs.getBoolean(Prefs.prefThemeBlack.name, false) - - val isThemeColorTinted: Boolean - get() = Build.VERSION.SDK_INT >= 31 && appPrefs.getBoolean(Prefs.prefTintedColors.name, false) - - var hiddenDrawerItems: List - get() { - val hiddenItems = appPrefs.getString(Prefs.prefHiddenDrawerItems.name, "") - return hiddenItems?.split(",") ?: listOf() - } - set(items) { - val str = items.joinToString() - appPrefs.edit() - .putString(Prefs.prefHiddenDrawerItems.name, str) - .apply() - } - - var fullNotificationButtons: List - get() { - val buttons = appPrefs.getString(Prefs.prefFullNotificationButtons.name, "${NOTIFICATION_BUTTON.SKIP.ordinal},${NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal}")?.split(",") ?: listOf() - val notificationButtons: MutableList = ArrayList() - for (button in buttons) { - notificationButtons.add(button.toInt()) - } - return notificationButtons - } - set(items) { - val str = items.joinToString() - appPrefs.edit() - .putString(Prefs.prefFullNotificationButtons.name, str) - .apply() - } - - val isAutoDelete: Boolean - get() = appPrefs.getBoolean(Prefs.prefAutoDelete.name, false) - - val isAutoDeleteLocal: Boolean - get() = appPrefs.getBoolean(Prefs.prefAutoDeleteLocal.name, false) - - val videoPlayMode: Int - get() { - try { - return appPrefs.getString(Prefs.prefVideoPlaybackMode.name, "1")!!.toInt() - } catch (e: NumberFormatException) { - Log.e(TAG, Log.getStackTraceString(e)) - setVideoMode(1) - return 1 - } - } - - var videoPlaybackSpeed: Float - get() { - try { - return appPrefs.getString(Prefs.prefVideoPlaybackSpeed.name, "1.00")!!.toFloat() - } catch (e: NumberFormatException) { - Log.e(TAG, Log.getStackTraceString(e)) - videoPlaybackSpeed = 1.0f - return 1.0f - } - } - set(speed) { - appPrefs.edit() - .putString(Prefs.prefVideoPlaybackSpeed.name, speed.toString()) - .apply() - } - - var isSkipSilence: Boolean - get() = appPrefs.getBoolean(Prefs.prefSkipSilence.name, false) - set(skipSilence) { - appPrefs.edit().putBoolean(Prefs.prefSkipSilence.name, skipSilence).apply() - } - - /** - * Returns the capacity of the episode cache. This method will return the - * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to - * 'unlimited'. - */ - val episodeCacheSize: Int - get() = appPrefs.getString(Prefs.prefEpisodeCacheSize.name, "20")!!.toInt() - - @set:VisibleForTesting - var isEnableAutodownload: Boolean - get() = appPrefs.getBoolean(Prefs.prefEnableAutoDl.name, false) - set(enabled) { - appPrefs.edit().putBoolean(Prefs.prefEnableAutoDl.name, enabled).apply() - } - - val isEnableAutodownloadOnBattery: Boolean - get() = appPrefs.getBoolean(Prefs.prefEnableAutoDownloadOnBattery.name, true) - - var speedforwardSpeed: Float - get() { - try { - return appPrefs.getString(Prefs.prefSpeedforwardSpeed.name, "0.00")!!.toFloat() - } catch (e: NumberFormatException) { - Log.e(TAG, Log.getStackTraceString(e)) - speedforwardSpeed = 0.0f - return 0.0f - } - } - set(speed) { - appPrefs.edit().putString(Prefs.prefSpeedforwardSpeed.name, speed.toString()).apply() - } - - var fallbackSpeed: Float - get() { - try { - return appPrefs.getString(Prefs.prefFallbackSpeed.name, "0.00")!!.toFloat() - } catch (e: NumberFormatException) { - Log.e(TAG, Log.getStackTraceString(e)) - fallbackSpeed = 0.0f - return 0.0f - } - } - set(speed) { - appPrefs.edit().putString(Prefs.prefFallbackSpeed.name, speed.toString()).apply() - } - - var fastForwardSecs: Int - get() = appPrefs.getInt(Prefs.prefFastForwardSecs.name, 30) - set(secs) { - appPrefs.edit().putInt(Prefs.prefFastForwardSecs.name, secs).apply() - } - - var rewindSecs: Int - get() = appPrefs.getInt(Prefs.prefRewindSecs.name, 10) - set(secs) { - appPrefs.edit().putInt(Prefs.prefRewindSecs.name, secs).apply() - } - - var proxyConfig: ProxyConfig - get() { - val type = Proxy.Type.valueOf(appPrefs.getString(Prefs.prefProxyType.name, Proxy.Type.DIRECT.name)!!) - val host = appPrefs.getString(Prefs.prefProxyHost.name, null) - val port = appPrefs.getInt(Prefs.prefProxyPort.name, 0) - val username = appPrefs.getString(Prefs.prefProxyUser.name, null) - val password = appPrefs.getString(Prefs.prefProxyPassword.name, null) - return ProxyConfig(type, host, port, username, password) - } - set(config) { - val editor = appPrefs.edit() - editor.putString(Prefs.prefProxyType.name, config.type.name) - if (config.host.isNullOrEmpty()) editor.remove(Prefs.prefProxyHost.name) - else editor.putString(Prefs.prefProxyHost.name, config.host) - - if (config.port <= 0 || config.port > 65535) editor.remove(Prefs.prefProxyPort.name) - else editor.putInt(Prefs.prefProxyPort.name, config.port) - - if (config.username.isNullOrEmpty()) editor.remove(Prefs.prefProxyUser.name) - else editor.putString(Prefs.prefProxyUser.name, config.username) - - if (config.password.isNullOrEmpty()) editor.remove(Prefs.prefProxyPassword.name) - else editor.putString(Prefs.prefProxyPassword.name, config.password) - - editor.apply() - } - - var defaultPage: String? - get() = appPrefs.getString(Prefs.prefDefaultPage.name, "SubscriptionsFragment") - set(defaultPage) { - appPrefs.edit().putString(Prefs.prefDefaultPage.name, defaultPage).apply() - } - - var isStreamOverDownload: Boolean - get() = appPrefs.getBoolean(Prefs.prefStreamOverDownload.name, false) - set(stream) { - appPrefs.edit().putBoolean(Prefs.prefStreamOverDownload.name, stream).apply() - } - - /** - * Sets up the UserPreferences class. - * @throws IllegalArgumentException if context is null - */ - fun init(context: Context) { - Logd(TAG, "Creating new instance of UserPreferences") - UserPreferences.context = context.applicationContext - FilesUtils.context = context.applicationContext - appPrefs = PreferenceManager.getDefaultSharedPreferences(context) - - createNoMediaFile() - } - - /** - * Helper function to return whether the specified button should be shown on full - * notifications. - * @param buttonId Either NOTIFICATION_BUTTON_REWIND, NOTIFICATION_BUTTON_FAST_FORWARD, - * NOTIFICATION_BUTTON.SKIP.ordinal, NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal - * or NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal. - * @return `true` if button should be shown, `false` otherwise - */ - private fun showButtonOnFullNotification(buttonId: Int): Boolean { - return fullNotificationButtons.contains(buttonId) - } - -// only used in test - fun showSkipOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON.SKIP.ordinal) - } - - // only used in test - fun showNextChapterOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON.NEXT_CHAPTER.ordinal) - } - - // only used in test - fun showPlaybackSpeedOnFullNotification(): Boolean { - return showButtonOnFullNotification(NOTIFICATION_BUTTON.PLAYBACK_SPEED.ordinal) - } - - /** - * @return `true` if we should show remaining time or the duration - */ - fun shouldShowRemainingTime(): Boolean { - return appPrefs.getBoolean(Prefs.showTimeLeft.name, false) - } - - /** - * Sets the preference for whether we show the remain time, if not show the duration. This will - * send out events so the current playing screen, queue and the episode list would refresh - * @return `true` if we should show remaining time or the duration - */ - fun setShowRemainTimeSetting(showRemain: Boolean?) { - appPrefs.edit().putBoolean(Prefs.showTimeLeft.name, showRemain!!).apply() - } - -// only used in test - fun shouldPauseForFocusLoss(): Boolean { - return appPrefs.getBoolean(Prefs.prefPauseForFocusLoss.name, true) - } - - fun backButtonOpensDrawer(): Boolean { - return appPrefs.getBoolean(Prefs.prefBackButtonOpensDrawer.name, false) - } - - fun timeRespectsSpeed(): Boolean { - return appPrefs.getBoolean(Prefs.prefPlaybackTimeRespectsSpeed.name, false) - } - - fun setPlaybackSpeed(speed: Float) { - appPrefs.edit().putString(Prefs.prefPlaybackSpeed.name, speed.toString()).apply() - } - - fun setVideoMode(mode: Int) { - appPrefs.edit().putString(Prefs.prefVideoPlaybackMode.name, mode.toString()).apply() - } - enum class ThemePreference { LIGHT, DARK, BLACK, SYSTEM } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt index baedef7d..2200765d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/ImportExportPreferencesFragment.kt @@ -190,11 +190,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { val fileUri = FileProvider.getUriForFile(context!!.applicationContext, context.getString(R.string.provider_authority), output!!) showExportSuccessSnackbar(fileUri, exportType.contentType) } - } catch (e: Exception) { - showTransportErrorDialog(e) - } finally { - progressDialog!!.dismiss() - } + } catch (e: Exception) { showTransportErrorDialog(e) + } finally { progressDialog!!.dismiss() } } } else { lifecycleScope.launch(Dispatchers.IO) { @@ -204,18 +201,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { withContext(Dispatchers.Main) { showExportSuccessSnackbar(output.uri, exportType.contentType) } - } catch (e: Exception) { - showTransportErrorDialog(e) - } finally { - progressDialog!!.dismiss() - } + } catch (e: Exception) { showTransportErrorDialog(e) + } finally { progressDialog!!.dismiss() } } } } private fun exportPreferences() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.addCategory(Intent.CATEGORY_DEFAULT) backupPreferencesLauncher.launch(intent) } @@ -240,7 +234,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { private fun exportMediaFiles() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.addCategory(Intent.CATEGORY_DEFAULT) backupMediaFilesLauncher.launch(intent) } @@ -277,6 +271,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { builder.setNegativeButton(R.string.no, null) builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.setType("*/*") intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream")) intent.addCategory(Intent.CATEGORY_OPENABLE) @@ -321,6 +316,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { builder.setNegativeButton(R.string.no, null) builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int -> val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.setType("*/*") intent.addCategory(Intent.CATEGORY_OPENABLE) restoreProgressLauncher.launch(intent) @@ -331,25 +327,33 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { private fun chooseProgressExportPathResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) return - val uri = result.data!!.data + val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) exportWithWriter(EpisodesProgressWriter(), uri, Export.PROGRESS) } private fun chooseOpmlExportPathResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) return - val uri = result.data!!.data + val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) exportWithWriter(OpmlWriter(), uri, Export.OPML) } private fun chooseHtmlExportPathResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) return - val uri = result.data!!.data + val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) exportWithWriter(HtmlWriter(), uri, Export.HTML) } private fun chooseFavoritesExportPathResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data == null) return - val uri = result.data!!.data + val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) exportWithWriter(FavoritesWriter(), uri, Export.FAVORITES) } @@ -357,6 +361,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { if (result.resultCode != RESULT_OK || result.data?.data == null) return val uri = result.data!!.data uri?.let { +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) if (isJsonFile(uri)) { progressDialog!!.show() lifecycleScope.launch { @@ -371,9 +377,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showImportSuccessDialog() progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } else { val context = requireContext() @@ -392,6 +396,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { if (result.resultCode != RESULT_OK || result.data == null) return val uri = result.data!!.data uri?.let { +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) if (isRealmFile(uri)) { progressDialog!!.show() lifecycleScope.launch { @@ -403,9 +409,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showImportSuccessDialog() progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } else { val context = requireContext() @@ -433,6 +437,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { private fun restorePreferencesResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data?.data == null) return val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) if (isPrefDir(uri)) { progressDialog!!.show() lifecycleScope.launch { @@ -444,9 +450,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showImportSuccessDialog() progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } else { val context = requireContext() @@ -458,6 +462,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { private fun restoreMediaFilesResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data?.data == null) return val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) if (isMediaFilesDir(uri)) { progressDialog!!.show() lifecycleScope.launch { @@ -469,9 +475,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showImportSuccessDialog() progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } else { val context = requireContext() @@ -483,6 +487,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { private fun exportMediaFilesResult(result: ActivityResult) { if (result.resultCode != RESULT_OK || result.data?.data == null) return val uri = result.data!!.data!! +// val takeFlags = result.data?.flags?.and(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) ?: 0 +// requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags) progressDialog!!.show() lifecycleScope.launch { try { @@ -493,9 +499,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showExportSuccessSnackbar(uri, null) progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } @@ -511,9 +515,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { showExportSuccessSnackbar(uri, "application/x-sqlite3") progressDialog!!.dismiss() } - } catch (e: Throwable) { - showTransportErrorDialog(e) - } + } catch (e: Throwable) { showTransportErrorDialog(e) } } } @@ -537,9 +539,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { try { result.launch(intentPickAction) return - } catch (e: ActivityNotFoundException) { - Log.e(TAG, "No activity found. Should never happen...") - } + } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") } // If we are using a SDK lower than API 21 or the implicit intent failed // fallback to the legacy export process @@ -578,8 +578,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { Logd(TAG, "feeds_: ${feeds_.size}") exportWriter.writeDocument(feeds_, writer, context) output - } catch (e: IOException) { - throw e + } catch (e: IOException) { throw e } finally { if (writer != null) try { writer.close() } catch (e: IOException) { throw e } if (outputStream != null) try { outputStream.close() } catch (e: IOException) { throw e } @@ -610,9 +609,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } catch (e: IOException) { Log.e(TAG, "Error during file export", e) null // return null in case of error - } finally { - writer?.close() - } + } finally { writer?.close() } } } companion object { @@ -637,9 +634,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { val destFile = exportSubDir.createFile("text/xml", file.name) if (destFile != null) copyFile(file, destFile, context) } - } else { - Log.e("Error", "shared_prefs directory not found") - } + } else Log.e("Error", "shared_prefs directory not found") } catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) throw e @@ -730,8 +725,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { object MediaFilesTransporter { private val TAG: String = MediaFilesTransporter::class.simpleName ?: "Anonymous" var feed: Feed? = null - val nameFeedMap: MutableMap = mutableMapOf() - val nameEpisodeMap: MutableMap = mutableMapOf() + private val nameFeedMap: MutableMap = mutableMapOf() + private val nameEpisodeMap: MutableMap = mutableMapOf() @Throws(IOException::class) fun exportToDocument(uri: Uri, context: Context) { try { @@ -913,9 +908,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } catch (e: IOException) { Log.e(TAG, Log.getStackTraceString(e)) throw e - } finally { - IOUtils.closeQuietly(inputStream) - } + } finally { IOUtils.closeQuietly(inputStream) } } } @@ -1124,6 +1117,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() { } } + @Suppress("EnumEntryName") private enum class IExport { prefOpmlExport, prefOpmlImport, diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt index 9129ae9a..0c064325 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/algorithms/AutoDownloads.kt @@ -117,15 +117,15 @@ object AutoDownloads { if (allowedDLCount > 0) { var queryString = "feedId == ${f.id} AND isAutoDownloadEnabled == true AND media != nil AND media.downloaded == false" when (f.preferences?.autoDLPolicy) { - FeedPreferences.AutoDLPolicy.ONLY_NEW -> { + FeedPreferences.AutoDownloadPolicy.ONLY_NEW -> { queryString += " AND playState == -1 SORT(pubDate DESC) LIMIT(${3*allowedDLCount})" episodes = realm.query(Episode::class).query(queryString).find().toMutableList() } - FeedPreferences.AutoDLPolicy.NEWER -> { + FeedPreferences.AutoDownloadPolicy.NEWER -> { queryString += " AND playState != 1 SORT(pubDate DESC) LIMIT(${3*allowedDLCount})" episodes = realm.query(Episode::class).query(queryString).find().toMutableList() } - FeedPreferences.AutoDLPolicy.OLDER -> { + FeedPreferences.AutoDownloadPolicy.OLDER -> { queryString += " AND playState != 1 SORT(pubDate ASC) LIMIT(${3*allowedDLCount})" episodes = realm.query(Episode::class).query(queryString).find().toMutableList() } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt index 592a5c44..1c32bcfa 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/Feeds.kt @@ -211,10 +211,11 @@ object Feeds { Logd(TAG, "New feed has a higher page number.") savedFeed.nextPageLink = newFeed.nextPageLink } - if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) { - Logd(TAG, "Feed has updated preferences. Updating old feed's preferences") - savedFeed.preferences!!.updateFromOther(newFeed.preferences) - } +// appears not useful +// if (savedFeed.preferences != null && savedFeed.preferences!!.compareWithOther(newFeed.preferences)) { +// Logd(TAG, "Feed has updated preferences. Updating old feed's preferences") +// savedFeed.preferences!!.updateFromOther(newFeed.preferences) +// } val priorMostRecent = savedFeed.mostRecentItem val priorMostRecentDate: Date? = priorMostRecent?.getPubDate() var idLong = Feed.newId() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt index 6c9e1654..61959f4d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/database/RealmDB.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.ContinuationInterceptor object RealmDB { private val TAG: String = RealmDB::class.simpleName ?: "Anonymous" - private const val SCHEMA_VERSION_NUMBER = 21L + private const val SCHEMA_VERSION_NUMBER = 22L private val ioScope = CoroutineScope(Dispatchers.IO) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt index 6c83e821..702b94d9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/Feed.kt @@ -243,7 +243,6 @@ class Feed : RealmObject { if (imageUrl == null || imageUrl != other.imageUrl) return true } if (eigenTitle != other.eigenTitle) return true - if (other.identifier != null) { if (identifier == null || identifier != other.identifier) return true } @@ -263,9 +262,7 @@ class Feed : RealmObject { if (paymentLinks.isEmpty() || paymentLinks != other.paymentLinks) return true } if (other.isPaged && !this.isPaged) return true - if (other.nextPageLink != this.nextPageLink) return true - return false } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt index 7a84c2cf..4b5b6603 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/storage/model/FeedPreferences.kt @@ -2,9 +2,9 @@ package ac.mdiq.podcini.storage.model import ac.mdiq.podcini.R import ac.mdiq.podcini.playback.base.InTheatre.curQueue +import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger -import androidx.compose.runtime.mutableStateOf import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.RealmSet @@ -26,7 +26,15 @@ class FeedPreferences : EmbeddedRealmObject { var username: String? = null var password: String? = null - var playAudioOnly: Boolean = false + // var playAudioOnly: Boolean = false + @Ignore + var videoModePolicy: VideoMode = VideoMode.NONE + get() = VideoMode.fromCode(videoMode) + set(value) { + field = value + videoMode = field.code + } + var videoMode: Int = 0 var playSpeed: Float = SPEED_USE_GLOBAL @@ -118,22 +126,57 @@ class FeedPreferences : EmbeddedRealmObject { var countingPlayed: Boolean = true @Ignore - var autoDLPolicy: AutoDLPolicy = AutoDLPolicy.ONLY_NEW - get() = AutoDLPolicy.fromCode(autoDLPolicyCode) + var autoDLPolicy: AutoDownloadPolicy = AutoDownloadPolicy.ONLY_NEW + get() = AutoDownloadPolicy.fromCode(autoDLPolicyCode) set(value) { field = value autoDLPolicyCode = value.code } var autoDLPolicyCode: Int = 0 - enum class AutoDLPolicy(val code: Int, val resId: Int) { + constructor() {} + + constructor(feedID: Long, autoDownload: Boolean, autoDeleteAction: AutoDeleteAction, + volumeAdaptionSetting: VolumeAdaptionSetting?, username: String?, password: String?) { + this.feedID = feedID + this.autoDownload = autoDownload + this.autoDeleteAction = autoDeleteAction + if (volumeAdaptionSetting != null) this.volumeAdaptionSetting = volumeAdaptionSetting + this.username = username + this.password = password + this.autoDelete = autoDeleteAction.code + this.volumeAdaption = volumeAdaptionSetting?.toInteger() ?: 0 + } + +// These appear not needed + /** + * Compare another FeedPreferences with this one. . + * @return True if the two objects are different. + */ +// fun compareWithOther(other: FeedPreferences?): Boolean { +// if (other == null) return true +// if (username != other.username) return true +// if (password != other.password) return true +// return false +// } + + /** + * Update this FeedPreferences object from another one. + */ +// fun updateFromOther(other: FeedPreferences?) { +// if (other == null) return +// this.username = other.username +// this.password = other.password +// } + + enum class AutoDownloadPolicy(val code: Int, val resId: Int) { ONLY_NEW(0, R.string.feed_auto_download_new), NEWER(1, R.string.feed_auto_download_newer), OLDER(2, R.string.feed_auto_download_older); companion object { - fun fromCode(code: Int): AutoDLPolicy { - return enumValues().firstOrNull { it.code == code } ?: ONLY_NEW + fun fromCode(code: Int): AutoDownloadPolicy { + return enumValues().firstOrNull { it.code == code } ?: ONLY_NEW } } } @@ -153,48 +196,11 @@ class FeedPreferences : EmbeddedRealmObject { } } - constructor() {} - - constructor(feedID: Long, autoDownload: Boolean, autoDeleteAction: AutoDeleteAction, - volumeAdaptionSetting: VolumeAdaptionSetting?, username: String?, password: String?) { - this.feedID = feedID - this.autoDownload = autoDownload - this.autoDeleteAction = autoDeleteAction - if (volumeAdaptionSetting != null) this.volumeAdaptionSetting = volumeAdaptionSetting - this.username = username - this.password = password - this.autoDelete = autoDeleteAction.code - this.volumeAdaption = volumeAdaptionSetting?.toInteger() ?: 0 - } - - /** - * Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the - * comparison. - * @return True if the two objects are different. - */ - fun compareWithOther(other: FeedPreferences?): Boolean { - if (other == null) return true - if (username != other.username) return true - if (password != other.password) return true - return false - } - - /** - * Update this FeedPreferences object from another one. The feedID, autoDownload and AutoDeleteAction attributes are excluded - * from the update. - */ - fun updateFromOther(other: FeedPreferences?) { - if (other == null) return - this.username = other.username - this.password = other.password - } - companion object { const val SPEED_USE_GLOBAL: Float = -1f const val TAG_ROOT: String = "#root" const val TAG_SEPARATOR: String = "\u001e" - val FeedAutoDeleteOptions = AutoDeleteAction.values().map { it.tag } - + val FeedAutoDeleteOptions = AutoDeleteAction.entries.map { it.tag } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt index 3b2e7f13..3c9fb1fe 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/EpisodeActionButton.kt @@ -2,10 +2,12 @@ package ac.mdiq.podcini.ui.actions.actionbutton import ac.mdiq.podcini.net.download.service.DownloadServiceInterface import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload -import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying -import ac.mdiq.podcini.storage.model.Feed +import ac.mdiq.podcini.playback.base.VideoMode +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent +import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode +import ac.mdiq.podcini.storage.model.* +import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import android.content.Context import android.view.View import android.widget.ImageView @@ -32,6 +34,13 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis icon.setImageResource(getDrawable()) } + protected fun playVideo(context: Context, media: Playable) { + if (item.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY + && videoPlayMode != VideoMode.AUDIO_ONLY.code && videoMode != VideoMode.AUDIO_ONLY + && media.getMediaType() == MediaType.VIDEO) + context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) + } + @UnstableApi companion object { fun forItem(episode: Episode): EpisodeActionButton { val media = episode.media ?: return TTSActionButton(episode) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt index 06255b57..5e5e7861 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt @@ -1,21 +1,16 @@ package ac.mdiq.podcini.ui.actions.actionbutton import ac.mdiq.podcini.R -import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.base.InTheatre import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService -import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.EpisodeMedia -import ac.mdiq.podcini.storage.model.MediaType -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode -import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent +import ac.mdiq.podcini.util.Logd import android.content.Context import android.util.Log import android.widget.Toast @@ -51,9 +46,11 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) { EventFlow.postEvent(FlowEvent.PlayEvent(item)) } - if (item.feed?.preferences?.playAudioOnly != true && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY - && media.getMediaType() == MediaType.VIDEO) - context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) +// if (item.feed?.preferences?.videoModePolicy != FeedPreferences.VideomodePolicy.AUDIO_ONLY +// && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY +// && media.getMediaType() == MediaType.VIDEO) +// context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) + playVideo(context, media) } /** diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt index fa036ec4..8a5b9f7f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt @@ -1,9 +1,9 @@ package ac.mdiq.podcini.ui.actions.actionbutton import ac.mdiq.podcini.R -import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.base.InTheatre +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.MediaType diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt index 6c803843..f36ef13a 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt @@ -2,18 +2,13 @@ package ac.mdiq.podcini.ui.actions.actionbutton import ac.mdiq.podcini.R import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileStreaming +import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.preferences.UsageStatistics import ac.mdiq.podcini.preferences.UsageStatistics.logAction import ac.mdiq.podcini.storage.model.Episode -import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.storage.model.Playable import ac.mdiq.podcini.storage.model.RemoteMedia -import ac.mdiq.podcini.net.utils.NetworkUtils.isStreamingAllowed -import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import android.content.Context @@ -47,8 +42,11 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) { .start() EventFlow.postEvent(FlowEvent.PlayEvent(item)) - if (item.feed?.preferences?.playAudioOnly != true && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY - && media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) +// if (item.feed?.preferences?.videoModePolicy != FeedPreferences.VideomodePolicy.AUDIO_ONLY +// && videoPlayMode != VideoMode.AUDIO_ONLY.mode && videoMode != VideoMode.AUDIO_ONLY +// && media.getMediaType() == MediaType.VIDEO) +// context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) + playVideo(context, media) } class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 7b9b1fb1..302dc9b6 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -5,14 +5,15 @@ import ac.mdiq.podcini.databinding.AudioControlsBinding import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding import ac.mdiq.podcini.databinding.VideoplayerActivityBinding import ac.mdiq.podcini.playback.ServiceStatusHandler -import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive @@ -36,17 +37,16 @@ import ac.mdiq.podcini.ui.fragment.ChaptersFragment import ac.mdiq.podcini.ui.utils.PictureInPictureUtil import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.view.ShownotesWebView +import ac.mdiq.podcini.util.EventFlow +import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.IntentUtils.openInBrowser import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare -import ac.mdiq.podcini.util.EventFlow -import ac.mdiq.podcini.util.FlowEvent import android.app.Activity import android.app.Dialog import android.content.DialogInterface import android.content.Intent import android.content.pm.ActivityInfo -import android.content.res.Configuration import android.graphics.PixelFormat import android.graphics.drawable.ColorDrawable import android.media.AudioManager @@ -95,18 +95,25 @@ class VideoplayerActivity : CastEnabledActivity() { override fun onCreate(savedInstanceState: Bundle?) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - videoMode = (intent.getSerializableExtra(VIDEO_MODE) as? VideoMode) ?: VideoMode.None - if (videoMode == VideoMode.None) { - videoMode = VideoMode.entries.toTypedArray().getOrElse(videoPlayMode) { VideoMode.WINDOW_VIEW } - if (videoMode == VideoMode.AUDIO_ONLY) { - switchToAudioOnly = true - finish() - } - if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) { - Logd(TAG, "videoMode not selected, use window mode") - videoMode = VideoMode.WINDOW_VIEW - } + var vmCode = 0 + if (curMedia is EpisodeMedia) { + val media_ = curMedia as EpisodeMedia + val vPol = media_.episode?.feed?.preferences?.videoModePolicy + if (vPol != null && vPol != VideoMode.NONE) vmCode = vPol.code } + Logd(TAG, "onCreate vmCode: $vmCode") + if (vmCode == 0) vmCode = videoPlayMode + Logd(TAG, "onCreate vmCode: $vmCode") + videoMode = VideoMode.entries.toTypedArray().getOrElse(vmCode) { VideoMode.WINDOW_VIEW } + if (videoMode == VideoMode.AUDIO_ONLY) { + switchToAudioOnly = true + finish() + } + if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) { + Logd(TAG, "videoMode not selected, use window mode") + videoMode = VideoMode.WINDOW_VIEW + } + supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY) setForVideoMode() super.onCreate(savedInstanceState) @@ -393,13 +400,6 @@ class VideoplayerActivity : CastEnabledActivity() { return super.onKeyUp(keyCode, event) } - enum class VideoMode(val mode: Int) { - None(0), - WINDOW_VIEW(1), - FULL_SCREEN_VIEW(2), - AUDIO_ONLY(3) - } - class PlaybackControlsDialog : DialogFragment() { private lateinit var dialog: AlertDialog private var _binding: AudioControlsBinding? = null @@ -533,7 +533,7 @@ class VideoplayerActivity : CastEnabledActivity() { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) Logd(TAG, "fragment onCreateView") - _binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(activity)) + _binding = VideoEpisodeFragmentBinding.inflate(inflater) root = binding.root statusHandler = newStatusHandler() statusHandler!!.init() @@ -646,17 +646,19 @@ class VideoplayerActivity : CastEnabledActivity() { private fun setupVideoAspectRatio() { if (videoSurfaceCreated) { val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity as Activity) - val videoWidth = if (videoMode == VideoMode.FULL_SCREEN_VIEW) max(windowMetrics.bounds.width(), windowMetrics.bounds.height()) - else min(windowMetrics.bounds.width(), windowMetrics.bounds.height()) - var videoHeight = 0 + val videoWidth = when (videoMode) { + VideoMode.FULL_SCREEN_VIEW -> max(windowMetrics.bounds.width(), windowMetrics.bounds.height()) + VideoMode.WINDOW_VIEW -> min(windowMetrics.bounds.width(), windowMetrics.bounds.height()) + else -> min(windowMetrics.bounds.width(), windowMetrics.bounds.height()) + } + val videoHeight: Int if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) { - Logd(TAG, "setupVideoAspectRatio Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}") + Logd(TAG, "setupVideoAspectRatio video width: ${videoSize!!.first} height: ${videoSize!!.second}") videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt() - Logd(TAG, "setupVideoAspectRatio Width,height of video 1: $videoWidth, $videoHeight") + Logd(TAG, "setupVideoAspectRatio adjusted video width: $videoWidth height: $videoHeight") } else { - Log.e(TAG, "setupVideoAspectRatio Could not determine video size") videoHeight = (videoWidth.toFloat() / 16 * 9).toInt() - Logd(TAG, "setupVideoAspectRatio Width,height of video 2: $videoWidth, $videoHeight") + Logd(TAG, "setupVideoAspectRatio Could not determine video size, use: $videoWidth $videoHeight") } val lp = binding.videoView.layoutParams lp.width = videoWidth @@ -924,9 +926,7 @@ class VideoplayerActivity : CastEnabledActivity() { companion object { private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous" - const val VIDEO_MODE = "Video_Mode" - - var videoMode = VideoMode.None + var videoMode = VideoMode.NONE private val audioTracks: List get() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt index 560cf162..346711be 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/activity/starter/VideoPlayerActivityStarter.kt @@ -2,8 +2,6 @@ package ac.mdiq.podcini.ui.activity.starter import ac.mdiq.podcini.R -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.VIDEO_MODE -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -14,7 +12,7 @@ import androidx.media3.common.util.UnstableApi * Launches the video player activity of the app with specific arguments. * Does not require a dependency on the actual implementation of the activity. */ -@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) { +@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context) { val intent: Intent = Intent(INTENT) val pendingIntent: PendingIntent get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent, @@ -23,7 +21,6 @@ import androidx.media3.common.util.UnstableApi init { intent.setPackage(context.packageName) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) - if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode) } fun start() { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index 5e152dbc..0acaf2af 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -5,17 +5,18 @@ import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding import ac.mdiq.podcini.databinding.PlayerUiFragmentBinding import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.ServiceStatusHandler -import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.base.InTheatre.curEpisode import ac.mdiq.podcini.playback.base.InTheatre.curMedia import ac.mdiq.podcini.playback.base.MediaPlayerBase import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed import ac.mdiq.podcini.playback.base.PlayerStatus +import ac.mdiq.podcini.playback.base.VideoMode import ac.mdiq.podcini.playback.cast.CastEnabledActivity import ac.mdiq.podcini.playback.service.PlaybackService import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.playback.service.PlaybackService.Companion.toggleFallbackSpeed import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive @@ -26,16 +27,12 @@ import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode import ac.mdiq.podcini.receiver.MediaButtonReceiver -import ac.mdiq.podcini.storage.model.Chapter -import ac.mdiq.podcini.storage.model.EpisodeMedia -import ac.mdiq.podcini.storage.model.Playable +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.utils.ChapterUtils import ac.mdiq.podcini.storage.utils.ImageResourceUtils -import ac.mdiq.podcini.storage.model.MediaType import ac.mdiq.podcini.ui.actions.handler.EpisodeMenuHandler import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog @@ -475,7 +472,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } R.id.show_video -> { playPause() - VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start() + VideoPlayerActivityStarter(requireContext()).start() } R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog") R.id.open_feed_item -> { @@ -550,8 +547,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val media = curMedia if (media != null) { val mediaType = media.getMediaType() - if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY - || (media is EpisodeMedia && media.episode?.feed?.preferences?.playAudioOnly == true)) { + if (mediaType == MediaType.AUDIO || videoPlayMode == VideoMode.AUDIO_ONLY.code || videoMode == VideoMode.AUDIO_ONLY + || (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy == VideoMode.AUDIO_ONLY)) { ensureService() (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) } else { @@ -577,7 +574,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar if (curMedia != null) { val media = curMedia!! if (media.getMediaType() == MediaType.VIDEO && MediaPlayerBase.status != PlayerStatus.PLAYING && - (media is EpisodeMedia && media.episode?.feed?.preferences?.playAudioOnly != true)) { + (media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) { playPause() requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType())) } else playPause() diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 6715295d..c66023d1 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -5,14 +5,15 @@ import ac.mdiq.podcini.databinding.AutodownloadFilterDialogBinding import ac.mdiq.podcini.databinding.FeedsettingsBinding import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnce +import ac.mdiq.podcini.playback.base.VideoMode +import ac.mdiq.podcini.playback.base.VideoMode.Companion.videoModeTags import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk import ac.mdiq.podcini.storage.model.* -import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDLPolicy -import ac.mdiq.podcini.storage.model.FeedPreferences.AutoDeleteAction +import ac.mdiq.podcini.storage.model.FeedPreferences.* import ac.mdiq.podcini.storage.model.FeedPreferences.Companion.FeedAutoDeleteOptions import ac.mdiq.podcini.ui.adapter.SimpleChipAdapter import ac.mdiq.podcini.ui.compose.CustomTheme @@ -64,7 +65,9 @@ class FeedSettingsFragment : Fragment() { private var feed: Feed? = null private var autoDeleteSummaryResId by mutableIntStateOf(R.string.global_default) private var curPrefQueue by mutableStateOf(feed?.preferences?.queueTextExt ?: "Default") - private var autoDeletePolicy = "global" + private var autoDeletePolicy = AutoDeleteAction.GLOBAL.name + private var videoModeSummaryResId by mutableIntStateOf(R.string.global_default) + private var videoMode = VideoMode.NONE.name private var queues: List? = null private var notificationPermissionDenied: Boolean = false @@ -88,6 +91,7 @@ class FeedSettingsFragment : Fragment() { val toolbar = binding.toolbar toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } + getVideoModePolicy() getAutoDeletePolicy() binding.composeView.setContent { @@ -130,30 +134,27 @@ class FeedSettingsFragment : Fragment() { // prefer play audio only Column { Row(Modifier.fillMaxWidth()) { - Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), - "", - tint = textColor) + Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor) Spacer(modifier = Modifier.width(20.dp)) Text( - text = stringResource(R.string.pref_audio_only_title), + text = stringResource(R.string.feed_video_mode_label), style = MaterialTheme.typography.h6, - color = textColor - ) - Spacer(modifier = Modifier.weight(1f)) - var checked by remember { mutableStateOf(feed?.preferences?.playAudioOnly ?: false) } - Switch( - checked = checked, - modifier = Modifier.height(24.dp), - onCheckedChange = { - checked = it - feed = upsertBlk(feed!!) { f -> - f.preferences?.playAudioOnly = checked + color = textColor, + modifier = Modifier.clickable(onClick = { + val composeView = ComposeView(requireContext()).apply { + setContent { + val showDialog = remember { mutableStateOf(true) } + CustomTheme(requireContext()) { + VideoModeDialog(showDialog.value, onDismissRequest = { showDialog.value = false }) + } + } } - } + (view as? ViewGroup)?.addView(composeView) + }) ) } Text( - text = stringResource(R.string.pref_audio_only_sum), + text = stringResource(videoModeSummaryResId), style = MaterialTheme.typography.body2, color = textColor ) @@ -270,8 +271,7 @@ class FeedSettingsFragment : Fragment() { setContent { val showDialog = remember { mutableStateOf(true) } CustomTheme(requireContext()) { - AutoDeleteDialog(showDialog.value, - onDismissRequest = { showDialog.value = false }) + AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false }) } } } @@ -596,19 +596,94 @@ class FeedSettingsFragment : Fragment() { super.onDestroyView() } + private fun getVideoModePolicy() { + when (feed?.preferences!!.videoModePolicy) { + VideoMode.NONE -> { + videoModeSummaryResId = R.string.global_default + videoMode = VideoMode.NONE.tag + } + VideoMode.WINDOW_VIEW -> { + videoModeSummaryResId = R.string.feed_video_mode_window + videoMode = VideoMode.WINDOW_VIEW.tag + } + VideoMode.FULL_SCREEN_VIEW -> { + videoModeSummaryResId = R.string.feed_video_mode_fullscreen + videoMode = VideoMode.FULL_SCREEN_VIEW.tag + } + VideoMode.AUDIO_ONLY -> { + videoModeSummaryResId = R.string.feed_video_mode_audioonly + videoMode = VideoMode.AUDIO_ONLY.tag + } + } + } + @Composable + fun VideoModeDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { + if (showDialog) { + val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) } + Dialog(onDismissRequest = { onDismissRequest() }) { + Card( + modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column { + videoModeTags.forEach { text -> + Row(Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked = (text == selectedOption), + onCheckedChange = { + Logd(TAG, "row clicked: $text $selectedOption") + if (text != selectedOption) { + onOptionSelected(text) + val mode_ = when (text) { + VideoMode.NONE.tag -> VideoMode.NONE + VideoMode.WINDOW_VIEW.tag -> VideoMode.WINDOW_VIEW + VideoMode.FULL_SCREEN_VIEW.tag -> VideoMode.FULL_SCREEN_VIEW + VideoMode.AUDIO_ONLY.tag -> VideoMode.AUDIO_ONLY + else -> VideoMode.NONE + } + feed = upsertBlk(feed!!) { it.preferences?.videoModePolicy = mode_ } + getVideoModePolicy() + onDismissRequest() + } + } + ) + Text( + text = text, + style = MaterialTheme.typography.body1.merge(), +// color = textColor, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + } + } + } + } + } + } + private fun getAutoDeletePolicy() { when (feed?.preferences!!.autoDeleteAction) { AutoDeleteAction.GLOBAL -> { autoDeleteSummaryResId = R.string.global_default - autoDeletePolicy = "global" + autoDeletePolicy = AutoDeleteAction.GLOBAL.tag } AutoDeleteAction.ALWAYS -> { autoDeleteSummaryResId = R.string.feed_auto_download_always - autoDeletePolicy = "always" + autoDeletePolicy = AutoDeleteAction.ALWAYS.tag } AutoDeleteAction.NEVER -> { autoDeleteSummaryResId = R.string.feed_auto_download_never - autoDeletePolicy = "never" + autoDeletePolicy = AutoDeleteAction.NEVER.tag } } } @@ -640,14 +715,12 @@ class FeedSettingsFragment : Fragment() { if (text != selectedOption) { onOptionSelected(text) val action_ = when (text) { - "global" -> AutoDeleteAction.GLOBAL - "always" -> AutoDeleteAction.ALWAYS - "never" -> AutoDeleteAction.NEVER + AutoDeleteAction.GLOBAL.tag -> AutoDeleteAction.GLOBAL + AutoDeleteAction.ALWAYS.tag -> AutoDeleteAction.ALWAYS + AutoDeleteAction.NEVER.tag -> AutoDeleteAction.NEVER else -> AutoDeleteAction.GLOBAL } - feed = upsertBlk(feed!!) { - it.preferences?.autoDeleteAction = action_ - } + feed = upsertBlk(feed!!) { it.preferences?.autoDeleteAction = action_ } getAutoDeletePolicy() onDismissRequest() } @@ -720,7 +793,7 @@ class FeedSettingsFragment : Fragment() { @Composable fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { if (showDialog) { - val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDLPolicy.ONLY_NEW) } + val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) } Dialog(onDismissRequest = { onDismissRequest() }) { Card( modifier = Modifier @@ -733,7 +806,7 @@ class FeedSettingsFragment : Fragment() { verticalArrangement = Arrangement.spacedBy(8.dp) ) { Column { - AutoDLPolicy.entries.forEach { item -> + AutoDownloadPolicy.entries.forEach { item -> Row(Modifier .fillMaxWidth() .padding(horizontal = 16.dp), diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt index 3140cd55..6568c6de 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/view/ShownotesWebView.kt @@ -67,7 +67,6 @@ class ShownotesWebView : WebView, View.OnLongClickListener { else IntentUtils.openInBrowser(context, url) return true } - override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) Logd(TAG, "Page finished") @@ -88,9 +87,8 @@ class ShownotesWebView : WebView, View.OnLongClickListener { HitTestResult.EMAIL_TYPE -> { Logd(TAG, "E-Mail of webview was long-pressed. Extra: " + r.extra) ContextCompat.getSystemService(context, ClipboardManager::class.java)?.setPrimaryClip(ClipData.newPlainText("Podcini", r.extra)) - if (Build.VERSION.SDK_INT <= 32 && this.context is MainActivity) { + if (Build.VERSION.SDK_INT <= 32 && this.context is MainActivity) (this.context as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) - } return true } else -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 226d828e..9e49ac64 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -142,6 +142,11 @@ Always Never + Video mode + Window mode + Full screen + Audio only + Auto download policy Only new items Newest unplayed diff --git a/changelog.md b/changelog.md index b40c1dce..0c2cd3d7 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,9 @@ +# 6.5.6 + +* in feed preferences, the setting "play audio only" for video feed is replaced with the setting of a video mode. If you set the previous setting, you need to redo with the new setting. +* added some extra permission requests when exporting/importing various files, maybe needed in some system +* re-enabed use of http traffic to work with relevant podcasts + # 6.5.5 * corrected issue of Youtube channel being set for auto-download when subscribing diff --git a/fastlane/metadata/android/en-US/changelogs/3020240.txt b/fastlane/metadata/android/en-US/changelogs/3020240.txt new file mode 100644 index 00000000..a023190f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020240.txt @@ -0,0 +1,5 @@ + Version 6.5.6 brings several changes: + +* in feed preferences, the setting "play audio only" for video feed is replaced with the setting of a video mode. If you set the previous setting, you need to redo with the new setting. +* added some extra permission requests when exporting/importing various files, maybe needed in some system +* re-enabed use of http traffic to work with relevant podcasts