4.9.0 commit

This commit is contained in:
Xilin Jia 2024-04-18 22:56:00 +01:00
parent 1f8bb954a0
commit a40da1310f
49 changed files with 841 additions and 858 deletions

View File

@ -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

View File

@ -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"

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -57,6 +57,7 @@
tools:ignore="ExportedService">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
</intent-filter>

View File

@ -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

View File

@ -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<String, String>()
@ -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<String>
@ -269,8 +240,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
private val formats: List<Format>
get() {
val formats: MutableList<Format> = 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<Int>?) {
@ -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()
}
}
}

View File

@ -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) {

View File

@ -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<MediaSessionCompat.QueueItem>? -> mediaSession?.setQueue(queueItems) },
.subscribe({ queueItems: List<MediaSessionCompat.QueueItem>? ->
// 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<List<MediaBrowserCompat.MediaItem>>
// ) {
// 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<List<MediaBrowserCompat.MediaItem>>
) {
Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId")
result.detach()
// private fun loadChildrenSynchronous(parentId: String): List<MediaBrowserCompat.MediaItem>? {
// val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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<FeedItem?>
// 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<MediaBrowserCompat.MediaItem>? {
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = 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<FeedItem?>
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<Playable>(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<Playable>(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<KeyEvent>(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()
}
}
}
}

View File

@ -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<Int>()
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<Int>()))
@ -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)

View File

@ -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

View File

@ -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<String?>?) {
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<Long, Int> {
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])

View File

@ -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))
}
}
}

View File

@ -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))
}
}
}

View File

@ -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))
}
}
}

View File

@ -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) {

View File

@ -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)

View File

@ -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 {

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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<Drawable> = 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
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color" android:pathData="M1,11l0,10l5,0l0,-6l4,0l0,6l5,0l0,-10l-7,-5z"/>
<path android:fillColor="?attr/action_icon_color" android:pathData="M10,3v1.97l7,5V11h2v2h-2v2h2v2h-2v4h6V3H10zM19,9h-2V7h2V9z"/>
</vector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/episode_home_fragment"
android:fitsSystemWindows="true"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarTheme"
app:navigationContentDescription="@string/toolbar_back_button_content_description"
app:navigationIcon="?homeAsUpIndicator" />
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

View File

@ -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">
<LinearLayout
<ImageView
android:id="@+id/butAction1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:gravity="center">
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
tools:src="@drawable/ic_settings" />
<ImageView
android:id="@+id/butAction0"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="40dp"
android:layout_marginStart="40dp"
android:layout_marginRight="30dp"
android:layout_marginEnd="30dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:src="@drawable/baseline_home_work_24"
tools:src="@drawable/ic_settings" />
<FrameLayout
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_gravity="bottom|end">
<ac.mdiq.podcini.ui.view.CircularProgressBar
android:id="@+id/circularProgressBar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:visibility="gone"
app:foregroundColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/butAction1Icon"
android:id="@+id/butAction2"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:layout_gravity="center"
tools:src="@drawable/ic_settings" />
<TextView
android:id="@+id/butAction1Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Button"
android:textColor="?android:attr/textColorPrimary"
tools:text="Button 1" />
</LinearLayout>
<LinearLayout
android:id="@+id/butAction2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal"
android:background="?android:attr/selectableItemBackground"
android:gravity="center">
<FrameLayout
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:layout_gravity="bottom|end">
<ac.mdiq.podcini.ui.view.CircularProgressBar
android:id="@+id/circularProgressBar"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="center"
android:visibility="gone"
app:foregroundColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/butAction2Icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
tools:src="@drawable/ic_settings" />
</FrameLayout>
<TextView
android:id="@+id/butAction2Text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Button"
android:textColor="?android:attr/textColorPrimary"
tools:text="Button 2" />
</LinearLayout>
</FrameLayout>
</LinearLayout>
@ -210,10 +183,17 @@
<ac.mdiq.podcini.ui.view.ShownotesWebView
android:id="@+id/webvDescription"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/header"
android:foreground="?android:windowContentOverlay" />
<!-- <WebView-->
<!-- android:id="@+id/webView"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_below="@id/webvDescription"-->
<!-- />-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/text_speech"
android:icon="@drawable/ic_play_24dp"
android:title="@string/play_label"
android:visible="false"
custom:showAsAction="always">
</item>
<item
android:id="@+id/switch_home"
android:icon="@drawable/baseline_home_24"
android:title="@string/home_label"
custom:showAsAction="always">
</item>
</menu>

View File

@ -2,6 +2,14 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:custom="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/show_video"
android:icon="@drawable/baseline_fullscreen_24"
android:title="@string/add_to_favorite_label"
android:visible="false"
custom:showAsAction="always">
</item>
<item
android:id="@+id/add_to_favorites_item"
android:icon="@drawable/ic_star_border"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

@ -11,6 +11,8 @@
<color name="feed_image_bg">#50000000</color>
<color name="feed_text_bg">#55333333</color>
<color name="ic_launcher_background">#000000</color>
<!-- Theme colors -->
<color name="background_light">#FFFFFF</color>
<color name="background_elevated_light">#EFEEEE</color>

View File

@ -255,6 +255,7 @@
<item quantity="one">%d episode removed from queue.</item>
<item quantity="other">%d episodes removed from queue.</item>
</plurals>
<string name="show_video_label">Play video</string>
<string name="add_to_favorite_label">Add to favorites</string>
<string name="remove_from_favorite_label">Remove from favorites</string>
<string name="visit_website_label">Visit website</string>

View File

@ -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)
}
}

View File

@ -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
* 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

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB