4.9.5 commit

This commit is contained in:
Xilin Jia 2024-04-26 22:36:37 +01:00
parent 3566f60b3e
commit 45e5fbef88
17 changed files with 361 additions and 262 deletions

View File

@ -158,8 +158,8 @@ android {
// Version code schema (not used): // Version code schema (not used):
// "1.2.3-beta4" -> 1020304 // "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395 // "1.2.3" -> 1020395
versionCode 3020135 versionCode 3020136
versionName "4.9.4" versionName "4.9.5"
def commit = "" def commit = ""
try { try {

View File

@ -165,9 +165,11 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
Log.d(TAG, "Received statusUpdate Intent.") Log.d(TAG, "Received statusUpdate Intent.")
if (playbackService != null) { if (playbackService != null) {
val info = playbackService!!.pSMPInfo val info = playbackService!!.pSMPInfo
if (status != info.playerStatus || media != info.playable) {
status = info.playerStatus status = info.playerStatus
media = info.playable media = info.playable
handleStatus() handleStatus()
}
} else { } else {
Log.w(TAG, "Couldn't receive status update: playbackService was null") Log.w(TAG, "Couldn't receive status update: playbackService was null")
if (PlaybackService.isRunning) { if (PlaybackService.isRunning) {

View File

@ -14,8 +14,7 @@ import android.util.Log
import android.view.SurfaceHolder import android.view.SurfaceHolder
import androidx.core.util.Consumer import androidx.core.util.Consumer
import androidx.media3.common.* import androidx.media3.common.*
import androidx.media3.common.Player.DiscontinuityReason import androidx.media3.common.Player.*
import androidx.media3.common.Player.PositionInfo
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
@ -65,11 +64,14 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
private fun createPlayer() { private fun createPlayer() {
if (exoPlayer == null) createStaticPlayer(context) if (exoPlayer == null) createStaticPlayer(context)
exoPlayer?.addListener(object : Player.Listener { exoPlayer?.addListener(object : Listener {
override fun onPlaybackStateChanged(playbackState: @Player.State Int) { override fun onPlaybackStateChanged(playbackState: @State Int) {
when { when (playbackState) {
audioCompletionListener != null && playbackState == Player.STATE_ENDED -> audioCompletionListener?.run() STATE_ENDED -> {
playbackState == Player.STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED) exoPlayer?.seekTo(C.TIME_UNSET)
if (audioCompletionListener != null) audioCompletionListener?.run()
}
STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
else -> bufferingUpdateListener?.accept(BUFFERING_ENDED) else -> bufferingUpdateListener?.accept(BUFFERING_ENDED)
} }
} }
@ -88,7 +90,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() if (reason == DISCONTINUITY_REASON_SEEK) audioSeekCompleteListener?.run()
} }
override fun onAudioSessionIdChanged(audioSessionId: Int) { override fun onAudioSessionIdChanged(audioSessionId: Int) {
@ -112,7 +114,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
val isPlaying: Boolean val isPlaying: Boolean
get() = exoPlayer!!.playWhenReady get() = exoPlayer!!.isPlaying
// get() = exoPlayer!!.playWhenReady
fun pause() { fun pause() {
exoPlayer?.pause() exoPlayer?.pause()
@ -205,7 +208,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
fun start() { fun start() {
if (exoPlayer?.playbackState == Player.STATE_IDLE) prepare() if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepare()
exoPlayer?.play() exoPlayer?.play()
// Can't set params when paused - so always set it on start in case they changed // Can't set params when paused - so always set it on start in case they changed
@ -258,8 +261,9 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
get() { get() {
val trackSelections = exoPlayer!!.currentTrackSelections val trackSelections = exoPlayer!!.currentTrackSelections
val availableFormats = formats val availableFormats = formats
Log.d(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}")
for (i in 0 until trackSelections.length) { for (i in 0 until trackSelections.length) {
val track = trackSelections[i] as ExoTrackSelection? ?: continue val track = trackSelections[i] as? ExoTrackSelection ?: continue
if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat) if (availableFormats.contains(track.selectedFormat)) return availableFormats.indexOf(track.selectedFormat)
} }
return -1 return -1

View File

@ -125,7 +125,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
return return
} else { } else {
// stop playback of this episode // stop playback of this episode
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) if (playerStatus == PlayerStatus.PAUSED || (playerStatus == PlayerStatus.PLAYING) || playerStatus == PlayerStatus.PREPARED)
playerWrapper?.stop() playerWrapper?.stop()
// set temporarily to pause in order to update list with current position // set temporarily to pause in order to update list with current position
@ -168,8 +168,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
} }
} }
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, this.playable)
this.playable)
if (prepareImmediately) { if (prepareImmediately) {
setPlayerStatus(PlayerStatus.PREPARING, this.playable) setPlayerStatus(PlayerStatus.PREPARING, this.playable)
@ -276,8 +275,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
if (playerWrapper != null && mediaType == MediaType.VIDEO) videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight) if (playerWrapper != null && mediaType == MediaType.VIDEO) videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight)
if (playable != null) { if (playable != null) {
// TODO this call has no effect! val pos = playable!!.getPosition()
if (playable!!.getPosition() > 0) seekTo(playable!!.getPosition()) if (pos > 0) seekTo(pos)
if (playable!!.getDuration() <= 0) { if (playable!!.getDuration() <= 0) {
Log.d(TAG, "Setting duration of media") Log.d(TAG, "Setting duration of media")
@ -318,8 +317,10 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
if (t >= getDuration()) { if (t >= getDuration()) {
Log.d(TAG, "Seek reached end of file, skipping to next episode") Log.d(TAG, "Seek reached end of file, skipping to next episode")
// TODO: test
playerWrapper?.seekTo(t)
endPlayback(true, wasSkipped = true, true, toStoppedState = true) endPlayback(true, wasSkipped = true, true, toStoppedState = true)
return // return
} }
when (playerStatus) { when (playerStatus) {
@ -367,7 +368,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
*/ */
override fun getDuration(): Int { override fun getDuration(): Int {
var retVal = Playable.INVALID_TIME var retVal = Playable.INVALID_TIME
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { if ((playerStatus == PlayerStatus.PLAYING)
|| playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
if (playerWrapper != null) retVal = playerWrapper!!.duration if (playerWrapper != null) retVal = playerWrapper!!.duration
} }
if (retVal <= 0 && playable != null && playable!!.getDuration() > 0) retVal = playable!!.getDuration() if (retVal <= 0 && playable != null && playable!!.getDuration() > 0) retVal = playable!!.getDuration()
@ -379,6 +381,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
*/ */
override fun getPosition(): Int { override fun getPosition(): Int {
var retVal = Playable.INVALID_TIME var retVal = Playable.INVALID_TIME
// TODO: test
if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) {
if (playerWrapper != null) retVal = playerWrapper!!.currentPosition if (playerWrapper != null) retVal = playerWrapper!!.currentPosition
} }
@ -409,7 +412,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
*/ */
override fun getPlaybackSpeed(): Float { override fun getPlaybackSpeed(): Float {
var retVal = 1f var retVal = 1f
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED || playerStatus == PlayerStatus.PREPARED) { if (playerStatus == PlayerStatus.PLAYING|| playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED
|| playerStatus == PlayerStatus.PREPARED) {
if (playerWrapper != null) retVal = playerWrapper!!.currentSpeedMultiplier if (playerWrapper != null) retVal = playerWrapper!!.currentSpeedMultiplier
} }
return retVal return retVal
@ -622,6 +626,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
shouldContinue || toStoppedState -> { shouldContinue || toStoppedState -> {
if (nextMedia == null) { if (nextMedia == null) {
callback.onPlaybackEnded(null, true) callback.onPlaybackEnded(null, true)
playable = null
ExoPlayerWrapper.exoPlayer?.stop()
stop() stop()
} }
val hasNext = nextMedia != null val hasNext = nextMedia != null
@ -660,6 +666,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
} }
}) })
mp.setOnErrorListener(Consumer { message: String -> mp.setOnErrorListener(Consumer { message: String ->
Log.e(TAG, "PlayerErrorEvent: $message")
EventBus.getDefault().postSticky(PlayerErrorEvent(message)) EventBus.getDefault().postSticky(PlayerErrorEvent(message))
}) })
} }

View File

@ -39,12 +39,8 @@ import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode
import ac.mdiq.podcini.preferences.UserPreferences.shouldSkipKeepEpisode import ac.mdiq.podcini.preferences.UserPreferences.shouldSkipKeepEpisode
import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed import ac.mdiq.podcini.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podcini.receiver.MediaButtonReceiver import ac.mdiq.podcini.receiver.MediaButtonReceiver
import ac.mdiq.podcini.service.playback.WearMediaSession
import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.DBReader
import ac.mdiq.podcini.storage.DBWriter import ac.mdiq.podcini.storage.DBWriter
import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItem
@ -67,21 +63,16 @@ import ac.mdiq.podcini.util.event.playback.*
import ac.mdiq.podcini.util.event.settings.SkipIntroEndingChangedEvent import ac.mdiq.podcini.util.event.settings.SkipIntroEndingChangedEvent
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.bluetooth.BluetoothA2dp import android.bluetooth.BluetoothA2dp
import android.content.* import android.content.*
import android.content.pm.PackageManager
import android.media.AudioManager import android.media.AudioManager
import android.os.* import android.os.*
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.text.TextUtils import android.text.TextUtils
import android.util.Log import android.util.Log
import android.util.Pair import android.util.Pair
@ -89,13 +80,17 @@ import android.view.KeyEvent
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.webkit.URLUtil import android.webkit.URLUtil
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.Player.* import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.* import androidx.media3.session.MediaSession
import com.google.common.collect.ImmutableList import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import io.reactivex.Observable import io.reactivex.Observable
@ -189,7 +184,6 @@ class PlaybackService : MediaSessionService() {
if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext) if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext)
mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!) mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!)
.setCallback(MyCallback()) .setCallback(MyCallback())
// .setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
.setCustomLayout(notificationCustomButtons) .setCustomLayout(notificationCustomButtons)
.build() .build()
@ -202,7 +196,7 @@ class PlaybackService : MediaSessionService() {
if (mediaPlayer != null) { if (mediaPlayer != null) {
media = mediaPlayer!!.getPlayable() media = mediaPlayer!!.getPlayable()
wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK
mediaPlayer!!.pause(true, false) mediaPlayer!!.pause(abandonFocus = true, reinit = false)
mediaPlayer!!.shutdown() mediaPlayer!!.shutdown()
} }
mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback) mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback)
@ -215,7 +209,7 @@ class PlaybackService : MediaSessionService() {
Log.d(TAG, "onTaskRemoved") Log.d(TAG, "onTaskRemoved")
val player = mediaSession?.player val player = mediaSession?.player
if (player != null) { if (player != null) {
if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == Player.STATE_ENDED) { if (!player.playWhenReady || player.mediaItemCount == 0 || player.playbackState == STATE_ENDED) {
// Stop the service if not playing, continue playing in the background // Stop the service if not playing, continue playing in the background
// otherwise. // otherwise.
stopSelf() stopSelf()
@ -251,7 +245,7 @@ class PlaybackService : MediaSessionService() {
} }
fun isServiceReady(): Boolean { fun isServiceReady(): Boolean {
return mediaSession?.player?.playbackState != STATE_IDLE return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
} }
private inner class MyCallback : MediaSession.Callback { private inner class MyCallback : MediaSession.Callback {
@ -345,9 +339,7 @@ class PlaybackService : MediaSessionService() {
{ obj: Throwable -> obj.printStackTrace() }) { obj: Throwable -> obj.printStackTrace() })
} }
// private fun createBrowsableMediaItem( // private fun createBrowsableMediaItem(@StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int): MediaItem {
// @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int
// ): MediaBrowserCompat.MediaItem {
// val uri = Uri.Builder() // val uri = Uri.Builder()
// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) // .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
// .authority(resources.getResourcePackageName(icon)) // .authority(resources.getResourcePackageName(icon))
@ -355,17 +347,17 @@ class PlaybackService : MediaSessionService() {
// .appendPath(resources.getResourceEntryName(icon)) // .appendPath(resources.getResourceEntryName(icon))
// .build() // .build()
// //
// val description = MediaDescriptionCompat.Builder() // val description = MediaDescription.Builder()
// .setIconUri(uri) // .setIconUri(uri)
// .setMediaId(resources.getString(title)) // .setMediaId(resources.getString(title))
// .setTitle(resources.getString(title)) // .setTitle(resources.getString(title))
// .setSubtitle(resources.getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes)) // .setSubtitle(resources.getQuantityString(R.plurals.num_episodes, numEpisodes, numEpisodes))
// .build() // .build()
// return MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) // return MediaItem(description, MediaItem.FLAG_BROWSABLE)
// } // }
// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem { // private fun createBrowsableMediaItemForFeed(feed: Feed): MediaItem {
// val builder = MediaDescriptionCompat.Builder() // val builder = MediaDescription.Builder()
// .setMediaId("FeedId:" + feed.id) // .setMediaId("FeedId:" + feed.id)
// .setTitle(feed.title) // .setTitle(feed.title)
// .setDescription(feed.description) // .setDescription(feed.description)
@ -377,13 +369,10 @@ class PlaybackService : MediaSessionService() {
// builder.setMediaUri(Uri.parse(feed.link)) // builder.setMediaUri(Uri.parse(feed.link))
// } // }
// val description = builder.build() // val description = builder.build()
// return MediaBrowserCompat.MediaItem(description, // return MediaItem(description, MediaItem.FLAG_BROWSABLE)
// MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
// } // }
// override fun onLoadChildren(parentId: String, // override fun onLoadChildren(parentId: String, result: Result<List<MediaItem>>) {
// result: Result<List<MediaBrowserCompat.MediaItem>>
// ) {
// Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId") // Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId")
// result.detach() // result.detach()
// //
@ -475,12 +464,14 @@ class PlaybackService : MediaSessionService() {
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) super.onStartCommand(intent, flags, startId)
Log.d(TAG, "OnStartCommand called") // Log.d(TAG, "OnStartCommand called")
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1 val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION) val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE) val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
Log.d(TAG, "OnStartCommand $keycode $customAction $hardwareButton $playable")
if (keycode == -1 && playable == null && customAction == null) { if (keycode == -1 && playable == null && customAction == null) {
Log.e(TAG, "PlaybackService was started with no arguments") Log.e(TAG, "PlaybackService was started with no arguments")
return START_NOT_STICKY return START_NOT_STICKY
@ -556,37 +547,38 @@ class PlaybackService : MediaSessionService() {
return return
} }
// val intentAllowThisTime = Intent(originalIntent) val intentAllowThisTime = Intent(originalIntent)
// intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME) intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME)
// intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true) intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true)
// val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) 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) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
// else PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, else PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
// val intentAlwaysAllow = Intent(intentAllowThisTime) val intentAlwaysAllow = Intent(intentAllowThisTime)
// intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS) intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS)
// intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true) intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true)
// val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) 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) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
// else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow, else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, NotificationUtils.CHANNEL_ID_USER_ACTION)
.setSmallIcon(R.drawable.ic_notification_stream)
.setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title))
.setContentText(getString(R.string.confirm_mobile_streaming_notification_message))
.setStyle(NotificationCompat.BigTextStyle()
.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)
.setAutoCancel(true)
// val builder = NotificationCompat.Builder(this, val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// NotificationUtils.CHANNEL_ID_USER_ACTION) notificationManager.notify(5566, builder.build())
// .setSmallIcon(R.drawable.ic_notification_stream)
// .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title))
// .setContentText(getString(R.string.confirm_mobile_streaming_notification_message))
// .setStyle(NotificationCompat.BigTextStyle()
// .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)
// .setAutoCancel(true)
} }
/** /**
@ -670,7 +662,8 @@ class PlaybackService : MediaSessionService() {
return false return false
} }
KeyEvent.KEYCODE_MEDIA_STOP -> { KeyEvent.KEYCODE_MEDIA_STOP -> {
if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) mediaPlayer?.pause(true, true) if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING)
mediaPlayer?.pause(abandonFocus = true, reinit = true)
return true return true
} }
else -> { else -> {
@ -705,7 +698,7 @@ class PlaybackService : MediaSessionService() {
val localFeed = URLUtil.isContentUrl(playable.getStreamUrl()) val localFeed = URLUtil.isContentUrl(playable.getStreamUrl())
val stream = !playable.localFileAvailable() || localFeed val stream = !playable.localFileAvailable() || localFeed
if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) { if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) {
// displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent) displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
writeNoMediaPlaying() writeNoMediaPlaying()
return return
} }
@ -716,8 +709,9 @@ class PlaybackService : MediaSessionService() {
mediaPlayer?.playMediaObject(playable, stream, true, true) mediaPlayer?.playMediaObject(playable, stream, true, true)
recreateMediaSessionIfNeeded() recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(playable) // updateNotificationAndMediaSession(playable)
addPlayableToQueue(playable) addPlayableToQueue(playable)
// EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_RESTARTED))
} }
/** /**
@ -732,7 +726,7 @@ class PlaybackService : MediaSessionService() {
fun notifyVideoSurfaceAbandoned() { fun notifyVideoSurfaceAbandoned() {
mediaPlayer?.pause(true, false) mediaPlayer?.pause(true, false)
mediaPlayer?.resetVideoSurface() mediaPlayer?.resetVideoSurface()
updateNotificationAndMediaSession(playable) // updateNotificationAndMediaSession(playable)
} }
private val taskManagerCallback: PSTMCallback = object : PSTMCallback { private val taskManagerCallback: PSTMCallback = object : PSTMCallback {
@ -755,19 +749,19 @@ class PlaybackService : MediaSessionService() {
override fun statusChanged(newInfo: PSMPInfo?) { override fun statusChanged(newInfo: PSMPInfo?) {
currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN
Log.d(TAG, "statusChanged called") Log.d(TAG, "statusChanged called")
updateMediaSession(newInfo?.playerStatus) // updateMediaSession(newInfo?.playerStatus)
if (newInfo != null) { if (newInfo != null) {
when (newInfo.playerStatus) { when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> { PlayerStatus.INITIALIZED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
updateNotificationAndMediaSession(newInfo.playable) // updateNotificationAndMediaSession(newInfo.playable)
} }
PlayerStatus.PREPARED -> { PlayerStatus.PREPARED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem) if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
taskManager.startChapterLoader(newInfo.playable!!) taskManager.startChapterLoader(newInfo.playable!!)
} }
PlayerStatus.PAUSED -> { PlayerStatus.PAUSED -> {
updateNotificationAndMediaSession(newInfo.playable) // updateNotificationAndMediaSession(newInfo.playable)
cancelPositionObserver() cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus) if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
} }
@ -776,7 +770,7 @@ class PlaybackService : MediaSessionService() {
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus) if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
saveCurrentPosition(true, null, Playable.INVALID_TIME) saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded() recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(newInfo.playable) // updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver() setupPositionObserver()
// set sleep timer if auto-enabled // set sleep timer if auto-enabled
var autoEnableByTime = true var autoEnableByTime = true
@ -815,7 +809,7 @@ class PlaybackService : MediaSessionService() {
override fun onMediaChanged(reloadUI: Boolean) { override fun onMediaChanged(reloadUI: Boolean) {
Log.d(TAG, "reloadUI callback reached") Log.d(TAG, "reloadUI callback reached")
if (reloadUI) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) if (reloadUI) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0)
updateNotificationAndMediaSession(this@PlaybackService.playable) // 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) {
@ -876,7 +870,7 @@ class PlaybackService : MediaSessionService() {
// Playable is being streamed and does not have a duration specified in the feed // Playable is being streamed and does not have a duration specified in the feed
playable.setDuration(mediaPlayer!!.getDuration()) playable.setDuration(mediaPlayer!!.getDuration())
DBWriter.setFeedMedia(playable as FeedMedia?) DBWriter.setFeedMedia(playable as FeedMedia?)
updateNotificationAndMediaSession(playable) // updateNotificationAndMediaSession(playable)
} }
} }
} }
@ -924,12 +918,12 @@ class PlaybackService : MediaSessionService() {
if (!isFollowQueue) { if (!isFollowQueue) {
Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.") Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.")
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED, currentitem) writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED, currentitem)
updateNotificationAndMediaSession(nextItem.media) // updateNotificationAndMediaSession(nextItem.media)
return null return null
} }
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) {
// displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent) displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent)
writeNoMediaPlaying() writeNoMediaPlaying()
return null return null
} }
@ -1081,62 +1075,62 @@ class PlaybackService : MediaSessionService() {
* @param playerStatus the current [PlayerStatus] * @param playerStatus the current [PlayerStatus]
*/ */
private fun updateMediaSession(playerStatus: PlayerStatus?) { private fun updateMediaSession(playerStatus: PlayerStatus?) {
val sessionState = PlaybackStateCompat.Builder() // val sessionState = PlaybackStateCompat.Builder()
val state = if (playerStatus != null) { // val state = if (playerStatus != null) {
when (playerStatus) { // when (playerStatus) {
PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING // PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING // PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED // PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED
PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED // PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED
PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING // PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING
PlayerStatus.PREPARING, PlayerStatus.INITIALIZING -> PlaybackStateCompat.STATE_CONNECTING // PlayerStatus.PREPARING, PlayerStatus.INITIALIZING -> PlaybackStateCompat.STATE_CONNECTING
PlayerStatus.ERROR -> PlaybackStateCompat.STATE_ERROR // PlayerStatus.ERROR -> PlaybackStateCompat.STATE_ERROR
PlayerStatus.INITIALIZED, PlayerStatus.INDETERMINATE -> PlaybackStateCompat.STATE_NONE // PlayerStatus.INITIALIZED, PlayerStatus.INDETERMINATE -> PlaybackStateCompat.STATE_NONE
} // }
} else { // } else {
PlaybackStateCompat.STATE_NONE // PlaybackStateCompat.STATE_NONE
} // }
//
sessionState.setState(state, currentPosition.toLong(), currentPlaybackSpeed) // sessionState.setState(state, currentPosition.toLong(), currentPlaybackSpeed)
val capabilities = (PlaybackStateCompat.ACTION_PLAY // val capabilities = (PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PLAY_PAUSE // or PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_REWIND // or PlaybackStateCompat.ACTION_REWIND
or PlaybackStateCompat.ACTION_PAUSE // or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_FAST_FORWARD // or PlaybackStateCompat.ACTION_FAST_FORWARD
or PlaybackStateCompat.ACTION_SEEK_TO // or PlaybackStateCompat.ACTION_SEEK_TO
or PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED) // or PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED)
//
sessionState.setActions(capabilities) // sessionState.setActions(capabilities)
// On Android Auto, custom actions are added in the following order around the play button, if no default // 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 // 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) // WearMediaSession.addWearExtrasToAction(rewindBuilder)
sessionState.addCustomAction(rewindBuilder.build()) //// 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) // WearMediaSession.addWearExtrasToAction(fastForwardBuilder)
sessionState.addCustomAction(fastForwardBuilder.build()) // sessionState.addCustomAction(fastForwardBuilder.build())
if (showPlaybackSpeedOnFullNotification()) // if (showPlaybackSpeedOnFullNotification())
sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED, // sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED,
getString(R.string.playback_speed), R.drawable.ic_notification_playback_speed).build()) // getString(R.string.playback_speed), R.drawable.ic_notification_playback_speed).build())
if (showNextChapterOnFullNotification()) { // if (showNextChapterOnFullNotification()) {
if (!playable?.getChapters().isNullOrEmpty()) // if (!playable?.getChapters().isNullOrEmpty())
sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_NEXT_CHAPTER, // sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_NEXT_CHAPTER,
getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter).build()) // getString(R.string.next_chapter), R.drawable.ic_notification_next_chapter).build())
} // }
if (showSkipOnFullNotification()) // if (showSkipOnFullNotification())
sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT, // sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT,
getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build()) // getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).build())
if (mediaSession != null) { // if (mediaSession != null) {
WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!) // WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
// mediaSession!!.setPlaybackState(sessionState.build()) //// mediaSession!!.setPlaybackState(sessionState.build())
} // }
} }
private fun updateNotificationAndMediaSession(p: Playable?) { private fun updateNotificationAndMediaSession(p: Playable?) {
@ -1147,25 +1141,27 @@ class PlaybackService : MediaSessionService() {
private fun updateMediaSessionMetadata(p: Playable?) { private fun updateMediaSessionMetadata(p: Playable?) {
if (p == null || mediaSession == null) return if (p == null || mediaSession == null) return
// TODO: what's this? // TODO: how to set meta data
// val builder = MediaMetadataCompat.Builder() // val builder = MediaMetadata.Builder()
// builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()) // builder.setArtist(p.getFeedTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()) // builder.setTitle(p.getEpisodeTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()) // builder.setAlbumArtist(p.getFeedTitle())
// builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong()) //// builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong())
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()) // builder.setDisplayTitle(p.getEpisodeTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()) // builder.setSubtitle(p.getFeedTitle())
// TODO: what's this? // TODO: what's this?
// mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, // mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
// getPlayerActivityIntent(this), FLAG_IMMUTABLE)) // getPlayerActivityIntent(this), FLAG_IMMUTABLE))
// try { // try {
// mediaSession!!.setMetadata(builder.build()) //// mediaSession!!.setMetadata(builder.build())
// val mediaItem = MediaItem.Builder().setMediaMetadata(builder.build()).build()
// } catch (e: OutOfMemoryError) { // } catch (e: OutOfMemoryError) {
// Log.e(TAG, "Setting media session metadata", e) // Log.e(TAG, "Setting media session metadata", e)
// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null) // builder.setArtworkUri(null)
// mediaSession!!.setMetadata(builder.build()) //// mediaSession!!.setMetadata(builder.build())
// val mediaItem = MediaItem.Builder().setMediaMetadata(builder.build()).build()
// } // }
} }
@ -1178,16 +1174,16 @@ class PlaybackService : MediaSessionService() {
* Prepares notification and starts the service in the foreground. * Prepares notification and starts the service in the foreground.
*/ */
// TODO: not needed? // TODO: not needed?
@Synchronized // @Synchronized
private fun setupNotification(playable: Playable?) { // private fun setupNotification(playable: Playable?) {
Log.d(TAG, "setupNotification") // Log.d(TAG, "setupNotification")
playableIconLoaderThread?.interrupt() // playableIconLoaderThread?.interrupt()
//
if (playable == null || mediaPlayer == null) { // if (playable == null || mediaPlayer == null) {
Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer") // Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer")
return // return
} // }
} // }
/** /**
* Persists the current position and last played time of the media file. * Persists the current position and last played time of the media file.
@ -1362,14 +1358,12 @@ class PlaybackService : MediaSessionService() {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused") @Suppress("unused")
fun speedPresetChanged(event: SpeedPresetChangedEvent) { fun onSpeedPresetChanged(event: SpeedPresetChangedEvent) {
val item = (playable as? FeedMedia)?.item ?: currentitem val item = (playable as? FeedMedia)?.item ?: currentitem
// if (playable is FeedMedia) {
if (item?.feed?.id == event.feedId) { if (item?.feed?.id == event.feedId) {
if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) setSpeed(getPlaybackSpeed(playable!!.getMediaType())) if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) setSpeed(getPlaybackSpeed(playable!!.getMediaType()))
else setSpeed(event.speed) else setSpeed(event.speed)
} }
// }
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@ -1394,7 +1388,6 @@ class PlaybackService : MediaSessionService() {
currentitem = event.item currentitem = event.item
} }
fun resume() { fun resume() {
mediaPlayer?.resume() mediaPlayer?.resume()
taskManager.restartSleepTimer() taskManager.restartSleepTimer()

View File

@ -14,9 +14,9 @@ class VisitWebsiteActionButton(item: FeedItem) : ItemActionButton(item) {
return R.drawable.ic_web return R.drawable.ic_web
} }
override fun onClick(context: Context) { override fun onClick(context: Context) {
if (item.link!= null) openInBrowser(context, item.link!!) if (!item.link.isNullOrEmpty()) openInBrowser(context, item.link!!)
} }
override val visibility: Int override val visibility: Int
get() = if (item.link == null) View.INVISIBLE else View.VISIBLE get() = if (item.link.isNullOrEmpty()) View.INVISIBLE else View.VISIBLE
} }

View File

@ -26,8 +26,6 @@ import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
import ac.mdiq.podcini.ui.fragment.EpisodeHomeFragment.Companion.fetchHtmlSource
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.view.ChapterSeekBar import ac.mdiq.podcini.ui.view.ChapterSeekBar
import ac.mdiq.podcini.ui.view.PlayButton import ac.mdiq.podcini.ui.view.PlayButton
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
@ -65,8 +63,6 @@ import io.reactivex.MaybeEmitter
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.runBlocking
import net.dankito.readability4j.Readability4J
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -75,6 +71,7 @@ import java.text.NumberFormat
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
/** /**
* Shows the audio player. * Shows the audio player.
*/ */
@ -192,7 +189,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val theMedia = controller?.getMedia() ?: return val theMedia = controller?.getMedia() ?: return
Log.d(TAG, "loadMediaInfo $theMedia") Log.d(TAG, "loadMediaInfo $theMedia")
if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) { if (currentMedia == null || theMedia.getIdentifier() != currentMedia?.getIdentifier()) {
Log.d(TAG, "loadMediaInfo loading details") Log.d(TAG, "loadMediaInfo loading details")
disposable?.dispose() disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> -> disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
@ -297,6 +294,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
currentitem = event.item currentitem = event.item
if (currentMedia?.getIdentifier() == null || currentitem!!.media!!.getIdentifier() != currentMedia?.getIdentifier()) if (currentMedia?.getIdentifier() == null || currentitem!!.media!!.getIdentifier() != currentMedia?.getIdentifier())
itemDescFrag.setItem(currentitem!!) itemDescFrag.setItem(currentitem!!)
(activity as MainActivity).setPlayerVisible(true)
} }
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@ -404,8 +402,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
return true return true
} }
R.id.share_notes -> { R.id.share_notes -> {
if (feedItem == null) return false val notes = if (itemDescFrag.showHomeText) itemDescFrag.readerhtml else feedItem?.description
val notes = feedItem.description
if (!notes.isNullOrEmpty()) { if (!notes.isNullOrEmpty()) {
val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString() val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString()
else Html.fromHtml(notes).toString() else Html.fromHtml(notes).toString()
@ -649,6 +646,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
when (event.action) { when (event.action) {
PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false) PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false)
PlaybackServiceEvent.Action.SERVICE_STARTED -> (activity as MainActivity).setPlayerVisible(true) PlaybackServiceEvent.Action.SERVICE_STARTED -> (activity as MainActivity).setPlayerVisible(true)
// PlaybackServiceEvent.Action.SERVICE_RESTARTED -> (activity as MainActivity).setPlayerVisible(true)
} }
} }

View File

@ -4,12 +4,14 @@ import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedItem
import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import android.speech.tts.TextToSpeech
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
@ -37,22 +39,18 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
// private var item: FeedItem? = null // private var item: FeedItem? = null
private lateinit var tts: TextToSpeech private var startIndex = 0
private var tts: TextToSpeech? = null
private var ttsSpeed = 1.0f
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private var disposable: Disposable? = null private var disposable: Disposable? = null
// private var readerhtml: String? = null private var readerhtml: String? = null
private var readMode = false private var readMode = false
private var ttsPlaying = false private var ttsPlaying = false
private var jsEnabled = 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 { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
@ -66,7 +64,22 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
if (currentItem?.link != null) showContent() if (!currentItem?.link.isNullOrEmpty()) showContent()
else {
Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
parentFragmentManager.popBackStack()
}
binding.webView.apply {
webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
val isEmpty = view?.title.isNullOrEmpty() && view?.contentDescription.isNullOrEmpty()
if (isEmpty) {
Log.d(TAG, "content is empty")
}
}
}
}
updateAppearance() updateAppearance()
return binding.root return binding.root
@ -80,34 +93,31 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
override fun onInit(status: Int) { override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) { if (status == TextToSpeech.SUCCESS) {
// TTS initialization successful
Log.i(TAG, "TTS init success with Locale: ${currentItem?.feed?.language}") Log.i(TAG, "TTS init success with Locale: ${currentItem?.feed?.language}")
if (currentItem?.feed?.language != null) { if (currentItem?.feed?.language != null) {
val result = tts.setLanguage(Locale(currentItem!!.feed!!.language!!)) val result = tts?.setLanguage(Locale(currentItem!!.feed!!.language!!))
// val result = tts.setLanguage(Locale.UK)
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) { if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.w(TAG, "TTS language not supported") Log.w(TAG, "TTS language not supported ${currentItem?.feed?.language}")
// Language not supported Toast.makeText(context, R.string.language_not_supported_by_tts, Toast.LENGTH_LONG).show()
// Handle the error or fallback to default behavior
} }
ttsSpeed = currentItem?.feed?.preferences?.feedPlaybackSpeed ?: 1.0f
tts?.setSpeechRate(ttsSpeed)
} }
} else { } else {
// TTS initialization failed
// Handle the error or fallback to default behavior
Log.w(TAG, "TTS init failed") Log.w(TAG, "TTS init failed")
Toast.makeText(context, R.string.tts_init_failed, Toast.LENGTH_LONG).show()
} }
} }
private fun showContent() { private fun showReaderContent() {
if (readMode) { if (!currentItem?.link.isNullOrEmpty()) {
var readerhtml: String? = null
if (cleanedNotes == null) { if (cleanedNotes == null) {
runBlocking { runBlocking {
val url = currentItem!!.link!! val url = currentItem!!.link!!
val htmlSource = fetchHtmlSource(url) val htmlSource = fetchHtmlSource(url)
val readability4J = Readability4J(currentItem?.link!!, htmlSource) val readability4J = Readability4J(currentItem?.link!!, htmlSource)
val article = readability4J.parse() val article = readability4J.parse()
textContent = article.textContent readerText = article.textContent
// Log.d(TAG, "readability4J: ${article.textContent}") // Log.d(TAG, "readability4J: ${article.textContent}")
readerhtml = article.contentWithDocumentsCharsetOrUtf8 readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (!readerhtml.isNullOrEmpty()) { if (!readerhtml.isNullOrEmpty()) {
@ -116,19 +126,28 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
} }
} }
} }
}
if (!cleanedNotes.isNullOrEmpty()) { if (!cleanedNotes.isNullOrEmpty()) {
if (tts == null) tts = TextToSpeech(requireContext(), this)
binding.readerView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null) binding.readerView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null)
// binding.readerView.loadDataWithBaseURL(currentItem!!.link!!, readerhtml!!, "text/html", "UTF-8", null)
binding.readerView.visibility = View.VISIBLE binding.readerView.visibility = View.VISIBLE
binding.webView.visibility = View.GONE binding.webView.visibility = View.GONE
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() } else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
} else { }
private fun showWebContent() {
if (!currentItem?.link.isNullOrEmpty()) { if (!currentItem?.link.isNullOrEmpty()) {
binding.webView.settings.javaScriptEnabled = jsEnabled
Log.d(TAG, "currentItem!!.link ${currentItem!!.link}")
binding.webView.loadUrl(currentItem!!.link!!) binding.webView.loadUrl(currentItem!!.link!!)
binding.readerView.visibility = View.GONE binding.readerView.visibility = View.GONE
binding.webView.visibility = View.VISIBLE binding.webView.visibility = View.VISIBLE
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() } else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
} }
private fun showContent() {
if (readMode) showReaderContent()
else showWebContent()
} }
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
@ -138,6 +157,7 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
if (readMode) { if (readMode) {
if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) else textSpeech.setIcon(R.drawable.ic_play_24dp) if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) else textSpeech.setIcon(R.drawable.ic_play_24dp)
} }
menu.findItem(R.id.share_notes).setVisible(readMode)
} }
@UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean { @UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean {
@ -147,31 +167,35 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
switchMode() switchMode()
return true return true
} }
R.id.switchJS -> {
Log.d(TAG, "switchJS selected")
jsEnabled = !jsEnabled
showWebContent()
return true
}
R.id.text_speech -> { R.id.text_speech -> {
Log.d(TAG, "text_speech selected: $textContent") Log.d(TAG, "text_speech selected: $readerText")
if (tts.isSpeaking) tts.stop() if (tts != null) {
if (tts!!.isSpeaking) tts?.stop()
if (!ttsPlaying) { if (!ttsPlaying) {
ttsPlaying = true ttsPlaying = true
if (textContent != null) { if (!readerText.isNullOrEmpty()) {
val maxTextLength = 4000 tts?.setSpeechRate(ttsSpeed)
var startIndex = 0 while (startIndex < readerText!!.length) {
var endIndex = minOf(maxTextLength, textContent!!.length) val endIndex = minOf(startIndex + maxChunkLength, readerText!!.length)
while (startIndex < textContent!!.length) { val chunk = readerText!!.substring(startIndex, endIndex)
val chunk = textContent!!.substring(startIndex, endIndex) tts?.speak(chunk, TextToSpeech.QUEUE_ADD, null, null)
tts.speak(chunk, TextToSpeech.QUEUE_ADD, null, null) startIndex += maxChunkLength
startIndex += maxTextLength
endIndex = minOf(endIndex + maxTextLength, textContent!!.length)
} }
} }
} else ttsPlaying = false } else ttsPlaying = false
updateAppearance() updateAppearance()
}
return true return true
} }
R.id.share_notes -> { R.id.share_notes -> {
if (currentItem == null) return false val notes = readerhtml
val notes = currentItem!!.description
if (!notes.isNullOrEmpty()) { if (!notes.isNullOrEmpty()) {
val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString() val shareText = if (Build.VERSION.SDK_INT >= 24) Html.fromHtml(notes, Html.FROM_HTML_MODE_LEGACY).toString()
else Html.fromHtml(notes).toString() else Html.fromHtml(notes).toString()
@ -201,7 +225,7 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
Log.d(TAG, "onDestroyView") Log.d(TAG, "onDestroyView")
_binding = null _binding = null
disposable?.dispose() disposable?.dispose()
tts.shutdown() tts?.shutdown()
} }
@UnstableApi private fun updateAppearance() { @UnstableApi private fun updateAppearance() {
@ -214,12 +238,14 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
} }
companion object { companion object {
private const val TAG = "EpisodeWebviewFragment" private const val TAG = "EpisodeHomeFragment"
private const val ARG_FEEDITEM = "feeditem" private const val ARG_FEEDITEM = "feeditem"
private var textContent: String? = null const val maxChunkLength = 200
private var readerText: String? = null
private var cleanedNotes: String? = null private var cleanedNotes: String? = null
private var currentItem: FeedItem? = null var currentItem: FeedItem? = null
@JvmStatic @JvmStatic
fun newInstance(item: FeedItem): EpisodeHomeFragment { fun newInstance(item: FeedItem): EpisodeHomeFragment {
@ -229,7 +255,9 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
if (item.itemIdentifier != currentItem?.itemIdentifier) { if (item.itemIdentifier != currentItem?.itemIdentifier) {
currentItem = item currentItem = item
cleanedNotes = null cleanedNotes = null
textContent = null readerText = null
} else {
currentItem?.feed = item.feed
} }
// args.putSerializable(ARG_FEEDITEM, item) // args.putSerializable(ARG_FEEDITEM, item)
// fragment.arguments = args // fragment.arguments = args

View File

@ -17,6 +17,7 @@ import ac.mdiq.podcini.ui.view.CircularProgressBar
import ac.mdiq.podcini.ui.utils.ThemeUtils import ac.mdiq.podcini.ui.utils.ThemeUtils
import ac.mdiq.podcini.ui.utils.ShownotesCleaner import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler import ac.mdiq.podcini.ui.actions.menuhandler.FeedItemMenuHandler
import ac.mdiq.podcini.ui.fragment.EpisodeHomeFragment.Companion.currentItem
import ac.mdiq.podcini.ui.view.ShownotesWebView import ac.mdiq.podcini.ui.view.ShownotesWebView
import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.Converter
import ac.mdiq.podcini.util.DateFormatter import ac.mdiq.podcini.util.DateFormatter
@ -25,6 +26,7 @@ import ac.mdiq.podcini.util.event.EpisodeDownloadEvent
import ac.mdiq.podcini.util.event.FeedItemEvent import ac.mdiq.podcini.util.event.FeedItemEvent
import ac.mdiq.podcini.util.event.PlayerStatusEvent import ac.mdiq.podcini.util.event.PlayerStatusEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
@ -71,6 +73,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var _binding: EpisodeInfoFragmentBinding? = null private var _binding: EpisodeInfoFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var homeFragment: EpisodeHomeFragment? = null
private var itemsLoaded = false private var itemsLoaded = false
private var item: FeedItem? = null private var item: FeedItem? = null
private var webviewData: String? = null private var webviewData: String? = null
@ -141,7 +145,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
noMediaLabel = binding.noMediaLabel noMediaLabel = binding.noMediaLabel
butAction0.setOnClickListener { butAction0.setOnClickListener {
if (item?.link != null) (activity as MainActivity).loadChildFragment(EpisodeHomeFragment.newInstance(item!!)) homeFragment = EpisodeHomeFragment.newInstance(item!!)
if (item?.link != null) (activity as MainActivity).loadChildFragment(homeFragment!!)
} }
butAction1.setOnClickListener(View.OnClickListener { butAction1.setOnClickListener(View.OnClickListener {
@ -398,7 +403,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi private fun load() { @UnstableApi private fun load() {
disposable?.dispose() disposable?.dispose()
if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE
disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() } disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }

View File

@ -64,7 +64,7 @@ import org.greenrobot.eventbus.ThreadMode
*/ */
@UnstableApi @UnstableApi
class PlayerDetailsFragment : Fragment() { class PlayerDetailsFragment : Fragment() {
private lateinit var webvDescription: ShownotesWebView private lateinit var shownoteView: ShownotesWebView
private var _binding: PlayerDetailsFragmentBinding? = null private var _binding: PlayerDetailsFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -79,8 +79,9 @@ class PlayerDetailsFragment : Fragment() {
private var webViewLoader: Disposable? = null private var webViewLoader: Disposable? = null
private var controller: PlaybackController? = null private var controller: PlaybackController? = null
private var showHomeText = false internal var showHomeText = false
var homeText: String? = null internal var homeText: String? = null
internal var readerhtml: String? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Log.d(TAG, "fragment onCreateView") Log.d(TAG, "fragment onCreateView")
@ -96,20 +97,20 @@ class PlayerDetailsFragment : Fragment() {
binding.butNextChapter.setOnClickListener { seekToNextChapter() } binding.butNextChapter.setOnClickListener { seekToNextChapter() }
Log.d(TAG, "fragment onCreateView") Log.d(TAG, "fragment onCreateView")
webvDescription = binding.webview shownoteView = binding.webview
webvDescription.setTimecodeSelectedListener { time: Int? -> controller?.seekTo(time!!) } shownoteView.setTimecodeSelectedListener { time: Int? -> controller?.seekTo(time!!) }
webvDescription.setPageFinishedListener { shownoteView.setPageFinishedListener {
// Restoring the scroll position might not always work // Restoring the scroll position might not always work
webvDescription.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50) shownoteView.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
} }
binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener { binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener {
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) { override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
if (binding.root.measuredHeight != webvDescription.minimumHeight) webvDescription.setMinimumHeight(binding.root.measuredHeight) if (binding.root.measuredHeight != shownoteView.minimumHeight) shownoteView.setMinimumHeight(binding.root.measuredHeight)
binding.root.removeOnLayoutChangeListener(this) binding.root.removeOnLayoutChangeListener(this)
} }
}) })
registerForContextMenu(webvDescription) registerForContextMenu(shownoteView)
controller = object : PlaybackController(requireActivity()) { controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() { override fun loadMediaInfo() {
load() load()
@ -125,12 +126,12 @@ class PlayerDetailsFragment : Fragment() {
controller?.release() controller?.release()
controller = null controller = null
Log.d(TAG, "Fragment destroyed") Log.d(TAG, "Fragment destroyed")
webvDescription.removeAllViews() shownoteView.removeAllViews()
webvDescription.destroy() shownoteView.destroy()
} }
override fun onContextItemSelected(item: MenuItem): Boolean { override fun onContextItemSelected(item: MenuItem): Boolean {
return webvDescription.onContextItemSelected(item) return shownoteView.onContextItemSelected(item)
} }
@UnstableApi private fun load() { @UnstableApi private fun load() {
@ -168,7 +169,7 @@ class PlayerDetailsFragment : Fragment() {
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ data: String? -> .subscribe({ data: String? ->
webvDescription.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html", "utf-8", "about:blank") shownoteView.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html", "utf-8", "about:blank")
Log.d(TAG, "Webview loaded") Log.d(TAG, "Webview loaded")
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }) }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
loadMediaInfo() loadMediaInfo()
@ -198,20 +199,20 @@ class PlayerDetailsFragment : Fragment() {
val htmlSource = fetchHtmlSource(url) val htmlSource = fetchHtmlSource(url)
val readability4J = Readability4J(item!!.link!!, htmlSource) val readability4J = Readability4J(item!!.link!!, htmlSource)
val article = readability4J.parse() val article = readability4J.parse()
val readerhtml = article.contentWithDocumentsCharsetOrUtf8 readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (readerhtml != null) { if (!readerhtml.isNullOrEmpty()) {
val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml, 0) val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml!!, 0)
homeText = shownotesCleaner.processShownotes() homeText = shownotesCleaner.processShownotes()
} }
} }
} }
if (!homeText.isNullOrEmpty()) if (!homeText.isNullOrEmpty())
binding.webview.loadDataWithBaseURL("https://127.0.0.1", homeText!!, "text/html", "UTF-8", null) shownoteView.loadDataWithBaseURL("https://127.0.0.1", homeText!!, "text/html", "UTF-8", null)
else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
} else { } else {
val shownotesCleaner = ShownotesCleaner(requireContext(), item?.description ?: "", media?.getDuration()?:0) val shownotesCleaner = ShownotesCleaner(requireContext(), item?.description ?: "", media?.getDuration()?:0)
cleanedNotes = shownotesCleaner.processShownotes() cleanedNotes = shownotesCleaner.processShownotes()
if (!cleanedNotes.isNullOrEmpty()) binding.webview.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null) if (!cleanedNotes.isNullOrEmpty()) shownoteView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null)
else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
} }
} }

View File

@ -17,8 +17,7 @@ object IntentUtils {
*/ */
@JvmStatic @JvmStatic
fun isCallable(context: Context, intent: Intent?): Boolean { fun isCallable(context: Context, intent: Intent?): Boolean {
val list = context.packageManager.queryIntentActivities(intent!!, val list = context.packageManager.queryIntentActivities(intent!!, PackageManager.MATCH_DEFAULT_ONLY)
PackageManager.MATCH_DEFAULT_ONLY)
for (info in list) { for (info in list) {
if (info.activityInfo.exported) return true if (info.activityInfo.exported) return true
} }
@ -32,6 +31,7 @@ object IntentUtils {
@JvmStatic @JvmStatic
fun openInBrowser(context: Context, url: String) { fun openInBrowser(context: Context, url: String) {
Log.d(TAG, "url: $url")
try { try {
val myIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val myIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) myIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)

View File

@ -3,6 +3,7 @@ package ac.mdiq.podcini.util.event.playback
class PlaybackServiceEvent(@JvmField val action: Action) { class PlaybackServiceEvent(@JvmField val action: Action) {
enum class Action { enum class Action {
SERVICE_STARTED, SERVICE_STARTED,
SERVICE_SHUT_DOWN SERVICE_SHUT_DOWN,
// SERVICE_RESTARTED
} }
} }

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.62,7.01V15.99L7.5,15.09"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="?attr/action_icon_color"
android:strokeLineCap="round"/>
<path
android:pathData="M16.5,7L13.35,7.45V12.4L16.5,11.95V15.1L12.9,16"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="?attr/action_icon_color"
android:strokeLineCap="round"/>
<path
android:pathData="M3.32,4.91L3.12,3.11C3.05,2.52 3.52,2 4.11,2H19.88C20.48,2 20.94,2.52 20.87,3.11L19.07,19.33C19.03,19.73 18.74,20.07 18.35,20.18L12.27,21.92C12.09,21.97 11.9,21.97 11.72,21.92L5.64,20.18C5.25,20.07 4.97,19.73 4.92,19.33L3.82,9.41"
android:strokeLineJoin="round"
android:strokeWidth="1.5"
android:fillColor="#00000000"
android:strokeColor="?attr/action_icon_color"
android:strokeLineCap="round"/>
</vector>

View File

@ -10,6 +10,13 @@
custom:showAsAction="always"> custom:showAsAction="always">
</item> </item>
<item
android:id="@+id/switchJS"
android:icon="@drawable/javascript_icon_245402"
android:title="@string/javasript_label"
custom:showAsAction="always">
</item>
<item <item
android:id="@+id/switch_home" android:id="@+id/switch_home"
android:icon="@drawable/baseline_home_24" android:icon="@drawable/baseline_home_24"
@ -17,5 +24,10 @@
custom:showAsAction="always"> custom:showAsAction="always">
</item> </item>
<item
android:id="@+id/share_notes"
android:title="@string/share_notes_label"
custom:showAsAction="never">
</item>
</menu> </menu>

View File

@ -59,6 +59,8 @@
<string name="statistics_counting_total">Played in total</string> <string name="statistics_counting_total">Played in total</string>
<string name="web_content_not_available">Web content is not available</string> <string name="web_content_not_available">Web content is not available</string>
<string name="language_not_supported_by_tts">The language is not supported by TTS</string>
<string name="tts_init_failed">TTS init failed</string>
<string name="notification_permission_text">Since Android 13, top level notification is needed for normal refresh and playback. You may disallow notifications of sub-catergories at your wish.</string> <string name="notification_permission_text">Since Android 13, top level notification is needed for normal refresh and playback. You may disallow notifications of sub-catergories at your wish.</string>
<string name="notification_permission_denied">You denied the permission.</string> <string name="notification_permission_denied">You denied the permission.</string>
@ -224,6 +226,8 @@
<item quantity="other">%d downloaded episodes deleted.</item> <item quantity="other">%d downloaded episodes deleted.</item>
</plurals> </plurals>
<string name="javasript_label">JavaScript</string>
<string name="no_action_label">No action</string> <string name="no_action_label">No action</string>
<string name="removed_inbox_label">Removed from inbox</string> <string name="removed_inbox_label">Removed from inbox</string>

View File

@ -310,3 +310,12 @@
* when global auto download setting is enabled, no existing feed is automatically included for auto download * when global auto download setting is enabled, no existing feed is automatically included for auto download
* when subscribing a new feed, there an option for auto download * when subscribing a new feed, there an option for auto download
* new episode of a feed is auto downloaded at a feed refresh only when both global and feed settings for auto download are enabled * new episode of a feed is auto downloaded at a feed refresh only when both global and feed settings for auto download are enabled
## 4.9.5
* added action bar option in episode home view to switch on/off JavaScript
* added share notes menu item in reader mode of episode home view
* TTS speed uses playback speed of the feed or 1.0
* on player detailed view, if showing episode home reader content, then "share notes" shares the reader content
* fixed bug of not re-playing a finished episode
* fixed (possibly) bug of marking multiple items played when one is finished playing

View File

@ -0,0 +1,9 @@
Version 4.9.5 brings several changes:
* added action bar option in episode home view to switch on/off JavaScript
* added share notes menu item in reader mode of episode home view
* TTS speed uses playback speed of the feed or 1.0
* on player detailed view, if showing episode home reader content, then "share notes" shares the reader content
* fixed bug of not re-playing a finished episode
* fixed (possibly) bug of marking multiple items played when one is finished playing