diff --git a/README.md b/README.md index 04461830..34af3ee2 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ Even so, the database remains backward compatible, and AntennaPod's db can be ea * New share notes menu option on various episode views * Feed info view offers a link for direct search of feeds related to author +* New episode home view with two display modes: webpage or reader +* Text-to-Speech is enabled in reader's mode +* RSS feeds with no playable media can be subscribed and read/listened ### Online feed diff --git a/app/build.gradle b/app/build.gradle index 37b7b5c8..4e10a202 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,8 +149,8 @@ android { // Version code schema (not used): // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020130 - versionName "4.8.0" + versionCode 3020131 + versionName "4.9.0" def commit = "" try { @@ -227,10 +227,11 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.6.2" implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation "androidx.media:media:1.7.0" - implementation "androidx.media3:media3-exoplayer:1.2.1" - implementation "androidx.media3:media3-ui:1.2.1" - implementation "androidx.media3:media3-datasource-okhttp:1.2.1" - implementation "androidx.media3:media3-common:1.2.1" + implementation "androidx.media3:media3-exoplayer:1.3.1" + implementation "androidx.media3:media3-ui:1.3.1" + implementation "androidx.media3:media3-datasource-okhttp:1.3.1" + implementation "androidx.media3:media3-common:1.3.1" + implementation "androidx.media3:media3-session:1.3.1" implementation "androidx.palette:palette-ktx:1.0.0" implementation "androidx.preference:preference-ktx:1.2.1" implementation "androidx.recyclerview:recyclerview:1.3.2" @@ -269,12 +270,15 @@ dependencies { implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3' implementation "com.annimon:stream:1.2.2" + implementation "net.dankito.readability4j:readability4j:1.0.8" + // Non-free dependencies: playImplementation 'com.google.android.play:core-ktx:1.8.1' compileOnly "com.google.android.wearable:wearable:2.9.0" // this one can not be updated? androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1' + androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1" androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1" androidTestImplementation "androidx.test.espresso:espresso-intents:3.5.1" 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 bd6bc55b..9a151060 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 @@ -9,9 +9,7 @@ import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback */ object CastPsmp { @JvmStatic - fun getInstanceIfConnected(context: Context, - callback: PSMPCallback - ): PlaybackServiceMediaPlayer? { + fun getInstanceIfConnected(context: Context, callback: PSMPCallback): PlaybackServiceMediaPlayer? { return null } } diff --git a/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt b/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt index 4db068f7..aaab7897 100644 --- a/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt +++ b/app/src/free/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt @@ -1,7 +1,7 @@ package ac.mdiq.podcini.service.playback -import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat +import androidx.media3.session.MediaSession internal object WearMediaSession { /** @@ -11,7 +11,7 @@ internal object WearMediaSession { // no-op } - fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat?) { + fun mediaSessionSetExtraForWear(mediaSession: MediaSession?) { // no-op } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39b4ccdc..11da927d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,7 @@ tools:ignore="ExportedService"> + diff --git a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt b/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt index eb623982..2199a1d2 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/base/PlaybackServiceMediaPlayer.kt @@ -20,8 +20,8 @@ import kotlin.concurrent.Volatile * Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local * and remote (cast devices) playback. */ -abstract class PlaybackServiceMediaPlayer protected constructor(protected val context: Context, - protected val callback: PSMPCallback) { +abstract class PlaybackServiceMediaPlayer protected constructor(protected val context: Context, protected val callback: PSMPCallback) { + @Volatile private var oldPlayerStatus: PlayerStatus? = null @@ -46,6 +46,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co } abstract fun isStartWhenPrepared(): Boolean + abstract fun setStartWhenPrepared(startWhenPrepared: Boolean) abstract fun getPlayable(): Playable? @@ -68,6 +69,8 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co abstract fun getSelectedAudioTrack(): Int + abstract fun createMediaPlayer() + /** * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will @@ -98,11 +101,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co * for playback immediately (see 'prepareImmediately' parameter for more details) * @param prepareImmediately Set to true if the method should also prepare the episode for playback. */ - abstract fun playMediaObject(playable: Playable, - stream: Boolean, - startWhenPrepared: Boolean, - prepareImmediately: Boolean - ) + abstract fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) /** * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. @@ -279,9 +278,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co * * @return a Future, just for the purpose of tracking its execution. */ - protected abstract fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, - shouldContinue: Boolean, toStoppedState: Boolean - ) + protected abstract fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) /** * @return `true` if the WifiLock feature should be used, `false` otherwise. @@ -327,9 +324,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co * Will be ignored if given the value of [Playable.INVALID_TIME]. */ @Synchronized - protected fun setPlayerStatus(newStatus: PlayerStatus, - newMedia: Playable?, position: Int - ) { + protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int) { Log.d(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus) this.oldPlayerStatus = playerStatus diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt index a40a81c5..d84be978 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt @@ -44,9 +44,6 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { private val bufferUpdateInterval = 5L private val bufferingUpdateDisposable: Disposable - private lateinit var exoPlayer: ExoPlayer - private lateinit var trackSelector: DefaultTrackSelector - private var loudnessEnhancer: LoudnessEnhancer? = null private var mediaSource: MediaSource? = null private var audioSeekCompleteListener: Runnable? = null @@ -58,36 +55,18 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { init { createPlayer() - playbackParameters = exoPlayer.playbackParameters + playbackParameters = exoPlayer!!.playbackParameters bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { - bufferingUpdateListener?.accept(exoPlayer.bufferedPercentage) + bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage) } } private fun createPlayer() { - val loadControl = DefaultLoadControl.Builder() - loadControl.setBufferDurationsMs(30000, 120000, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, - DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) - loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true) - trackSelector = DefaultTrackSelector(context) - val audioOffloadPreferences = AudioOffloadPreferences.Builder() - .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed - .setIsGaplessSupportRequired(true) - .setIsSpeedChangeSupportRequired(true) - .build() - exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context)) - .setTrackSelector(trackSelector) - .setLoadControl(loadControl.build()) - .build() - exoPlayer.setSeekParameters(SeekParameters.EXACT) - exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters - .buildUpon() - .setAudioOffloadPreferences(audioOffloadPreferences) - .build() - exoPlayer.addListener(object : Player.Listener { + if (exoPlayer == null) createStaticPlayer(context) + + exoPlayer?.addListener(object : Player.Listener { override fun onPlaybackStateChanged(playbackState: @Player.State Int) { when { audioCompletionListener != null && playbackState == Player.STATE_ENDED -> { @@ -119,10 +98,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } } - override fun onPositionDiscontinuity(oldPosition: PositionInfo, - newPosition: PositionInfo, - reason: @DiscontinuityReason Int - ) { + override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) { if (reason == Player.DISCONTINUITY_REASON_SEEK) { audioSeekCompleteListener?.run() } @@ -133,40 +109,37 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } }) - initLoudnessEnhancer(exoPlayer.audioSessionId) + initLoudnessEnhancer(exoPlayer!!.audioSessionId) } val currentPosition: Int - get() = exoPlayer.currentPosition.toInt() + get() = exoPlayer!!.currentPosition.toInt() val currentSpeedMultiplier: Float get() = playbackParameters.speed val duration: Int get() { - if (exoPlayer.duration == C.TIME_UNSET) { - return Playable.INVALID_TIME - } - return exoPlayer.duration.toInt() + if (exoPlayer?.duration == C.TIME_UNSET) return Playable.INVALID_TIME + return exoPlayer!!.duration.toInt() } val isPlaying: Boolean - get() = exoPlayer.playWhenReady + get() = exoPlayer!!.playWhenReady fun pause() { - exoPlayer.pause() + exoPlayer?.pause() } @Throws(IllegalStateException::class) fun prepare() { if (mediaSource == null) return - exoPlayer.setMediaSource(mediaSource!!, false) - exoPlayer.prepare() + exoPlayer?.setMediaSource(mediaSource!!, false) + exoPlayer?.prepare() } fun release() { bufferingUpdateDisposable.dispose() - exoPlayer.release() audioSeekCompleteListener = null audioCompletionListener = null @@ -175,23 +148,22 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } fun reset() { - exoPlayer.release() createPlayer() } @Throws(IllegalStateException::class) fun seekTo(i: Int) { - exoPlayer.seekTo(i.toLong()) + exoPlayer?.seekTo(i.toLong()) audioSeekCompleteListener?.run() } fun setAudioStreamType(i: Int) { - val a = exoPlayer.audioAttributes + val a = exoPlayer!!.audioAttributes val b = AudioAttributes.Builder() b.setContentType(i) b.setFlags(a.flags) b.setUsage(a.usage) - exoPlayer.setAudioAttributes(b.build(), false) + exoPlayer?.setAudioAttributes(b.build(), false) } @Throws(IllegalArgumentException::class, IllegalStateException::class) @@ -201,9 +173,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { // Call.Factory callFactory = PodciniHttpClient.getHttpClient(); // Assuming it returns OkHttpClient // OkHttpDataSource.Factory httpDataSourceFactory = new OkHttpDataSource.Factory(callFactory) // .setUserAgent(ClientConfig.USER_AGENT); - val httpDataSourceFactory = - OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) - .setUserAgent(ClientConfig.USER_AGENT) + val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory) + .setUserAgent(ClientConfig.USER_AGENT) if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) { val requestProperties = HashMap() @@ -225,35 +196,35 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } fun setDisplay(sh: SurfaceHolder?) { - exoPlayer.setVideoSurfaceHolder(sh) + exoPlayer?.setVideoSurfaceHolder(sh) } fun setPlaybackParams(speed: Float, skipSilence: Boolean) { Log.d(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence") playbackParameters = PlaybackParameters(speed, playbackParameters.pitch) - exoPlayer.skipSilenceEnabled = skipSilence - exoPlayer.playbackParameters = playbackParameters + exoPlayer!!.skipSilenceEnabled = skipSilence + exoPlayer!!.playbackParameters = playbackParameters } fun setVolume(v: Float, v1: Float) { if (v > 1) { - exoPlayer.volume = 1f + exoPlayer!!.volume = 1f loudnessEnhancer?.setEnabled(true) loudnessEnhancer?.setTargetGain((1000 * (v - 1)).toInt()) } else { - exoPlayer.volume = v + exoPlayer!!.volume = v loudnessEnhancer?.setEnabled(false) } } fun start() { - exoPlayer.play() + exoPlayer?.play() // Can't set params when paused - so always set it on start in case they changed - exoPlayer.playbackParameters = playbackParameters + exoPlayer!!.playbackParameters = playbackParameters } fun stop() { - exoPlayer.stop() + exoPlayer?.stop() } val audioTracks: List @@ -269,8 +240,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { private val formats: List get() { val formats: MutableList = arrayListOf() - val trackInfo = trackSelector.currentMappedTrackInfo - ?: return emptyList() + val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList() val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) for (i in 0 until trackGroups.length) { formats.add(trackGroups[i].getFormat(0)) @@ -279,28 +249,25 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { } fun setAudioTrack(track: Int) { - val trackInfo = trackSelector.currentMappedTrackInfo ?: return + val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) val override = SelectionOverride(track, 0) val rendererIndex = audioRendererIndex - val params = trackSelector.buildUponParameters() - .setSelectionOverride(rendererIndex, trackGroups, override) - trackSelector.setParameters(params) + val params = trackSelector!!.buildUponParameters().setSelectionOverride(rendererIndex, trackGroups, override) + trackSelector!!.setParameters(params) } private val audioRendererIndex: Int get() { - for (i in 0 until exoPlayer.rendererCount) { - if (exoPlayer.getRendererType(i) == C.TRACK_TYPE_AUDIO) { - return i - } + for (i in 0 until exoPlayer!!.rendererCount) { + if (exoPlayer?.getRendererType(i) == C.TRACK_TYPE_AUDIO) return i } return -1 } val selectedAudioTrack: Int get() { - val trackSelections = exoPlayer.currentTrackSelections + val trackSelections = exoPlayer!!.currentTrackSelections val availableFormats = formats for (i in 0 until trackSelections.length) { val track = trackSelections[i] as ExoTrackSelection? ?: continue @@ -325,12 +292,12 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { val videoWidth: Int get() { - return exoPlayer.videoFormat?.width ?: 0 + return exoPlayer?.videoFormat?.width ?: 0 } val videoHeight: Int get() { - return exoPlayer.videoFormat?.height ?: 0 + return exoPlayer?.videoFormat?.height ?: 0 } fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer?) { @@ -356,5 +323,33 @@ class ExoPlayerWrapper internal constructor(private val context: Context) { const val BUFFERING_ENDED: Int = -2 private const val TAG = "ExoPlayerWrapper" const val ERROR_CODE_OFFSET: Int = 1000 + + private var trackSelector: DefaultTrackSelector? = null + var exoPlayer: ExoPlayer? = null + + fun createStaticPlayer(context: Context) { + val loadControl = DefaultLoadControl.Builder() + loadControl.setBufferDurationsMs(30000, 120000, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, + DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) + loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true) + trackSelector = DefaultTrackSelector(context) + val audioOffloadPreferences = AudioOffloadPreferences.Builder() + .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed + .setIsGaplessSupportRequired(true) + .setIsSpeedChangeSupportRequired(true) + .build() + Log.d(TAG, "createPlayer creating exoPlayer_") + + exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context)) + .setTrackSelector(trackSelector!!) + .setLoadControl(loadControl.build()) + .build() + + exoPlayer?.setSeekParameters(SeekParameters.EXACT) + exoPlayer!!.trackSelectionParameters = exoPlayer!!.trackSelectionParameters + .buildUpon() + .setAudioOffloadPreferences(audioOffloadPreferences) + .build() + } } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt index 0cc46044..6fd907ed 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt @@ -98,11 +98,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * for playback immediately (see 'prepareImmediately' parameter for more details) * @param prepareImmediately Set to true if the method should also prepare the episode for playback. */ - override fun playMediaObject(playable: Playable, - stream: Boolean, - startWhenPrepared: Boolean, - prepareImmediately: Boolean - ) { + override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { Log.d(TAG, "playMediaObject(...)") try { playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately) @@ -121,12 +117,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia * * @see .playMediaObject */ - private fun playMediaObject(playable: Playable, - forceReset: Boolean, - stream: Boolean, - startWhenPrepared: Boolean, - prepareImmediately: Boolean - ) { + private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) { if (media != null) { if (!forceReset && media!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) { // episode is already playing -> ignore method call @@ -161,7 +152,6 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia try { callback.ensureMediaInfoLoaded(media!!) callback.onMediaChanged(false) -// TODO: speed setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence) when { stream -> { @@ -169,10 +159,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (streamurl != null) { if (playable is FeedMedia && playable.item?.feed?.preferences != null) { val preferences = playable.item!!.feed!!.preferences!! - mediaPlayer?.setDataSource( - streamurl, - preferences.username, - preferences.password) + mediaPlayer?.setDataSource(streamurl, preferences.username, preferences.password) } else { mediaPlayer?.setDataSource(streamurl) } @@ -221,14 +208,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia Log.d(TAG, "Audiofocus successfully requested") Log.d(TAG, "Resuming/Starting playback") acquireWifiLockIfNecessary() -// TODO: speed setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence) setVolume(1.0f, 1.0f) if (media != null && playerStatus == PlayerStatus.PREPARED && media!!.getPosition() > 0) { - val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( - media!!.getPosition(), - media!!.getLastPlayedTime()) + val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(media!!.getPosition(), media!!.getLastPlayedTime()) seekTo(newPosition) } mediaPlayer?.start() @@ -502,9 +486,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia if (mediaPlayer != null) { try { clearMediaPlayerListeners() - if (mediaPlayer!!.isPlaying) { - mediaPlayer!!.stop() - } + if (mediaPlayer!!.isPlaying) mediaPlayer!!.stop() } catch (e: Exception) { e.printStackTrace() } @@ -571,7 +553,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia return mediaPlayer?.selectedAudioTrack?:0 } - private fun createMediaPlayer() { + override fun createMediaPlayer() { mediaPlayer?.release() if (media == null) { @@ -650,9 +632,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia .build() } - override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, - shouldContinue: Boolean, toStoppedState: Boolean - ) { + override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) { releaseWifiLockIfNecessary() val isPlaying = playerStatus == PlayerStatus.PLAYING @@ -720,13 +700,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia } private fun setMediaPlayerListeners(mp: ExoPlayerWrapper?) { - if (mp == null || media == null) { - return - } - mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true, - wasSkipped = false, - shouldContinue = true, - toStoppedState = true) }) + if (mp == null || media == null) return + + mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true) }) mp.setOnSeekCompleteListener(Runnable { this.genericSeekCompleteListener() }) mp.setOnBufferingUpdateListener(Consumer { percent: Int -> when (percent) { 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 e3b82c63..fe2de3fe 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 @@ -11,11 +11,9 @@ import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.cast.CastPsmp import ac.mdiq.podcini.playback.cast.CastStateListener import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager.PSTMCallback -import ac.mdiq.podcini.preferences.PlaybackPreferences import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.clearCurrentlyPlayingTemporaryPlaybackSpeed import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentEpisodeIsVideo -import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentPlayerStatus import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingTemporaryPlaybackSpeed import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeMediaPlaying @@ -26,8 +24,6 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis -import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder -import ac.mdiq.podcini.preferences.UserPreferences.downloadsSortedOrder import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs import ac.mdiq.podcini.preferences.UserPreferences.getPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.hardwareForwardButton @@ -39,7 +35,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect -import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode @@ -52,8 +47,10 @@ import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.service.playback.WearMediaSession import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.DBWriter -import ac.mdiq.podcini.storage.FeedSearcher -import ac.mdiq.podcini.storage.model.feed.* +import ac.mdiq.podcini.storage.model.feed.Feed +import ac.mdiq.podcini.storage.model.feed.FeedItem +import ac.mdiq.podcini.storage.model.feed.FeedMedia +import ac.mdiq.podcini.storage.model.feed.FeedPreferences import ac.mdiq.podcini.storage.model.feed.FeedPreferences.AutoDeleteAction import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable @@ -61,7 +58,6 @@ import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter import ac.mdiq.podcini.ui.utils.NotificationUtils import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState -import ac.mdiq.podcini.util.ChapterUtils.getCurrentChapterIndex import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast @@ -76,15 +72,16 @@ import android.Manifest import android.annotation.SuppressLint import android.app.NotificationManager import android.app.PendingIntent -import android.app.UiModeManager import android.bluetooth.BluetoothA2dp import android.content.* import android.content.pm.PackageManager -import android.content.res.Configuration import android.media.AudioManager import android.net.Uri -import android.os.* +import android.os.Binder +import android.os.Build import android.os.Build.VERSION_CODES +import android.os.IBinder +import android.os.Vibrator import android.service.quicksettings.TileService import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat @@ -96,7 +93,6 @@ import android.util.Log import android.util.Pair import android.view.KeyEvent import android.view.SurfaceHolder -import android.view.ViewConfiguration import android.webkit.URLUtil import android.widget.Toast import androidx.annotation.DrawableRes @@ -104,10 +100,13 @@ import androidx.annotation.StringRes import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat -import androidx.media.MediaBrowserServiceCompat import androidx.media3.common.util.UnstableApi -import io.reactivex.* +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaSession import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.SingleEmitter import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers @@ -123,7 +122,7 @@ import kotlin.math.max * Controls the MediaPlayer that plays a FeedMedia-file */ @UnstableApi -class PlaybackService : MediaBrowserServiceCompat() { +class PlaybackService : MediaLibraryService() { private var mediaPlayer: PlaybackServiceMediaPlayer? = null private var positionEventTimer: Disposable? = null @@ -133,8 +132,8 @@ class PlaybackService : MediaBrowserServiceCompat() { private lateinit var castStateListener: CastStateListener private var autoSkippedFeedMediaId: String? = null - private var clickCount = 0 - private val clickHandler = Handler(Looper.getMainLooper()) +// private var clickCount = 0 +// private val clickHandler = Handler(Looper.getMainLooper()) private var isSpeedForward = false private var normalSpeed = 1.0f @@ -145,7 +144,7 @@ class PlaybackService : MediaBrowserServiceCompat() { /** * Used for Lollipop notifications, Android Wear, and Android Auto. */ - private var mediaSession: MediaSessionCompat? = null + private var mediaSession: MediaSession? = null private val mBinder: IBinder = LocalBinder() @@ -191,36 +190,14 @@ class PlaybackService : MediaBrowserServiceCompat() { } fun recreateMediaSessionIfNeeded() { - if (mediaSession != null) { - // Media session was not destroyed, so we can re-use it. - if (!mediaSession!!.isActive) { - mediaSession!!.isActive = true - } - return - } - val eventReceiver = ComponentName(applicationContext, MediaButtonReceiver::class.java) - val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - mediaButtonIntent.setComponent(eventReceiver) - val buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0)) + if (mediaSession != null) return - mediaSession = MediaSessionCompat(applicationContext, TAG, eventReceiver, buttonReceiverIntent) - sessionToken = mediaSession!!.sessionToken - - try { - mediaSession!!.setCallback(sessionCallback) - mediaSession!!.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) - } catch (npe: NullPointerException) { - // on some devices (Huawei) setting active can cause a NullPointerException - // even with correct use of the api. - // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat - // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d - Log.e(TAG, "NullPointerException while setting up MediaSession") - npe.printStackTrace() - } + if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext) + mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!) + .setCallback(sessionCallback) + .build() recreateMediaPlayer() - mediaSession!!.isActive = true } fun recreateMediaPlayer() { @@ -234,7 +211,7 @@ class PlaybackService : MediaBrowserServiceCompat() { } mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback) if (mediaPlayer == null) { - mediaPlayer = LocalPSMP(this, mediaPlayerCallback) // Cast not supported or not connected + mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected } if (media != null) { mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true) @@ -271,8 +248,13 @@ class PlaybackService : MediaBrowserServiceCompat() { castStateListener.destroy() cancelPositionObserver() - mediaSession?.release() - mediaSession = null + mediaSession?.run { + player.release() + release() + mediaSession = null + } + ExoPlayerWrapper.exoPlayer?.release() + ExoPlayerWrapper.exoPlayer = null mediaPlayer?.shutdown() unregisterReceiver(autoStateUpdated) @@ -284,18 +266,8 @@ class PlaybackService : MediaBrowserServiceCompat() { EventBus.getDefault().unregister(this) } - override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot { - Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName + - "; clientUid=" + clientUid + " ; rootHints=" + rootHints) - if (rootHints != null && rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) { - val extras = Bundle() - extras.putBoolean(BrowserRoot.EXTRA_RECENT, true) - Log.d(TAG, "OnGetRoot: Returning BrowserRoot " + R.string.current_playing_episode) - return BrowserRoot(resources.getString(R.string.current_playing_episode), extras) - } - - // Name visible in Android Auto - return BrowserRoot(resources.getString(R.string.app_name), null) + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? { + return null } private fun loadQueueForMediaSession() { @@ -311,139 +283,141 @@ class PlaybackService : MediaBrowserServiceCompat() { } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ queueItems: List? -> mediaSession?.setQueue(queueItems) }, + .subscribe({ queueItems: List? -> +// mediaSession?.setQueue(queueItems) + }, { obj: Throwable -> obj.printStackTrace() }) } - private fun createBrowsableMediaItem( - @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int - ): MediaBrowserCompat.MediaItem { - val uri = Uri.Builder() - .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) - .authority(resources.getResourcePackageName(icon)) - .appendPath(resources.getResourceTypeName(icon)) - .appendPath(resources.getResourceEntryName(icon)) - .build() +// private fun createBrowsableMediaItem( +// @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int +// ): MediaBrowserCompat.MediaItem { +// val uri = Uri.Builder() +// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) +// .authority(resources.getResourcePackageName(icon)) +// .appendPath(resources.getResourceTypeName(icon)) +// .appendPath(resources.getResourceEntryName(icon)) +// .build() +// +// val description = MediaDescriptionCompat.Builder() +// .setIconUri(uri) +// .setMediaId(resources.getString(title)) +// .setTitle(resources.getString(title)) +// .setSubtitle(resources.getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) +// .build() +// return MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) +// } - val description = MediaDescriptionCompat.Builder() - .setIconUri(uri) - .setMediaId(resources.getString(title)) - .setTitle(resources.getString(title)) - .setSubtitle(resources.getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) - .build() - return MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } +// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem { +// val builder = MediaDescriptionCompat.Builder() +// .setMediaId("FeedId:" + feed.id) +// .setTitle(feed.title) +// .setDescription(feed.description) +// .setSubtitle(feed.getCustomTitle()) +// if (feed.imageUrl != null) { +// builder.setIconUri(Uri.parse(feed.imageUrl)) +// } +// if (feed.link != null) { +// builder.setMediaUri(Uri.parse(feed.link)) +// } +// val description = builder.build() +// return MediaBrowserCompat.MediaItem(description, +// MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) +// } - private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem { - val builder = MediaDescriptionCompat.Builder() - .setMediaId("FeedId:" + feed.id) - .setTitle(feed.title) - .setDescription(feed.description) - .setSubtitle(feed.getCustomTitle()) - if (feed.imageUrl != null) { - builder.setIconUri(Uri.parse(feed.imageUrl)) - } - if (feed.link != null) { - builder.setMediaUri(Uri.parse(feed.link)) - } - val description = builder.build() - return MediaBrowserCompat.MediaItem(description, - MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } +// override fun onLoadChildren(parentId: String, +// result: Result> +// ) { +// Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId") +// result.detach() +// +// Completable.create { emitter: CompletableEmitter -> +// result.sendResult(loadChildrenSynchronous(parentId)) +// emitter.onComplete() +// } +// .subscribeOn(Schedulers.io()) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe( +// {}, { e: Throwable -> +// e.printStackTrace() +// result.sendResult(null) +// }) +// } - override fun onLoadChildren(parentId: String, - result: Result> - ) { - Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId") - result.detach() +// private fun loadChildrenSynchronous(parentId: String): List? { +// val mediaItems: MutableList = ArrayList() +// if (parentId == resources.getString(R.string.app_name)) { +// val currentlyPlaying = currentPlayerStatus.toLong() +// if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING.toLong() +// || currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED.toLong()) { +// mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1)) +// } +// mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black, +// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.QUEUED)))) +// mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, +// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED)))) +// mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black, +// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.UNPLAYED)))) +// val feeds = DBReader.getFeedList() +// for (feed in feeds) { +// mediaItems.add(createBrowsableMediaItemForFeed(feed)) +// } +// return mediaItems +// } +// +// val feedItems: List +// when { +// parentId == resources.getString(R.string.queue_label) -> { +// feedItems = DBReader.getQueue() +// } +// parentId == resources.getString(R.string.downloads_label) -> { +// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, +// FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder) +// } +// parentId == resources.getString(R.string.episodes_label) -> { +// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, +// FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder) +// } +// parentId.startsWith("FeedId:") -> { +// val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong() +// val feed = DBReader.getFeed(feedId) +// feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf() +// } +// parentId == getString(R.string.current_playing_episode) -> { +// val playable = createInstanceFromPreferences(this) +// if (playable is FeedMedia) { +// feedItems = listOf(playable.item) +// } else { +// return null +// } +// } +// else -> { +// Log.e(TAG, "Parent ID not found: $parentId") +// return null +// } +// } +// var count = 0 +// for (feedItem in feedItems) { +// if (feedItem?.media != null) { +// mediaItems.add(feedItem.media!!.mediaItem) +// if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) { +// break +// } +// } +// } +// return mediaItems +// } - Completable.create { emitter: CompletableEmitter -> - result.sendResult(loadChildrenSynchronous(parentId)) - emitter.onComplete() - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - {}, { e: Throwable -> - e.printStackTrace() - result.sendResult(null) - }) - } - - private fun loadChildrenSynchronous(parentId: String): List? { - val mediaItems: MutableList = ArrayList() - if (parentId == resources.getString(R.string.app_name)) { - val currentlyPlaying = currentPlayerStatus.toLong() - if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING.toLong() - || currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED.toLong()) { - mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1)) - } - mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black, - DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.QUEUED)))) - mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black, - DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED)))) - mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black, - DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.UNPLAYED)))) - val feeds = DBReader.getFeedList() - for (feed in feeds) { - mediaItems.add(createBrowsableMediaItemForFeed(feed)) - } - return mediaItems - } - - val feedItems: List - when { - parentId == resources.getString(R.string.queue_label) -> { - feedItems = DBReader.getQueue() - } - parentId == resources.getString(R.string.downloads_label) -> { - feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, - FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder) - } - parentId == resources.getString(R.string.episodes_label) -> { - feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED, - FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder) - } - parentId.startsWith("FeedId:") -> { - val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong() - val feed = DBReader.getFeed(feedId) - feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf() - } - parentId == getString(R.string.current_playing_episode) -> { - val playable = createInstanceFromPreferences(this) - if (playable is FeedMedia) { - feedItems = listOf(playable.item) - } else { - return null - } - } - else -> { - Log.e(TAG, "Parent ID not found: $parentId") - return null - } - } - var count = 0 - for (feedItem in feedItems) { - if (feedItem?.media != null) { - mediaItems.add(feedItem.media!!.mediaItem) - if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) { - break - } - } - } - return mediaItems - } - - override fun onBind(intent: Intent): IBinder? { + override fun onBind(intent: Intent?): IBinder? { Log.d(TAG, "Received onBind event") - return if (intent.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) { + return if (intent?.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) { super.onBind(intent) } else { mBinder } } - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) Log.d(TAG, "OnStartCommand called") @@ -451,10 +425,10 @@ class PlaybackService : MediaBrowserServiceCompat() { val notificationManager = NotificationManagerCompat.from(this) notificationManager.cancel(R.id.notification_streaming_confirmation) - val keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) - val customAction = intent.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) - val hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) - val playable = intent.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE) + val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 + val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) + val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false + val playable = intent?.getParcelableExtra(PlaybackServiceInterface.EXTRA_PLAYABLE) if (keycode == -1 && playable == null && customAction == null) { Log.e(TAG, "PlaybackService was started with no arguments") stateManager.stopService() @@ -483,10 +457,8 @@ class PlaybackService : MediaBrowserServiceCompat() { } playable != null -> { stateManager.validStartCommandWasReceived() - val allowStreamThisTime = intent.getBooleanExtra( - PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) - val allowStreamAlways = intent.getBooleanExtra( - PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false) + val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) + val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) if (allowStreamAlways) { isAllowMobileStreaming = true @@ -510,7 +482,7 @@ class PlaybackService : MediaBrowserServiceCompat() { return START_NOT_STICKY } else -> { - mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null) +// mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null) } } } @@ -541,8 +513,7 @@ class PlaybackService : MediaBrowserServiceCompat() { @SuppressLint("LaunchActivityFromNotification") private fun displayStreamingNotAllowedNotification(originalIntent: Intent) { if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) { - EventBus.getDefault().post(MessageEvent( - getString(R.string.confirm_mobile_streaming_notification_message))) + EventBus.getDefault().post(MessageEvent(getString(R.string.confirm_mobile_streaming_notification_message))) return } @@ -550,24 +521,22 @@ class PlaybackService : MediaBrowserServiceCompat() { intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME) intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true) val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) { - PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, + PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } else { - PendingIntent.getService(this, - R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } val intentAlwaysAllow = Intent(intentAllowThisTime) intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS) intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true) val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) { - PendingIntent.getForegroundService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, + PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } else { - PendingIntent.getService(this, - R.id.pending_intent_allow_stream_always, intentAlwaysAllow, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } val builder = NotificationCompat.Builder(this, @@ -579,12 +548,8 @@ class PlaybackService : MediaBrowserServiceCompat() { .bigText(getString(R.string.confirm_mobile_streaming_notification_message))) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setContentIntent(pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, - getString(R.string.confirm_mobile_streaming_button_once), - pendingIntentAllowThisTime) - .addAction(R.drawable.ic_notification_stream, - getString(R.string.confirm_mobile_streaming_button_always), - pendingIntentAlwaysAllow) + .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime) + .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow) .setAutoCancel(true) val notificationManager = NotificationManagerCompat.from(this) if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this, @@ -725,8 +690,7 @@ class PlaybackService : MediaBrowserServiceCompat() { private fun startPlayingFromPreferences() { Observable.fromCallable { - createInstanceFromPreferences( - applicationContext) + createInstanceFromPreferences(applicationContext) } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -856,8 +820,7 @@ class PlaybackService : MediaBrowserServiceCompat() { } } if (Build.VERSION.SDK_INT >= VERSION_CODES.N) { - TileService.requestListeningState(applicationContext, - ComponentName(applicationContext, QuickSettingsTileService::class.java)) + TileService.requestListeningState(applicationContext, ComponentName(applicationContext, QuickSettingsTileService::class.java)) } sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED) @@ -878,9 +841,7 @@ class PlaybackService : MediaBrowserServiceCompat() { updateNotificationAndMediaSession(this@PlaybackService.playable) } - override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, - playingNext: Boolean - ) { + override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) { this@PlaybackService.onPostPlayback(media, ended, skipped, playingNext) } @@ -1088,12 +1049,10 @@ class PlaybackService : MediaBrowserServiceCompat() { if (media is FeedMedia) { if (ended || smartMarkAsPlayed) { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( - applicationContext, media, true) + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, true) media.onPlaybackCompleted(applicationContext) } else { - SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive( - applicationContext, media, false) + SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, false) media.onPlaybackPause(applicationContext) } } @@ -1108,12 +1067,11 @@ class PlaybackService : MediaBrowserServiceCompat() { val action = item.feed?.preferences?.currentAutoDelete val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS || (action == AutoDeleteAction.GLOBAL && item.feed != null && shouldAutoDeleteItemsOnThatFeed(item.feed!!))) - if (media is FeedMedia && shouldAutoDelete && - (!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) { + if (media is FeedMedia && shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) { DBWriter.deleteFeedMediaOfItem(this@PlaybackService, media.id) Log.d(TAG, "Episode Deleted") } - notifyChildrenChanged(getString(R.string.queue_label)) +// notifyChildrenChanged(getString(R.string.queue_label)) } } @@ -1153,13 +1111,13 @@ class PlaybackService : MediaBrowserServiceCompat() { val skipEndMS = skipEnd * 1000 // Log.d(TAG, "skipEndingIfNecessary: checking " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed) if (skipEnd > 0 && skipEndMS < this.duration && (remainingTime - skipEndMS < 0)) { - Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed) + Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining $remainingTime $skipEndMS speed $currentPlaybackSpeed") val context = applicationContext val skipMesg = context.getString(R.string.pref_feed_skip_ending_toast, skipEnd) val toast = Toast.makeText(context, skipMesg, Toast.LENGTH_LONG) toast.show() - this.autoSkippedFeedMediaId = item?.identifyingValue + this.autoSkippedFeedMediaId = item.identifyingValue mediaPlayer?.skip() } } @@ -1199,51 +1157,35 @@ class PlaybackService : MediaBrowserServiceCompat() { // On Android Auto, custom actions are added in the following order around the play button, if no default // actions are present: Near left, near right, far left, far right, additional actions panel - val rewindBuilder = PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_REWIND, - getString(R.string.rewind_label), - R.drawable.ic_notification_fast_rewind - ) + val rewindBuilder = PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REWIND, getString(R.string.rewind_label), R.drawable.ic_notification_fast_rewind) WearMediaSession.addWearExtrasToAction(rewindBuilder) sessionState.addCustomAction(rewindBuilder.build()) - val fastForwardBuilder = PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_FAST_FORWARD, - getString(R.string.fast_forward_label), - R.drawable.ic_notification_fast_forward - ) + val fastForwardBuilder = PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) WearMediaSession.addWearExtrasToAction(fastForwardBuilder) sessionState.addCustomAction(fastForwardBuilder.build()) if (showPlaybackSpeedOnFullNotification()) { - sessionState.addCustomAction( - PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, - getString(R.string.playback_speed), - R.drawable.ic_notification_playback_speed - ).build() - ) + sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, + getString(R.string.playback_speed), R.drawable.ic_notification_playback_speed).build()) } if (showNextChapterOnFullNotification()) { if (!playable?.getChapters().isNullOrEmpty()) { - sessionState.addCustomAction( - PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_NEXT_CHAPTER, - getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter) - .build()) + sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_NEXT_CHAPTER, + getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter).build()) } } if (showSkipOnFullNotification()) { - sessionState.addCustomAction( - PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT, getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build() + sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT, + getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build() ) } if (mediaSession != null) { WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!) - mediaSession!!.setPlaybackState(sessionState.build()) +// mediaSession!!.setPlaybackState(sessionState.build()) } } @@ -1253,9 +1195,7 @@ class PlaybackService : MediaBrowserServiceCompat() { } private fun updateMediaSessionMetadata(p: Playable?) { - if (p == null || mediaSession == null) { - return - } + if (p == null || mediaSession == null) return val builder = MediaMetadataCompat.Builder() builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()) @@ -1294,13 +1234,13 @@ class PlaybackService : MediaBrowserServiceCompat() { mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))) - try { - mediaSession!!.setMetadata(builder.build()) - } catch (e: OutOfMemoryError) { - Log.e(TAG, "Setting media session metadata", e) - builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null) - mediaSession!!.setMetadata(builder.build()) - } +// try { +// mediaSession!!.setMetadata(builder.build()) +// } catch (e: OutOfMemoryError) { +// Log.e(TAG, "Setting media session metadata", e) +// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null) +// mediaSession!!.setMetadata(builder.build()) +// } } } @@ -1327,7 +1267,7 @@ class PlaybackService : MediaBrowserServiceCompat() { val playerStatus = mediaPlayer!!.playerStatus notificationBuilder.setPlayable(playable) - if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.sessionToken) + if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken()) notificationBuilder.playerStatus = playerStatus notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed) @@ -1646,8 +1586,7 @@ class PlaybackService : MediaBrowserServiceCompat() { feedPreferences.feedPlaybackSpeed = speed Log.d(TAG, "setSpeed ${feed.title} $speed") DBWriter.setFeedPreferences(feedPreferences) - EventBus.getDefault().post( - SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) + EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) } } } @@ -1776,188 +1715,13 @@ class PlaybackService : MediaBrowserServiceCompat() { if (playable is FeedMedia) { val itemId = playable.item?.id ?: return DBWriter.addQueueItem(this, false, true, itemId) - notifyChildrenChanged(getString(R.string.queue_label)) +// notifyChildrenChanged(getString(R.string.queue_label)) } } - private val sessionCallback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() { + private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback { private val TAG = "MediaSessionCompat" - - override fun onPlay() { - Log.d(TAG, "onPlay()") - val status: PlayerStatus = this@PlaybackService.status - when (status) { - PlayerStatus.PAUSED, PlayerStatus.PREPARED -> { - resume() - } - PlayerStatus.INITIALIZED -> { - this@PlaybackService.isStartWhenPrepared = true - prepare() - } - else -> {} - } - } - - override fun onPlayFromMediaId(mediaId: String, extras: Bundle) { - Log.d(TAG, "onPlayFromMediaId: mediaId: $mediaId extras: $extras") - val p = DBReader.getFeedMedia(mediaId.toLong()) - if (p != null) { - startPlaying(p, false) - } - } - - override fun onPlayFromSearch(query: String, extras: Bundle) { - Log.d(TAG, "onPlayFromSearch query=$query extras=$extras") - - if (query == "") { - Log.d(TAG, "onPlayFromSearch called with empty query, resuming from the last position") - startPlayingFromPreferences() - return - } - - val results = FeedSearcher.searchFeedItems(query, 0) - if (results.isNotEmpty() && results[0].media != null) { - val media = results[0].media - startPlaying(media, false) - return - } - onPlay() - } - - override fun onPause() { - Log.d(TAG, "onPause()") - if (this@PlaybackService.status == PlayerStatus.PLAYING) { - pause(!isPersistNotify, false) - } - } - - override fun onStop() { - Log.d(TAG, "onStop()") - mediaPlayer?.stopPlayback(true) - } - - override fun onSkipToPrevious() { - Log.d(TAG, "onSkipToPrevious()") - seekDelta(-rewindSecs * 1000) - } - - override fun onRewind() { - Log.d(TAG, "onRewind()") - seekDelta(-rewindSecs * 1000) - } - - fun onNextChapter() { - val chapters = mediaPlayer?.getPlayable()?.getChapters() ?: listOf() - if (chapters.isEmpty()) { - // No chapters, just fallback to next episode - mediaPlayer?.skip() - return - } - - val nextChapter = getCurrentChapterIndex(mediaPlayer?.getPlayable(), (mediaPlayer?.getPosition()?:0)) + 1 - - if (chapters.size < nextChapter + 1) { - // We are on the last chapter, just fallback to the next episode - mediaPlayer?.skip() - return - } - - mediaPlayer?.seekTo(chapters[nextChapter].start.toInt()) - } - - override fun onFastForward() { - Log.d(TAG, "onFastForward()") -// speedForward(2.5f) - seekDelta(fastForwardSecs * 1000) - } - - override fun onSkipToNext() { - Log.d(TAG, "onSkipToNext()") - val uiModeManager = applicationContext.getSystemService(UI_MODE_SERVICE) as UiModeManager - if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT - || uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) { - mediaPlayer?.skip() - } else { - seekDelta(fastForwardSecs * 1000) - } - } - - - override fun onSeekTo(pos: Long) { - Log.d(TAG, "onSeekTo()") - seekTo(pos.toInt()) - } - - override fun onSetPlaybackSpeed(speed: Float) { - Log.d(TAG, "onSetPlaybackSpeed()") - setSpeed(speed) - } - - override fun onMediaButtonEvent(mediaButton: Intent): Boolean { - Log.d(TAG, "onMediaButtonEvent($mediaButton)") - val keyEvent = mediaButton.getParcelableExtra(Intent.EXTRA_KEY_EVENT) - if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) { - val keyCode = keyEvent.keyCode - if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { - clickCount++ - clickHandler.removeCallbacksAndMessages(null) - clickHandler.postDelayed({ - when (clickCount) { - 1 -> { - handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false) - } - 2 -> { - onFastForward() - } - 3 -> { - onRewind() - } - } - clickCount = 0 - }, ViewConfiguration.getDoubleTapTimeout().toLong()) - return true - } else { - return handleKeycode(keyCode, false) - } - } - return false - } - - override fun onCustomAction(action: String, extra: Bundle) { - Log.d(TAG, "onCustomAction($action)") - when (action) { - CUSTOM_ACTION_FAST_FORWARD -> { - onFastForward() - } - CUSTOM_ACTION_REWIND -> { - onRewind() - } - CUSTOM_ACTION_SKIP_TO_NEXT -> { - mediaPlayer?.skip() - } - CUSTOM_ACTION_NEXT_CHAPTER -> { - onNextChapter() - } - CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED -> { - val selectedSpeeds = playbackSpeedArray - - // If the list has zero or one element, there's nothing we can do to change the playback speed. - if (selectedSpeeds.size > 1) { - val speedPosition = selectedSpeeds.indexOf(mediaPlayer?.getPlaybackSpeed()?:0f) - - val newSpeed = if (speedPosition == selectedSpeeds.size - 1) { - // This is the last element. Wrap instead of going over the size of the list. - selectedSpeeds[0] - } else { - // If speedPosition is still -1 (the user isn't using a preset), use the first preset in the - // list. - selectedSpeeds[speedPosition + 1] - } - onSetPlaybackSpeed(newSpeed) - } - } - } - } +// TODO: not used now with media3 } companion object { @@ -1976,8 +1740,7 @@ class PlaybackService : MediaBrowserServiceCompat() { private const val CUSTOM_ACTION_SKIP_TO_NEXT = "action.ac.mdiq.podcini.service.skipToNext" private const val CUSTOM_ACTION_FAST_FORWARD = "action.ac.mdiq.podcini.service.fastForward" private const val CUSTOM_ACTION_REWIND = "action.ac.mdiq.podcini.service.rewind" - private const val CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = - "action.ac.mdiq.podcini.service.changePlaybackSpeed" + private const val CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = "action.ac.mdiq.podcini.service.changePlaybackSpeed" const val CUSTOM_ACTION_NEXT_CHAPTER: String = "action.ac.mdiq.podcini.service.next_chapter" /** @@ -2030,15 +1793,16 @@ class PlaybackService : MediaBrowserServiceCompat() { /** * Same as [.getPlayerActivityIntent], but here the type of activity - * depends on the FeedMedia that is provided as an argument. + * depends on the medaitype that is provided as an argument. */ @JvmStatic - fun getPlayerActivityIntent(context: Context, media: Playable): Intent { - return if (media.getMediaType() == MediaType.VIDEO && !isCasting) { + fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent { + return if (mediaType == MediaType.VIDEO && !isCasting) { VideoPlayerActivityStarter(context).intent } else { MainActivityStarter(context).withOpenPlayer().getIntent() } } + } } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt index 2d2b6e46..ffaab349 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackServiceNotificationBuilder.kt @@ -130,43 +130,33 @@ class PlaybackServiceNotificationBuilder(private val context: Context) { private val playerActivityPendingIntent: PendingIntent get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, - PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_IMMUTABLE) + PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) private fun addActions(notification: NotificationCompat.Builder, mediaSessionToken: MediaSessionCompat.Token?, - playerStatus: PlayerStatus? - ) { + playerStatus: PlayerStatus?) { val compactActionList = ArrayList() var numActions = 0 // we start and 0 and then increment by 1 for each call to addAction - val rewindButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_REWIND, numActions) - notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), - rewindButtonPendingIntent) + val rewindButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_REWIND, numActions) + notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), rewindButtonPendingIntent) compactActionList.add(numActions) numActions++ if (playerStatus == PlayerStatus.PLAYING) { - val pauseButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PAUSE, numActions) - notification.addAction(R.drawable.ic_notification_pause, //pause action - context.getString(R.string.pause_label), - pauseButtonPendingIntent) + val pauseButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PAUSE, numActions) + //pause action + notification.addAction(R.drawable.ic_notification_pause, context.getString(R.string.pause_label), pauseButtonPendingIntent) } else { - val playButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_PLAY, numActions) - notification.addAction(R.drawable.ic_notification_play, //play action - context.getString(R.string.play_label), - playButtonPendingIntent) + val playButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PLAY, numActions) + //play action + notification.addAction(R.drawable.ic_notification_play, context.getString(R.string.play_label), playButtonPendingIntent) } compactActionList.add(numActions++) // ff follows play, then we have skip (if it's present) - val ffButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions) - notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), - ffButtonPendingIntent) + val ffButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions) + notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), ffButtonPendingIntent) compactActionList.add(numActions) numActions++ @@ -177,15 +167,12 @@ class PlaybackServiceNotificationBuilder(private val context: Context) { } if (UserPreferences.showSkipOnFullNotification()) { - val skipButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_NEXT, numActions) - notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), - skipButtonPendingIntent) + val skipButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_NEXT, numActions) + notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), skipButtonPendingIntent) numActions++ } - val stopButtonPendingIntent = getPendingIntentForMediaAction( - KeyEvent.KEYCODE_MEDIA_STOP, numActions) + val stopButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_STOP, numActions) notification.setStyle(androidx.media.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSessionToken) .setShowActionsInCompactView(*ArrayUtils.toPrimitive(compactActionList.toTypedArray())) @@ -199,11 +186,9 @@ class PlaybackServiceNotificationBuilder(private val context: Context) { intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue) return if (Build.VERSION.SDK_INT >= 26) { - PendingIntent.getForegroundService(context, requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } else { - PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } } @@ -213,11 +198,9 @@ class PlaybackServiceNotificationBuilder(private val context: Context) { intent.putExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION, action) return if (Build.VERSION.SDK_INT >= 26) { - PendingIntent.getForegroundService(context, requestCode, intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } else { - PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT - or PendingIntent.FLAG_IMMUTABLE) + PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } } @@ -230,8 +213,7 @@ class PlaybackServiceNotificationBuilder(private val context: Context) { private var defaultIcon: Bitmap? = null private fun getBitmap(vectorDrawable: VectorDrawable): Bitmap { - val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, - vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888) + val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) vectorDrawable.draw(canvas) diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt index f4d0aac0..03c826d3 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackVolumeUpdater.kt @@ -7,8 +7,7 @@ import ac.mdiq.podcini.playback.base.PlayerStatus internal class PlaybackVolumeUpdater { fun updateVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, - volumeAdaptionSetting: VolumeAdaptionSetting - ) { + volumeAdaptionSetting: VolumeAdaptionSetting) { val playable = mediaPlayer.getPlayable() if (playable is FeedMedia) { @@ -17,8 +16,7 @@ internal class PlaybackVolumeUpdater { } private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, - volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia - ) { + volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia) { if (feedMedia.item?.feed?.id == feedId) { val preferences = feedMedia.item!!.feed!!.preferences if (preferences != null) preferences.volumeAdaptionSetting = volumeAdaptionSetting diff --git a/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt b/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt index f598a910..dc1081cf 100644 --- a/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt +++ b/app/src/main/java/ac/mdiq/podcini/storage/database/PodDBAdapter.kt @@ -91,8 +91,7 @@ class PodDBAdapter private constructor() { feed.id = db.insert(TABLE_NAME_FEEDS, null, values) } else { Log.d(this.toString(), "Updating existing Feed in db") - db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", - arrayOf(feed.id.toString())) + db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(feed.id.toString())) } return feed.id } @@ -116,14 +115,12 @@ class PodDBAdapter private constructor() { values.put(KEY_FEED_SKIP_ENDING, prefs.feedSkipEnding) values.put(KEY_EPISODE_NOTIFICATION, prefs.showEpisodeNotification) values.put(KEY_NEW_EPISODES_ACTION, prefs.newEpisodesAction!!.code) - db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf( - prefs.feedID.toString())) + db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(prefs.feedID.toString())) } fun setFeedItemFilter(feedId: Long, filterValues: Set?) { val valuesList = TextUtils.join(",", filterValues!!) - Log.d(TAG, String.format(Locale.US, - "setFeedItemFilter() called with: feedId = [%d], filterValues = [%s]", feedId, valuesList)) + Log.d(TAG, String.format(Locale.US, "setFeedItemFilter() called with: feedId = [%d], filterValues = [%s]", feedId, valuesList)) val values = ContentValues() values.put(KEY_HIDE, valuesList) db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(feedId.toString())) @@ -163,8 +160,7 @@ class PodDBAdapter private constructor() { if (media.id == 0L) { media.id = db.insert(TABLE_NAME_FEED_MEDIA, null, values) } else { - db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", - arrayOf(media.id.toString())) + db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", arrayOf(media.id.toString())) } return media.id } @@ -176,8 +172,7 @@ class PodDBAdapter private constructor() { values.put(KEY_DURATION, media.getDuration()) values.put(KEY_PLAYED_DURATION, media.playedDuration) values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime()) - db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", - arrayOf(media.id.toString())) + db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", arrayOf(media.id.toString())) } else { Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0") } @@ -317,8 +312,7 @@ class PodDBAdapter private constructor() { if (item.id == 0L) { item.id = db.insert(TABLE_NAME_FEED_ITEMS, null, values) } else { - db.update(TABLE_NAME_FEED_ITEMS, values, "$KEY_ID=?", - arrayOf(item.id.toString())) + db.update(TABLE_NAME_FEED_ITEMS, values, "$KEY_ID=?", arrayOf(item.id.toString())) } if (item.media != null) { setMedia(item.media) @@ -329,9 +323,7 @@ class PodDBAdapter private constructor() { return item.id } - fun setFeedItemRead(played: Int, itemId: Long, mediaId: Long, - resetMediaPosition: Boolean - ) { + fun setFeedItemRead(played: Int, itemId: Long, mediaId: Long, resetMediaPosition: Boolean) { try { db.beginTransactionNonExclusive() val values = ContentValues() @@ -387,8 +379,7 @@ class PodDBAdapter private constructor() { if (chapter.id == 0L) { chapter.id = db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values) } else { - db.update(TABLE_NAME_SIMPLECHAPTERS, values, "$KEY_ID=?", - arrayOf(chapter.id.toString())) + db.update(TABLE_NAME_SIMPLECHAPTERS, values, "$KEY_ID=?", arrayOf(chapter.id.toString())) } } } @@ -428,8 +419,7 @@ class PodDBAdapter private constructor() { if (status.id == 0L) { status.id = db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values) } else { - db.update(TABLE_NAME_DOWNLOAD_LOG, values, "$KEY_ID=?", - arrayOf(status.id.toString())) + db.update(TABLE_NAME_DOWNLOAD_LOG, values, "$KEY_ID=?", arrayOf(status.id.toString())) } return status.id } @@ -470,16 +460,12 @@ class PodDBAdapter private constructor() { } fun removeFavoriteItem(item: FeedItem) { - val deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s", - TABLE_NAME_FAVORITES, - KEY_FEEDITEM, item.id, - KEY_FEED, item.feedId) + val deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s", TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id, KEY_FEED, item.feedId) db.execSQL(deleteClause) } private fun isItemInFavorites(item: FeedItem): Boolean { - val query = String.format(Locale.US, "SELECT %s from %s WHERE %s=%d", - KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id) + val query = String.format(Locale.US, "SELECT %s from %s WHERE %s=%d", KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id) val c = db.rawQuery(query, null) val count = c.count c.close() @@ -557,8 +543,7 @@ class PodDBAdapter private constructor() { db.delete(TABLE_NAME_DOWNLOAD_LOG, "$KEY_FEEDFILE=? AND $KEY_FEEDFILETYPE=?", arrayOf(feed.id.toString(), Feed.FEEDFILETYPE_FEED.toString())) - db.delete(TABLE_NAME_FEEDS, "$KEY_ID=?", - arrayOf(feed.id.toString())) + db.delete(TABLE_NAME_FEEDS, "$KEY_ID=?", arrayOf(feed.id.toString())) db.setTransactionSuccessful() } catch (e: SQLException) { Log.e(TAG, Log.getStackTraceString(e)) @@ -717,8 +702,7 @@ class PodDBAdapter private constructor() { val orderByQuery = generateFrom(sortOrder) val filterQuery = generateFrom(filter!!) val whereClause = if ("" == filterQuery) "" else " WHERE $filterQuery" - val query = (SELECT_FEED_ITEMS_AND_MEDIA + whereClause - + "ORDER BY " + orderByQuery + " LIMIT " + offset + ", " + limit) + val query = (SELECT_FEED_ITEMS_AND_MEDIA + whereClause + "ORDER BY " + orderByQuery + " LIMIT " + offset + ", " + limit) return db.rawQuery(query, null) } @@ -771,8 +755,7 @@ class PodDBAdapter private constructor() { get() = DatabaseUtils.queryNumEntries(db, TABLE_NAME_FEED_MEDIA, "$KEY_PLAYBACK_COMPLETION_DATE> 0") fun getSingleFeedMediaCursor(id: Long): Cursor { - val query = ("SELECT " + KEYS_FEED_MEDIA + " FROM " + TABLE_NAME_FEED_MEDIA - + " WHERE " + KEY_ID + "=" + id) + val query = ("SELECT " + KEYS_FEED_MEDIA + " FROM " + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_ID + "=" + id) return db.rawQuery(query, null) } @@ -918,8 +901,7 @@ class PodDBAdapter private constructor() { fun getFeedCounters(setting: FeedCounter?, vararg feedIds: Long): Map { val whereRead = when (setting) { // FeedCounter.SHOW_NEW -> KEY_READ + "=" + FeedItem.NEW - FeedCounter.SHOW_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW - + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")") + FeedCounter.SHOW_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")") FeedCounter.SHOW_DOWNLOADED -> "$KEY_DOWNLOADED=1" FeedCounter.SHOW_DOWNLOADED_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")" @@ -1044,13 +1026,11 @@ class PodDBAdapter private constructor() { "1 = 1" } - val queryStart = (SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION - + " WHERE " + queryFeedId + " AND (") + val queryStart = (SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION + " WHERE " + queryFeedId + " AND (") val sb = StringBuilder(queryStart) for (i in queryWords.indices) { - sb - .append("(") + sb.append("(") .append("$KEY_DESCRIPTION LIKE '%").append(queryWords[i]) .append("%' OR ") .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) @@ -1078,8 +1058,7 @@ class PodDBAdapter private constructor() { val sb = StringBuilder(queryStart) for (i in queryWords.indices) { - sb - .append("(") + sb.append("(") .append(KEY_TITLE).append(" LIKE '%").append(queryWords[i]) .append("%' OR ") .append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i]) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt index c4d34318..8940dacb 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayActionButton.kt @@ -3,9 +3,9 @@ package ac.mdiq.podcini.ui.actions.actionbutton import android.content.Context import androidx.media3.common.util.UnstableApi import ac.mdiq.podcini.R -import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.storage.DBTasks import ac.mdiq.podcini.playback.PlaybackServiceStarter +import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.util.event.playback.StartPlayEvent @@ -33,7 +33,7 @@ class PlayActionButton(item: FeedItem) : ItemActionButton(item) { .start() if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) + context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) } } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt index 696ffad5..f0b7ecbe 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/PlayLocalActionButton.kt @@ -23,7 +23,7 @@ class PlayLocalActionButton(item: FeedItem?) : ItemActionButton(item!!) { .start() if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) + context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) } } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt index ddab6132..a7959876 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/StreamActionButton.kt @@ -43,7 +43,7 @@ class StreamActionButton(item: FeedItem) : ItemActionButton(item) { .start() if (media.getMediaType() == MediaType.VIDEO) { - context.startActivity(getPlayerActivityIntent(context, media)) + context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO)) } } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt index 0a874ecc..c246fdc0 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/menuhandler/FeedItemMenuHandler.kt @@ -85,9 +85,8 @@ object FeedItemMenuHandler { * @param visibility The new visibility status of given menu item */ private fun setItemVisibility(menu: Menu?, menuId: Int, visibility: Boolean) { - if (menu == null) { - return - } + if (menu == null) return + val item = menu.findItem(menuId) item?.setVisible(visibility) } @@ -112,9 +111,8 @@ object FeedItemMenuHandler { */ @UnstableApi fun onPrepareMenu(menu: Menu?, selectedItem: FeedItem?, vararg excludeIds: Int): Boolean { - if (menu == null || selectedItem == null) { - return false - } + if (menu == null || selectedItem == null) return false + val rc = onPrepareMenu(menu, selectedItem) if (rc && excludeIds.isNotEmpty()) { for (id in excludeIds) { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index b62ad692..bfac9e85 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -274,6 +274,7 @@ class MainActivity : CastEnabledActivity() { private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() { override fun onStateChanged(view: View, state: Int) { + Log.d(TAG, "bottomSheet onStateChanged $state") when (state) { BottomSheetBehavior.STATE_COLLAPSED -> { onSlide(view,0.0f) @@ -610,8 +611,8 @@ class MainActivity : CastEnabledActivity() { bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) } intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false) -> { - bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED - bottomSheetCallback.onSlide(dummyView, 1.0f) +// bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED +// bottomSheetCallback.onSlide(dummyView, 1.0f) } else -> { handleDeeplink(intent.data) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt index 46358742..473072df 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/VideoplayerActivity.kt @@ -40,39 +40,45 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode - /** * Activity for playing video files. */ @UnstableApi class VideoplayerActivity : CastEnabledActivity() { + enum class VideoMode(val mode: Int) { + None(0,), + WINDOW_VIEW(1), + FULL_SCREEN_VIEW(2), + AUDIO_ONLY(3) + } + private var _binding: VideoplayerActivityBinding? = null private val binding get() = _binding!! lateinit var videoEpisodeFragment: VideoEpisodeFragment - var videoMode = 0 var switchToAudioOnly = false + override fun onCreate(savedInstanceState: Bundle?) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - videoMode = intent.getIntExtra("fullScreenMode",0) - if (videoMode == 0) { - videoMode = videoPlayMode - if (videoMode == AUDIO_ONLY) { + 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 != FULL_SCREEN_VIEW && videoMode != WINDOW_VIEW) { + if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) { Log.i(TAG, "videoMode not selected, use window mode") - videoMode = WINDOW_VIEW + videoMode = VideoMode.WINDOW_VIEW } } when (videoMode) { - FULL_SCREEN_VIEW -> { + VideoMode.FULL_SCREEN_VIEW -> { window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN) // has to be called before setting layout content supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY) @@ -81,13 +87,14 @@ class VideoplayerActivity : CastEnabledActivity() { window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) window.setFormat(PixelFormat.TRANSPARENT) } - WINDOW_VIEW -> { + VideoMode.WINDOW_VIEW -> { supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY) setTheme(R.style.Theme_Podcini_VideoEpisode) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN) window.setFormat(PixelFormat.TRANSPARENT) } + else -> {} } super.onCreate(savedInstanceState) @@ -156,7 +163,7 @@ class VideoplayerActivity : CastEnabledActivity() { fun toggleViews() { val newIntent = Intent(this, VideoplayerActivity::class.java) - newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW) + newIntent.putExtra(VIDEO_MODE, if (videoMode == VideoMode.FULL_SCREEN_VIEW) VideoMode.WINDOW_VIEW else VideoMode.FULL_SCREEN_VIEW) finish() startActivity(newIntent) } @@ -233,7 +240,7 @@ class VideoplayerActivity : CastEnabledActivity() { menu.findItem(R.id.playback_speed).setVisible(true) menu.findItem(R.id.player_show_chapters).setVisible(true) - if (videoMode == WINDOW_VIEW) { + if (videoMode == VideoMode.WINDOW_VIEW) { menu.findItem(R.id.add_to_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER) menu.findItem(R.id.remove_from_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER) menu.findItem(R.id.set_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER) @@ -316,7 +323,7 @@ class VideoplayerActivity : CastEnabledActivity() { private fun compatEnterPictureInPicture() { if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (videoMode == FULL_SCREEN_VIEW) supportActionBar?.hide() + if (videoMode == VideoMode.FULL_SCREEN_VIEW) supportActionBar?.hide() videoEpisodeFragment.hideVideoControls(false) enterPictureInPictureMode() } @@ -381,9 +388,9 @@ class VideoplayerActivity : CastEnabledActivity() { companion object { private const val TAG = "VideoplayerActivity" - const val WINDOW_VIEW = 1 - const val FULL_SCREEN_VIEW = 2 - const val AUDIO_ONLY = 3 + const val VIDEO_MODE = "Video_Mode" + + var videoMode = VideoMode.None private fun getWebsiteLinkWithFallback(media: Playable?): String? { return when { diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/appstartintent/VideoPlayerActivityStarter.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/appstartintent/VideoPlayerActivityStarter.kt index 5008c6a7..4399f523 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/appstartintent/VideoPlayerActivityStarter.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/appstartintent/VideoPlayerActivityStarter.kt @@ -2,21 +2,25 @@ package ac.mdiq.podcini.ui.activity.appstartintent 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 -import android.os.Build +import androidx.annotation.OptIn +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. */ -class VideoPlayerActivityStarter(private val context: Context) { +@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) { val intent: Intent = Intent(INTENT) init { intent.setPackage(context.packageName) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT) + if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode) } val pendingIntent: PendingIntent 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 4da2f77d..63d55e78 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 @@ -6,36 +6,35 @@ import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding import ac.mdiq.podcini.feed.util.ImageResourceUtils import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils import ac.mdiq.podcini.playback.PlaybackController -import ac.mdiq.podcini.playback.PlaybackController.Companion -import ac.mdiq.podcini.playback.PlaybackServiceStarter import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.cast.CastEnabledActivity -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.playback.service.PlaybackService +import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode +import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.storage.model.feed.Chapter import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable +import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView +import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode +import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode +import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.dialog.SleepTimerDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog -import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler -import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.AUDIO_ONLY import ac.mdiq.podcini.ui.view.ChapterSeekBar import ac.mdiq.podcini.ui.view.PlayButton +import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.TimeSpeedConverter import ac.mdiq.podcini.util.event.FavoritesEvent import ac.mdiq.podcini.util.event.PlayerErrorEvent import ac.mdiq.podcini.util.event.playback.* -import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Build @@ -114,10 +113,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar toolbar = binding.toolbar toolbar.title = "" toolbar.setNavigationOnClickListener { - val bottomSheet = (activity as MainActivity).bottomSheet - if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) - bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED - else bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED +// val mtype = controller?.getMedia()?.getMediaType() +// if (mtype == MediaType.AUDIO || (mtype == MediaType.VIDEO && videoPlayMode == VideoMode.AUDIO_ONLY)) { + val bottomSheet = (activity as MainActivity).bottomSheet +// if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED +// else bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED +// } } toolbar.setOnMenuItemClickListener(this) @@ -400,6 +402,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar if (item == null && isFeedMedia) item = (media as FeedMedia).item FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item) + val mediaType = controller?.getMedia()?.getMediaType() + toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO) + if (controller != null) { toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive()) toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive()) @@ -419,6 +424,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val itemId = menuItem.itemId when (itemId) { + R.id.show_video -> { + controller!!.playPause() + VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start() + return true + } R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> { SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog") return true @@ -523,13 +533,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar Log.d(TAG, "internalPlayerFragment was clicked") val media = controller?.getMedia() if (media != null) { - if (media.getMediaType() == MediaType.AUDIO || videoPlayMode == AUDIO_ONLY) { + val mediaType = media.getMediaType() + if (mediaType == MediaType.AUDIO || + (mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) { controller!!.ensureService() (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) } else { controller?.playPause() // controller!!.ensureService() - val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media) + val intent = PlaybackService.getPlayerActivityIntent(requireContext(), mediaType) startActivity(intent) } } @@ -554,7 +566,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar val media = controller!!.getMedia() if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) { controller!!.playPause() - requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media)) + requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media?.getMediaType())) } else { controller!!.playPause() } @@ -569,8 +581,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } butRev.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) + SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) true } butPlay.setOnClickListener { 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 new file mode 100644 index 00000000..02789ee1 --- /dev/null +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt @@ -0,0 +1,234 @@ +package ac.mdiq.podcini.ui.fragment + +import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding +import ac.mdiq.podcini.storage.model.feed.FeedItem +import android.speech.tts.TextToSpeech +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.util.Log +import android.view.* +import androidx.annotation.OptIn +import androidx.appcompat.widget.Toolbar +import androidx.core.app.ShareCompat +import androidx.fragment.app.Fragment +import androidx.media3.common.util.UnstableApi +import com.google.android.material.appbar.MaterialToolbar +import io.reactivex.disposables.Disposable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import net.dankito.readability4j.Readability4J +import java.io.BufferedReader +import java.io.InputStreamReader +import java.net.URL +import java.util.* + + +/** + * Displays information about an Episode (FeedItem) and actions. + */ +class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToSpeech.OnInitListener { + private var _binding: EpisodeHomeFragmentBinding? = null + private val binding get() = _binding!! + + private var item: FeedItem? = null + + private lateinit var tts: TextToSpeech + private lateinit var toolbar: MaterialToolbar + + private var disposable: Disposable? = null + + private var readerhtml: String? = null + private var textContent: String? = null + private var readMode = false + private var ttsPlaying = false + + 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 + tts = TextToSpeech(requireContext(), this) + } + + @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + super.onCreateView(inflater, container, savedInstanceState) + + _binding = EpisodeHomeFragmentBinding.inflate(inflater, container, false) + Log.d(TAG, "fragment onCreateView") + + toolbar = binding.toolbar + toolbar.title = "" + toolbar.inflateMenu(R.menu.episode_home) + toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } + toolbar.setOnMenuItemClickListener(this) + + if (item?.link != null) { + showContent() + } + updateAppearance() + return binding.root + } + + @OptIn(UnstableApi::class) private fun switchMode() { + readMode = !readMode + showContent() + updateAppearance() + } + + override fun onInit(status: Int) { + if (status == TextToSpeech.SUCCESS) { + // TTS initialization successful + Log.i(TAG, "TTS init success with Locale: ${item?.feed?.language}") + if (item?.feed?.language != null) { + val result = tts.setLanguage(Locale(item!!.feed!!.language)) +// val result = tts.setLanguage(Locale.UK) + if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { + Log.w(TAG, "TTS language not supported") + // Language not supported + // Handle the error or fallback to default behavior + } + } + } else { + // TTS initialization failed + // Handle the error or fallback to default behavior + Log.w(TAG, "TTS init failed") + } + } + + private fun showContent() { + if (readMode) { + if (readerhtml == null) { + runBlocking { + val url = item!!.link!! + val htmlSource = fetchHtmlSource(url) + val readability4J = Readability4J(item?.link!!, htmlSource) + val article = readability4J.parse() + textContent = article.textContent +// Log.d(TAG, "readability4J: ${article.textContent}") + readerhtml = article.contentWithDocumentsCharsetOrUtf8 + } + } + if (readerhtml != null) binding.webView.loadDataWithBaseURL(item!!.link!!, readerhtml!!, "text/html", "UTF-8", null) + } else { + if (item?.link != null) binding.webView.loadUrl(item!!.link!!) + } + } + + private suspend fun fetchHtmlSource(urlString: String): String = withContext(Dispatchers.IO) { + val url = URL(urlString) + val connection = url.openConnection() + val inputStream = connection.getInputStream() + val bufferedReader = BufferedReader(InputStreamReader(inputStream)) + + val stringBuilder = StringBuilder() + var line: String? + while (bufferedReader.readLine().also { line = it } != null) { + stringBuilder.append(line) + } + + bufferedReader.close() + inputStream.close() + + stringBuilder.toString() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + val textSpeech = menu.findItem(R.id.text_speech) + textSpeech.isVisible = readMode + if (readMode) { + if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) + else textSpeech.setIcon(R.drawable.ic_play_24dp) + } + } + + @UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean { + when (menuItem.itemId) { + R.id.switch_home -> { + Log.d(TAG, "switch_home selected") + switchMode() + return true + } + R.id.text_speech -> { + Log.d(TAG, "text_speech selected: $textContent") + if (tts.isSpeaking) tts.stop() + if (!ttsPlaying) { + ttsPlaying = true + if (textContent != null) { + val maxTextLength = 4000 + var startIndex = 0 + var endIndex = minOf(maxTextLength, textContent!!.length) + while (startIndex < textContent!!.length) { + val chunk = textContent!!.substring(startIndex, endIndex) + tts.speak(chunk, TextToSpeech.QUEUE_ADD, null, null) + + startIndex += maxTextLength + endIndex = minOf(endIndex + maxTextLength, textContent!!.length) + } + } + } else ttsPlaying = false + + updateAppearance() + return true + } + R.id.share_notes -> { + if (item == null) return false + val notes = item!!.description + if (!notes.isNullOrEmpty()) { + val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString() + else Html.fromHtml(notes).toString() + val context = requireContext() + val intent = ShareCompat.IntentBuilder(context) + .setType("text/plain") + .setText(shareText) + .setChooserTitle(R.string.share_notes_label) + .createChooserIntent() + context.startActivity(intent) + } + return true + } + else -> { + if (item == null) return false + return true + } + } + } + + @UnstableApi override fun onResume() { + super.onResume() + updateAppearance() + } + + @OptIn(UnstableApi::class) override fun onDestroyView() { + super.onDestroyView() + Log.d(TAG, "onDestroyView") + _binding = null + disposable?.dispose() + tts.shutdown() + } + + @UnstableApi private fun updateAppearance() { + if (item == null) { + Log.d(TAG, "updateAppearance item is null") + return + } + onPrepareOptionsMenu(toolbar.menu) +// FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.switch_home) + } + + companion object { + private const val TAG = "EpisodeWebviewFragment" + private const val ARG_FEEDITEM = "feeditem" + + @JvmStatic + fun newInstance(item: FeedItem): EpisodeHomeFragment { + val fragment = EpisodeHomeFragment() + 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/EpisodeInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index a769332b..d76b22f0 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 @@ -85,12 +85,9 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { private lateinit var imgvCover: ImageView private lateinit var progbarDownload: CircularProgressBar private lateinit var progbarLoading: ProgressBar - private lateinit var butAction1Text: TextView - private lateinit var butAction2Text: TextView - private lateinit var butAction1Icon: ImageView - private lateinit var butAction2Icon: ImageView - private lateinit var butAction1: View - private lateinit var butAction2: View + private lateinit var butAction0: View + private lateinit var butAction1: ImageView + private lateinit var butAction2: ImageView private lateinit var noMediaLabel: View private var actionButton1: ItemActionButton? = null @@ -142,14 +139,15 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { imgvCover.setOnClickListener { openPodcast() } progbarDownload = binding.circularProgressBar progbarLoading = binding.progbarLoading + butAction0 = binding.butAction0 butAction1 = binding.butAction1 butAction2 = binding.butAction2 - butAction1Icon = binding.butAction1Icon - butAction2Icon = binding.butAction2Icon - butAction1Text = binding.butAction1Text - butAction2Text = binding.butAction2Text noMediaLabel = binding.noMediaLabel + butAction0.setOnClickListener(View.OnClickListener { + if (item?.link != null) (activity as MainActivity).loadChildFragment(EpisodeHomeFragment.newInstance(item!!)) + }) + butAction1.setOnClickListener(View.OnClickListener { when { actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload @@ -272,6 +270,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (webviewData != null && !itemsLoaded) { webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData!!, "text/html", "utf-8", "about:blank") } +// if (item?.link != null) binding.webView.loadUrl(item!!.link!!) updateAppearance() } @@ -298,8 +297,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { val options: RequestOptions = RequestOptions() .error(R.color.light_gray) - .transform(FitCenter(), - RoundedCorners((8 * resources.displayMetrics.density).toInt())) + .transform(FitCenter(), RoundedCorners((8 * resources.displayMetrics.density).toInt())) .dontAnimate() val imgLocFB = ImageResourceUtils.getFallbackImageLocation(item!!) @@ -337,8 +335,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { noMediaLabel.visibility = View.GONE if (media.getDuration() > 0) { txtvDuration.text = Converter.getDurationStringLong(media.getDuration()) - txtvDuration.setContentDescription( - Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong())) + txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong())) } if (item != null) { actionButton1 = when { @@ -377,17 +374,17 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener { } if (actionButton1 != null) { - butAction1Text.setText(actionButton1!!.getLabel()) - butAction1Icon.setImageResource(actionButton1!!.getDrawable()) +// butAction1Text.setText(actionButton1!!.getLabel()) + butAction1.setImageResource(actionButton1!!.getDrawable()) } - butAction1Text.transformationMethod = null +// butAction1Text.transformationMethod = null if (actionButton1 != null) butAction1.visibility = actionButton1!!.visibility if (actionButton2 != null) { - butAction2Text.setText(actionButton2!!.getLabel()) - butAction2Icon.setImageResource(actionButton2!!.getDrawable()) +// butAction2Text.setText(actionButton2!!.getLabel()) + butAction2.setImageResource(actionButton2!!.getDrawable()) } - butAction2Text.transformationMethod = null +// butAction2Text.transformationMethod = null if (actionButton2 != null) butAction2.visibility = actionButton2!!.visibility } 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 b2120f94..a4cc3e01 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 @@ -4,7 +4,6 @@ import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding import ac.mdiq.podcini.feed.util.ImageResourceUtils import ac.mdiq.podcini.playback.PlaybackController -import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.model.feed.Chapter import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage @@ -16,6 +15,7 @@ import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.util.ChapterUtils import ac.mdiq.podcini.util.DateFormatter +import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.animation.AnimatorSet @@ -44,6 +44,7 @@ import com.bumptech.glide.RequestBuilder import com.bumptech.glide.load.resource.bitmap.FitCenter import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions +import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import io.reactivex.Maybe import io.reactivex.MaybeEmitter @@ -212,13 +213,9 @@ class PlayerDetailsFragment : Fragment() { val lines = binding.txtvEpisodeTitle.lineCount val animUnit = 1500 if (lines > binding.txtvEpisodeTitle.maxLines) { - val titleHeight = (binding.txtvEpisodeTitle.height - - binding.txtvEpisodeTitle.paddingTop - - binding.txtvEpisodeTitle.paddingBottom) - val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt( - binding.txtvEpisodeTitle, "scrollY", 0, - (lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)) - .setDuration((lines * animUnit).toLong()) + val titleHeight = (binding.txtvEpisodeTitle.height - binding.txtvEpisodeTitle.paddingTop - binding.txtvEpisodeTitle.paddingBottom) + val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(binding.txtvEpisodeTitle, "scrollY", 0, + (lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)).setDuration((lines * animUnit).toLong()) val fadeOut: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 0f) fadeOut.setStartDelay(animUnit.toLong()) fadeOut.addListener(object : AnimatorListenerAdapter() { @@ -254,11 +251,8 @@ class PlayerDetailsFragment : Fragment() { val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE if (binding.chapterButton.visibility != newVisibility) { binding.chapterButton.visibility = newVisibility - ObjectAnimator.ofFloat(binding.chapterButton, - "alpha", - (if (chapterControlVisible) 0 else 1).toFloat(), - (if (chapterControlVisible) 1 else 0).toFloat()) - .start() + ObjectAnimator.ofFloat(binding.chapterButton, "alpha", + (if (chapterControlVisible) 0 else 1).toFloat(), (if (chapterControlVisible) 1 else 0).toFloat()).start() } } @@ -279,8 +273,7 @@ class PlayerDetailsFragment : Fragment() { if (media == null) return val options: RequestOptions = RequestOptions() .dontAnimate() - .transform(FitCenter(), - RoundedCorners((16 * resources.displayMetrics.density).toInt())) + .transform(FitCenter(), RoundedCorners((16 * resources.displayMetrics.density).toInt())) val cover: RequestBuilder = Glide.with(this) .load(media!!.getImageLocation()) @@ -308,9 +301,7 @@ class PlayerDetailsFragment : Fragment() { private val currentChapter: Chapter? get() { - if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) { - return null - } + if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null return media!!.getChapters()[displayedChapterIndex] } @@ -364,6 +355,8 @@ class PlayerDetailsFragment : Fragment() { } @UnstableApi private fun restoreFromPreference(): Boolean { + if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false + Log.d(TAG, "Restoring from preferences") val activity: Activity? = activity if (activity != null) { @@ -411,18 +404,14 @@ class PlayerDetailsFragment : Fragment() { // private fun configureForOrientation(newConfig: Configuration) { // val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT // -// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL +//// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL // // if (isPortrait) { -// binding.coverHolder.layoutParams = -// LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f) -// binding.coverFragmentTextContainer.layoutParams = -// LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) +// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f) +//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) // } else { -// binding.coverHolder.layoutParams = -// LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) -// binding.coverFragmentTextContainer.layoutParams = -// LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) +// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) +//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f) // } // // (binding.episodeDetails.parent as ViewGroup).removeView(binding.episodeDetails) @@ -442,8 +431,7 @@ class PlayerDetailsFragment : Fragment() { val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java) clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text)) if (Build.VERSION.SDK_INT <= 32) { - (requireActivity() as MainActivity).showSnackbarAbovePlayer( - resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) + (requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } return true } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt index 43dfb88b..36c62475 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/VideoEpisodeFragment.kt @@ -12,6 +12,7 @@ import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.ui.activity.VideoplayerActivity +import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.utils.PictureInPictureUtil @@ -343,7 +344,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { } if (videoControlsShowing) { hideVideoControls(false) - if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW) + if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide() videoControlsShowing = false } @@ -360,7 +361,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { fun toggleVideoControlsVisibility() { if (videoControlsShowing) { hideVideoControls(true) - if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW) + if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide() } else { showVideoControls() @@ -461,8 +462,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener { if (videoControlsShowing) { Log.d(TAG, "Hiding video controls") hideVideoControls(true) - if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW) - (activity as? AppCompatActivity)?.supportActionBar?.hide() + if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW) + (activity as? AppCompatActivity)?.supportActionBar?.hide() videoControlsShowing = false } } 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 723146cf..ccc5c907 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 @@ -27,8 +27,7 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva init { val colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary) val colorAccent = colorToHtml(context, R.attr.colorAccent) - val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, - context.resources.displayMetrics).toInt() + val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, context.resources.displayMetrics).toInt() var styleString: String? = "" try { val templateStream = context.assets.open("shownotes-style.css") @@ -36,15 +35,13 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva } catch (e: IOException) { e.printStackTrace() } - webviewStyle = String.format(Locale.US, styleString!!, colorPrimary, colorAccent, - margin, margin, margin, margin) + webviewStyle = String.format(Locale.US, styleString!!, colorPrimary, colorAccent, margin, margin, margin, margin) } private fun colorToHtml(context: Context, colorAttr: Int): String { val res = context.theme.obtainStyledAttributes(intArrayOf(colorAttr)) @ColorInt val col = res.getColor(0, 0) - val color = ("rgba(" + Color.red(col) + "," + Color.green(col) + "," - + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")") + val color = ("rgba(" + Color.red(col) + "," + Color.green(col) + "," + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")") res.recycle() return color } 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 1e5a61a3..be13e7e6 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 @@ -40,9 +40,8 @@ object WidgetUpdater { * Update the widgets with the given parameters. Must be called in a background thread. */ fun updateWidget(context: Context, widgetState: WidgetState?) { - if (!isEnabled(context) || widgetState == null) { - return - } + if (!isEnabled(context) || widgetState == null) return + val startMediaPlayer = if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO) { VideoPlayerActivityStarter(context).pendingIntent } else { @@ -88,8 +87,7 @@ object WidgetUpdater { views.setViewVisibility(R.id.txtvTitle, View.VISIBLE) views.setViewVisibility(R.id.txtNoPlaying, View.GONE) - val progressString = getProgressString(widgetState.position, - widgetState.duration, widgetState.playbackSpeed) + val progressString = getProgressString(widgetState.position, widgetState.duration, widgetState.playbackSpeed) if (progressString != null) { views.setViewVisibility(R.id.txtvProgress, View.VISIBLE) views.setTextViewText(R.id.txtvProgress, progressString) @@ -106,22 +104,16 @@ object WidgetUpdater { views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play) views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label)) } - views.setOnClickPendingIntent(R.id.butPlay, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) - views.setOnClickPendingIntent(R.id.butPlayExtended, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) - views.setOnClickPendingIntent(R.id.butRew, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND)) - views.setOnClickPendingIntent(R.id.butFastForward, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)) - views.setOnClickPendingIntent(R.id.butSkip, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)) + views.setOnClickPendingIntent(R.id.butPlay, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + views.setOnClickPendingIntent(R.id.butRew, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND)) + views.setOnClickPendingIntent(R.id.butFastForward, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD)) + views.setOnClickPendingIntent(R.id.butSkip, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT)) } else { // start the app if they click anything views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer) views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer) - views.setOnClickPendingIntent(R.id.butPlayExtended, - createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) + views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) views.setViewVisibility(R.id.txtvProgress, View.GONE) views.setViewVisibility(R.id.txtvTitle, View.GONE) views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE) @@ -183,27 +175,18 @@ object WidgetUpdater { } private fun getProgressString(position: Int, duration: Int, speed: Float): String? { - if (position < 0 || duration <= 0) { - return null - } + if (position < 0 || duration <= 0) return null + val converter = TimeSpeedConverter(speed) return if (shouldShowRemainingTime()) { - (getDurationStringLong(converter.convert(position)) + " / -" - + getDurationStringLong(converter.convert(max(0.0, - (duration - position).toDouble()) - .toInt()))) + ("${getDurationStringLong(converter.convert(position))} / -${getDurationStringLong(converter.convert(max(0.0, (duration - position).toDouble()).toInt())) + }") } else { - (getDurationStringLong(converter.convert(position)) + " / " - + getDurationStringLong(converter.convert(duration))) + (getDurationStringLong(converter.convert(position)) + " / " + getDurationStringLong(converter.convert(duration))) } } - class WidgetState(val media: Playable?, - val status: PlayerStatus, - val position: Int, - val duration: Int, - val playbackSpeed: Float - ) { + class WidgetState(val media: Playable?, val status: PlayerStatus, val position: Int, val duration: Int, val playbackSpeed: Float) { constructor(status: PlayerStatus) : this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f) } } diff --git a/app/src/main/res/drawable/baseline_home_24.xml b/app/src/main/res/drawable/baseline_home_24.xml new file mode 100644 index 00000000..57d515fa --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/baseline_home_work_24.xml b/app/src/main/res/drawable/baseline_home_work_24.xml new file mode 100644 index 00000000..b9092237 --- /dev/null +++ b/app/src/main/res/drawable/baseline_home_work_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/layout/episode_home_fragment.xml b/app/src/main/res/layout/episode_home_fragment.xml new file mode 100644 index 00000000..8400d222 --- /dev/null +++ b/app/src/main/res/layout/episode_home_fragment.xml @@ -0,0 +1,26 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/episode_info_fragment.xml b/app/src/main/res/layout/episode_info_fragment.xml index e200193a..823b9f36 100644 --- a/app/src/main/res/layout/episode_info_fragment.xml +++ b/app/src/main/res/layout/episode_info_fragment.xml @@ -114,80 +114,53 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:gravity="center_vertical" + android:gravity="center" android:baselineAligned="false"> - + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" + tools:src="@drawable/ic_settings" /> + + + + + + - - - - - - - - - - - - - - - - - + @@ -210,10 +183,17 @@ + + + + + + + diff --git a/app/src/main/res/menu/episode_home.xml b/app/src/main/res/menu/episode_home.xml new file mode 100644 index 00000000..9ba29f01 --- /dev/null +++ b/app/src/main/res/menu/episode_home.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/mediaplayer.xml b/app/src/main/res/menu/mediaplayer.xml index 4534dd98..594888c8 100644 --- a/app/src/main/res/menu/mediaplayer.xml +++ b/app/src/main/res/menu/mediaplayer.xml @@ -2,6 +2,14 @@ + + + - + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png deleted file mode 100644 index b5472227..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png deleted file mode 100644 index f0f90141..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png deleted file mode 100644 index 7e2f933b..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png deleted file mode 100644 index 60a71c94..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png deleted file mode 100644 index 7424edaf..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5671be3f..b637c893 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,6 +11,8 @@ #50000000 #55333333 + #000000 + #FFFFFF #EFEEEE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ae6fc9b..e8606ff8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -255,6 +255,7 @@ %d episode removed from queue. %d episodes removed from queue. + Play video Add to favorites Remove from favorites Visit website diff --git a/app/src/play/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt b/app/src/play/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt index 18dedc77..6d12168c 100644 --- a/app/src/play/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt +++ b/app/src/play/java/ac/mdiq/podcini/service/playback/WearMediaSession.kt @@ -1,7 +1,7 @@ package ac.mdiq.podcini.service.playback import android.os.Bundle -import android.support.v4.media.session.MediaSessionCompat +import androidx.media3.session.MediaSession import android.support.v4.media.session.PlaybackStateCompat import android.support.wearable.media.MediaControlConstants @@ -16,10 +16,10 @@ object WearMediaSession { actionBuilder.setExtras(actionExtras) } - fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat) { + fun mediaSessionSetExtraForWear(mediaSession: MediaSession) { val sessionExtras = Bundle() sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, false) sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, false) - mediaSession.setExtras(sessionExtras) + mediaSession.setSessionExtras(sessionExtras) } } diff --git a/changelog.md b/changelog.md index d79cbca4..1c4c8369 100644 --- a/changelog.md +++ b/changelog.md @@ -272,4 +272,13 @@ * when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view * webkit updated to Androidx * fixed bug in setting speed to wrong categories -* improved fetching of episode images when invalid addresses are given \ No newline at end of file +* improved fetching of episode images when invalid addresses are given + +## 4.9.0 + +* fixed bug of player always expands when changing audio +* migrated to media3's MediaSession and MediaLibraryService thought no new features added with this. some behavior might change or issues might arise, need to be mindful +* when video mode is temporarily audio only, click on image on audio player on a video episode also brings up the normal player detailed view* added a menu action item in player detailed view to turn to fullscreen video for video episode +* added episode home view accessible right from episode info view. episode home view has two display modes: webpage or reader. +* added text-to-speech function in the reader mode. there is a play/pause button on the top action bar, when play is pressed, text-to-speech will be used to play the text. play features now are controlled by system setting of the TTS engine. Advanced operations in Podcini are expected to come later. +* RSS feeds with no playable media can be subscribed and read/listened via the above two ways \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020131.txt b/fastlane/metadata/android/en-US/changelogs/3020131.txt new file mode 100644 index 00000000..9a18421f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020131.txt @@ -0,0 +1,9 @@ + +Version 4.9.0 brings several changes: + +* fixed bug of player always expands when changing audio +* migrated to media3's MediaSession and MediaLibraryService thought no new features added with this. some behavior might change or issues might arise, need to be mindful +* when video mode is temporarily audio only, click on image on audio player on a video episode also brings up the normal player detailed view* added a menu action item in player detailed view to turn to fullscreen video for video episode +* added episode home view accessible right from episode info view. episode home view has two display modes: webpage or reader. +* added text-to-speech function in the reader mode. there is a play/pause button on the top action bar, when play is pressed, text-to-speech will be used to play the text. play features now are controlled by system setting of the TTS engine. Advanced operations in Podcini are expected to come later. +* RSS feeds with no playable media can be subscribed and read/listened via the above two ways diff --git a/images/black 108.png b/images/black 108.png deleted file mode 100644 index f0f90141..00000000 Binary files a/images/black 108.png and /dev/null differ diff --git a/images/black 192.png b/images/black 192.png deleted file mode 100644 index b5472227..00000000 Binary files a/images/black 192.png and /dev/null differ diff --git a/images/black 216.png b/images/black 216.png deleted file mode 100644 index 7e2f933b..00000000 Binary files a/images/black 216.png and /dev/null differ diff --git a/images/black 324.png b/images/black 324.png deleted file mode 100644 index 60a71c94..00000000 Binary files a/images/black 324.png and /dev/null differ diff --git a/images/black 432.png b/images/black 432.png deleted file mode 100644 index 7424edaf..00000000 Binary files a/images/black 432.png and /dev/null differ diff --git a/images/black.png b/images/black.png deleted file mode 100644 index d0cee814..00000000 Binary files a/images/black.png and /dev/null differ