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):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020135
versionName "4.9.4"
versionCode 3020136
versionName "4.9.5"
def commit = ""
try {

View File

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

View File

@ -14,8 +14,7 @@ import android.util.Log
import android.view.SurfaceHolder
import androidx.core.util.Consumer
import androidx.media3.common.*
import androidx.media3.common.Player.DiscontinuityReason
import androidx.media3.common.Player.PositionInfo
import androidx.media3.common.Player.*
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
@ -65,11 +64,14 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
private fun createPlayer() {
if (exoPlayer == null) createStaticPlayer(context)
exoPlayer?.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: @Player.State Int) {
when {
audioCompletionListener != null && playbackState == Player.STATE_ENDED -> audioCompletionListener?.run()
playbackState == Player.STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
exoPlayer?.addListener(object : Listener {
override fun onPlaybackStateChanged(playbackState: @State Int) {
when (playbackState) {
STATE_ENDED -> {
exoPlayer?.seekTo(C.TIME_UNSET)
if (audioCompletionListener != null) audioCompletionListener?.run()
}
STATE_BUFFERING -> bufferingUpdateListener?.accept(BUFFERING_STARTED)
else -> bufferingUpdateListener?.accept(BUFFERING_ENDED)
}
}
@ -88,7 +90,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
}
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) {
@ -112,7 +114,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
}
val isPlaying: Boolean
get() = exoPlayer!!.playWhenReady
get() = exoPlayer!!.isPlaying
// get() = exoPlayer!!.playWhenReady
fun pause() {
exoPlayer?.pause()
@ -205,7 +208,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
}
fun start() {
if (exoPlayer?.playbackState == Player.STATE_IDLE) prepare()
if (exoPlayer?.playbackState == STATE_IDLE || exoPlayer?.playbackState == STATE_ENDED ) prepare()
exoPlayer?.play()
// 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() {
val trackSelections = exoPlayer!!.currentTrackSelections
val availableFormats = formats
Log.d(TAG, "selectedAudioTrack called tracks: ${trackSelections.length} formats: ${availableFormats.size}")
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)
}
return -1

View File

@ -125,7 +125,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
return
} else {
// 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()
// 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
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED,
this.playable)
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, this.playable)
if (prepareImmediately) {
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 (playable != null) {
// TODO this call has no effect!
if (playable!!.getPosition() > 0) seekTo(playable!!.getPosition())
val pos = playable!!.getPosition()
if (pos > 0) seekTo(pos)
if (playable!!.getDuration() <= 0) {
Log.d(TAG, "Setting duration of media")
@ -318,8 +317,10 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
if (t >= getDuration()) {
Log.d(TAG, "Seek reached end of file, skipping to next episode")
// TODO: test
playerWrapper?.seekTo(t)
endPlayback(true, wasSkipped = true, true, toStoppedState = true)
return
// return
}
when (playerStatus) {
@ -367,7 +368,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
*/
override fun getDuration(): Int {
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 (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 {
var retVal = Playable.INVALID_TIME
// TODO: test
if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) {
if (playerWrapper != null) retVal = playerWrapper!!.currentPosition
}
@ -409,7 +412,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
*/
override fun getPlaybackSpeed(): Float {
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
}
return retVal
@ -622,6 +626,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
shouldContinue || toStoppedState -> {
if (nextMedia == null) {
callback.onPlaybackEnded(null, true)
playable = null
ExoPlayerWrapper.exoPlayer?.stop()
stop()
}
val hasNext = nextMedia != null
@ -660,6 +666,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
}
})
mp.setOnErrorListener(Consumer { message: String ->
Log.e(TAG, "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.shouldFavoriteKeepEpisode
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.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.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.SpeedPresetChangedEvent
import ac.mdiq.podcini.util.event.settings.VolumeAdaptionChangedEvent
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.bluetooth.BluetoothA2dp
import android.content.*
import android.content.pm.PackageManager
import android.media.AudioManager
import android.os.*
import android.os.Build.VERSION_CODES
import android.service.quicksettings.TileService
import android.support.v4.media.MediaMetadataCompat
import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.text.TextUtils
import android.util.Log
import android.util.Pair
@ -89,13 +80,17 @@ import android.view.KeyEvent
import android.view.SurfaceHolder
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.app.ActivityCompat
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.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.*
import com.google.common.collect.ImmutableList
import androidx.media3.session.MediaSession
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.ListenableFuture
import io.reactivex.Observable
@ -189,7 +184,6 @@ class PlaybackService : MediaSessionService() {
if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext)
mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!)
.setCallback(MyCallback())
// .setCustomLayout(customMediaNotificationProvider.notificationMediaButtons)
.setCustomLayout(notificationCustomButtons)
.build()
@ -202,7 +196,7 @@ class PlaybackService : MediaSessionService() {
if (mediaPlayer != null) {
media = mediaPlayer!!.getPlayable()
wasPlaying = mediaPlayer!!.playerStatus == PlayerStatus.PLAYING || mediaPlayer!!.playerStatus == PlayerStatus.FALLBACK
mediaPlayer!!.pause(true, false)
mediaPlayer!!.pause(abandonFocus = true, reinit = false)
mediaPlayer!!.shutdown()
}
mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback)
@ -215,7 +209,7 @@ class PlaybackService : MediaSessionService() {
Log.d(TAG, "onTaskRemoved")
val player = mediaSession?.player
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
// otherwise.
stopSelf()
@ -251,7 +245,7 @@ class PlaybackService : MediaSessionService() {
}
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 {
@ -345,9 +339,7 @@ class PlaybackService : MediaSessionService() {
{ obj: Throwable -> obj.printStackTrace() })
}
// private fun createBrowsableMediaItem(
// @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int
// ): MediaBrowserCompat.MediaItem {
// private fun createBrowsableMediaItem(@StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int): MediaItem {
// val uri = Uri.Builder()
// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
// .authority(resources.getResourcePackageName(icon))
@ -355,17 +347,17 @@ class PlaybackService : MediaSessionService() {
// .appendPath(resources.getResourceEntryName(icon))
// .build()
//
// val description = MediaDescriptionCompat.Builder()
// val description = MediaDescription.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)
// return MediaItem(description, MediaItem.FLAG_BROWSABLE)
// }
// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem {
// val builder = MediaDescriptionCompat.Builder()
// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaItem {
// val builder = MediaDescription.Builder()
// .setMediaId("FeedId:" + feed.id)
// .setTitle(feed.title)
// .setDescription(feed.description)
@ -377,13 +369,10 @@ class PlaybackService : MediaSessionService() {
// builder.setMediaUri(Uri.parse(feed.link))
// }
// val description = builder.build()
// return MediaBrowserCompat.MediaItem(description,
// MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
// return MediaItem(description, MediaItem.FLAG_BROWSABLE)
// }
// override fun onLoadChildren(parentId: String,
// result: Result<List<MediaBrowserCompat.MediaItem>>
// ) {
// override fun onLoadChildren(parentId: String, result: Result<List<MediaItem>>) {
// Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId")
// result.detach()
//
@ -475,12 +464,14 @@ class PlaybackService : MediaSessionService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
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 customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
Log.d(TAG, "OnStartCommand $keycode $customAction $hardwareButton $playable")
if (keycode == -1 && playable == null && customAction == null) {
Log.e(TAG, "PlaybackService was started with no arguments")
return START_NOT_STICKY
@ -556,37 +547,38 @@ class PlaybackService : MediaSessionService() {
return
}
// val intentAllowThisTime = Intent(originalIntent)
// 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.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)
val intentAllowThisTime = Intent(originalIntent)
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.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)
// 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.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)
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.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)
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,
// 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 notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.notify(5566, builder.build())
}
/**
@ -670,7 +662,8 @@ class PlaybackService : MediaSessionService() {
return false
}
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
}
else -> {
@ -705,7 +698,7 @@ class PlaybackService : MediaSessionService() {
val localFeed = URLUtil.isContentUrl(playable.getStreamUrl())
val stream = !playable.localFileAvailable() || localFeed
if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) {
// displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
writeNoMediaPlaying()
return
}
@ -716,8 +709,9 @@ class PlaybackService : MediaSessionService() {
mediaPlayer?.playMediaObject(playable, stream, true, true)
recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(playable)
// updateNotificationAndMediaSession(playable)
addPlayableToQueue(playable)
// EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_RESTARTED))
}
/**
@ -732,7 +726,7 @@ class PlaybackService : MediaSessionService() {
fun notifyVideoSurfaceAbandoned() {
mediaPlayer?.pause(true, false)
mediaPlayer?.resetVideoSurface()
updateNotificationAndMediaSession(playable)
// updateNotificationAndMediaSession(playable)
}
private val taskManagerCallback: PSTMCallback = object : PSTMCallback {
@ -755,19 +749,19 @@ class PlaybackService : MediaSessionService() {
override fun statusChanged(newInfo: PSMPInfo?) {
currentMediaType = mediaPlayer?.getCurrentMediaType() ?: MediaType.UNKNOWN
Log.d(TAG, "statusChanged called")
updateMediaSession(newInfo?.playerStatus)
// updateMediaSession(newInfo?.playerStatus)
if (newInfo != null) {
when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
updateNotificationAndMediaSession(newInfo.playable)
// updateNotificationAndMediaSession(newInfo.playable)
}
PlayerStatus.PREPARED -> {
if (mediaPlayer != null) writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus, currentitem)
taskManager.startChapterLoader(newInfo.playable!!)
}
PlayerStatus.PAUSED -> {
updateNotificationAndMediaSession(newInfo.playable)
// updateNotificationAndMediaSession(newInfo.playable)
cancelPositionObserver()
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
}
@ -776,7 +770,7 @@ class PlaybackService : MediaSessionService() {
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(newInfo.playable)
// updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver()
// set sleep timer if auto-enabled
var autoEnableByTime = true
@ -815,7 +809,7 @@ class PlaybackService : MediaSessionService() {
override fun onMediaChanged(reloadUI: Boolean) {
Log.d(TAG, "reloadUI callback reached")
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) {
@ -876,7 +870,7 @@ class PlaybackService : MediaSessionService() {
// Playable is being streamed and does not have a duration specified in the feed
playable.setDuration(mediaPlayer!!.getDuration())
DBWriter.setFeedMedia(playable as FeedMedia?)
updateNotificationAndMediaSession(playable)
// updateNotificationAndMediaSession(playable)
}
}
}
@ -924,12 +918,12 @@ class PlaybackService : MediaSessionService() {
if (!isFollowQueue) {
Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.")
writeMediaPlaying(nextItem.media, PlayerStatus.STOPPED, currentitem)
updateNotificationAndMediaSession(nextItem.media)
// updateNotificationAndMediaSession(nextItem.media)
return null
}
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()
return null
}
@ -1081,62 +1075,62 @@ class PlaybackService : MediaSessionService() {
* @param playerStatus the current [PlayerStatus]
*/
private fun updateMediaSession(playerStatus: PlayerStatus?) {
val sessionState = PlaybackStateCompat.Builder()
val state = if (playerStatus != null) {
when (playerStatus) {
PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING
PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED
PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED
PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING
PlayerStatus.PREPARING, PlayerStatus.INITIALIZING -> PlaybackStateCompat.STATE_CONNECTING
PlayerStatus.ERROR -> PlaybackStateCompat.STATE_ERROR
PlayerStatus.INITIALIZED, PlayerStatus.INDETERMINATE -> PlaybackStateCompat.STATE_NONE
}
} else {
PlaybackStateCompat.STATE_NONE
}
sessionState.setState(state, currentPosition.toLong(), currentPlaybackSpeed)
val capabilities = (PlaybackStateCompat.ACTION_PLAY
or PlaybackStateCompat.ACTION_PLAY_PAUSE
or PlaybackStateCompat.ACTION_REWIND
or PlaybackStateCompat.ACTION_PAUSE
or PlaybackStateCompat.ACTION_FAST_FORWARD
or PlaybackStateCompat.ACTION_SEEK_TO
or PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED)
sessionState.setActions(capabilities)
// val sessionState = PlaybackStateCompat.Builder()
// val state = if (playerStatus != null) {
// when (playerStatus) {
// PlayerStatus.PLAYING -> PlaybackStateCompat.STATE_PLAYING
// PlayerStatus.FALLBACK -> PlaybackStateCompat.STATE_PLAYING
// PlayerStatus.PREPARED, PlayerStatus.PAUSED -> PlaybackStateCompat.STATE_PAUSED
// PlayerStatus.STOPPED -> PlaybackStateCompat.STATE_STOPPED
// PlayerStatus.SEEKING -> PlaybackStateCompat.STATE_FAST_FORWARDING
// PlayerStatus.PREPARING, PlayerStatus.INITIALIZING -> PlaybackStateCompat.STATE_CONNECTING
// PlayerStatus.ERROR -> PlaybackStateCompat.STATE_ERROR
// PlayerStatus.INITIALIZED, PlayerStatus.INDETERMINATE -> PlaybackStateCompat.STATE_NONE
// }
// } else {
// PlaybackStateCompat.STATE_NONE
// }
//
// sessionState.setState(state, currentPosition.toLong(), currentPlaybackSpeed)
// val capabilities = (PlaybackStateCompat.ACTION_PLAY
// or PlaybackStateCompat.ACTION_PLAY_PAUSE
// or PlaybackStateCompat.ACTION_REWIND
// or PlaybackStateCompat.ACTION_PAUSE
// or PlaybackStateCompat.ACTION_FAST_FORWARD
// or PlaybackStateCompat.ACTION_SEEK_TO
// or PlaybackStateCompat.ACTION_SET_PLAYBACK_SPEED)
//
// sessionState.setActions(capabilities)
// 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)
WearMediaSession.addWearExtrasToAction(rewindBuilder)
sessionState.addCustomAction(rewindBuilder.build())
// 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)
WearMediaSession.addWearExtrasToAction(fastForwardBuilder)
sessionState.addCustomAction(fastForwardBuilder.build())
// 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())
// if (showPlaybackSpeedOnFullNotification())
// 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())
}
// 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())
// }
if (showSkipOnFullNotification())
sessionState.addCustomAction(PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_SKIP_TO_NEXT,
getString(R.string.skip_episode_label), R.drawable.ic_notification_skip).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())
if (mediaSession != null) {
WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
// mediaSession!!.setPlaybackState(sessionState.build())
}
// if (mediaSession != null) {
// WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
//// mediaSession!!.setPlaybackState(sessionState.build())
// }
}
private fun updateNotificationAndMediaSession(p: Playable?) {
@ -1147,25 +1141,27 @@ class PlaybackService : MediaSessionService() {
private fun updateMediaSessionMetadata(p: Playable?) {
if (p == null || mediaSession == null) return
// TODO: what's this?
// val builder = MediaMetadataCompat.Builder()
// builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle())
// builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong())
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle())
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle())
// TODO: how to set meta data
// val builder = MediaMetadata.Builder()
// builder.setArtist(p.getFeedTitle())
// builder.setTitle(p.getEpisodeTitle())
// builder.setAlbumArtist(p.getFeedTitle())
//// builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong())
// builder.setDisplayTitle(p.getEpisodeTitle())
// builder.setSubtitle(p.getFeedTitle())
// TODO: what's this?
// mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
// getPlayerActivityIntent(this), FLAG_IMMUTABLE))
// try {
// mediaSession!!.setMetadata(builder.build())
//// mediaSession!!.setMetadata(builder.build())
// val mediaItem = MediaItem.Builder().setMediaMetadata(builder.build()).build()
// } catch (e: OutOfMemoryError) {
// Log.e(TAG, "Setting media session metadata", e)
// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null)
// mediaSession!!.setMetadata(builder.build())
// builder.setArtworkUri(null)
//// 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.
*/
// TODO: not needed?
@Synchronized
private fun setupNotification(playable: Playable?) {
Log.d(TAG, "setupNotification")
playableIconLoaderThread?.interrupt()
if (playable == null || mediaPlayer == null) {
Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer")
return
}
}
// @Synchronized
// private fun setupNotification(playable: Playable?) {
// Log.d(TAG, "setupNotification")
// playableIconLoaderThread?.interrupt()
//
// if (playable == null || mediaPlayer == null) {
// Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer")
// return
// }
// }
/**
* Persists the current position and last played time of the media file.
@ -1362,14 +1358,12 @@ class PlaybackService : MediaSessionService() {
@Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused")
fun speedPresetChanged(event: SpeedPresetChangedEvent) {
fun onSpeedPresetChanged(event: SpeedPresetChangedEvent) {
val item = (playable as? FeedMedia)?.item ?: currentitem
// if (playable is FeedMedia) {
if (item?.feed?.id == event.feedId) {
if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) setSpeed(getPlaybackSpeed(playable!!.getMediaType()))
else setSpeed(event.speed)
}
// }
}
@Subscribe(threadMode = ThreadMode.MAIN)
@ -1394,7 +1388,6 @@ class PlaybackService : MediaSessionService() {
currentitem = event.item
}
fun resume() {
mediaPlayer?.resume()
taskManager.restartSleepTimer()

View File

@ -14,9 +14,9 @@ class VisitWebsiteActionButton(item: FeedItem) : ItemActionButton(item) {
return R.drawable.ic_web
}
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
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.SleepTimerDialog
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.PlayButton
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
@ -65,8 +63,6 @@ import io.reactivex.MaybeEmitter
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.runBlocking
import net.dankito.readability4j.Readability4J
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -75,6 +71,7 @@ import java.text.NumberFormat
import kotlin.math.max
import kotlin.math.min
/**
* Shows the audio player.
*/
@ -192,7 +189,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
val theMedia = controller?.getMedia() ?: return
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")
disposable?.dispose()
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
@ -297,6 +294,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
currentitem = event.item
if (currentMedia?.getIdentifier() == null || currentitem!!.media!!.getIdentifier() != currentMedia?.getIdentifier())
itemDescFrag.setItem(currentitem!!)
(activity as MainActivity).setPlayerVisible(true)
}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
@ -404,8 +402,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
return true
}
R.id.share_notes -> {
if (feedItem == null) return false
val notes = feedItem.description
val notes = if (itemDescFrag.showHomeText) itemDescFrag.readerhtml else feedItem?.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()
@ -649,6 +646,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
when (event.action) {
PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> (activity as MainActivity).setPlayerVisible(false)
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.storage.model.feed.FeedItem
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
import android.speech.tts.TextToSpeech
import android.os.Build
import android.os.Bundle
import android.speech.tts.TextToSpeech
import android.text.Html
import android.util.Log
import android.view.*
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.widget.Toolbar
@ -37,22 +39,18 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
// 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 var disposable: Disposable? = null
// private var readerhtml: String? = null
private var readerhtml: 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)
}
private var jsEnabled = false
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
@ -66,7 +64,22 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
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()
return binding.root
@ -80,34 +93,31 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
override fun onInit(status: Int) {
if (status == TextToSpeech.SUCCESS) {
// TTS initialization successful
Log.i(TAG, "TTS init success with Locale: ${currentItem?.feed?.language}")
if (currentItem?.feed?.language != null) {
val result = tts.setLanguage(Locale(currentItem!!.feed!!.language!!))
// val result = tts.setLanguage(Locale.UK)
val result = tts?.setLanguage(Locale(currentItem!!.feed!!.language!!))
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
Log.w(TAG, "TTS language not supported ${currentItem?.feed?.language}")
Toast.makeText(context, R.string.language_not_supported_by_tts, Toast.LENGTH_LONG).show()
}
ttsSpeed = currentItem?.feed?.preferences?.feedPlaybackSpeed ?: 1.0f
tts?.setSpeechRate(ttsSpeed)
}
} else {
// TTS initialization failed
// Handle the error or fallback to default behavior
Log.w(TAG, "TTS init failed")
Toast.makeText(context, R.string.tts_init_failed, Toast.LENGTH_LONG).show()
}
}
private fun showContent() {
if (readMode) {
var readerhtml: String? = null
if (cleanedNotes == null) {
private fun showReaderContent() {
if (!currentItem?.link.isNullOrEmpty()) {
if (cleanedNotes == null) {
runBlocking {
val url = currentItem!!.link!!
val htmlSource = fetchHtmlSource(url)
val readability4J = Readability4J(currentItem?.link!!, htmlSource)
val article = readability4J.parse()
textContent = article.textContent
readerText = article.textContent
// Log.d(TAG, "readability4J: ${article.textContent}")
readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (!readerhtml.isNullOrEmpty()) {
@ -116,19 +126,28 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
}
}
}
if (!cleanedNotes.isNullOrEmpty()) {
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.webView.visibility = View.GONE
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
} else {
if (!currentItem?.link.isNullOrEmpty()) {
binding.webView.loadUrl(currentItem!!.link!!)
binding.readerView.visibility = View.GONE
binding.webView.visibility = View.VISIBLE
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
}
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.visibility = View.VISIBLE
binding.webView.visibility = View.GONE
} else Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show()
}
private fun showWebContent() {
if (!currentItem?.link.isNullOrEmpty()) {
binding.webView.settings.javaScriptEnabled = jsEnabled
Log.d(TAG, "currentItem!!.link ${currentItem!!.link}")
binding.webView.loadUrl(currentItem!!.link!!)
binding.readerView.visibility = View.GONE
binding.webView.visibility = View.VISIBLE
} 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")
@ -138,6 +157,7 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
if (readMode) {
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 {
@ -147,31 +167,35 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
switchMode()
return true
}
R.id.switchJS -> {
Log.d(TAG, "switchJS selected")
jsEnabled = !jsEnabled
showWebContent()
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)
Log.d(TAG, "text_speech selected: $readerText")
if (tts != null) {
if (tts!!.isSpeaking) tts?.stop()
if (!ttsPlaying) {
ttsPlaying = true
if (!readerText.isNullOrEmpty()) {
tts?.setSpeechRate(ttsSpeed)
while (startIndex < readerText!!.length) {
val endIndex = minOf(startIndex + maxChunkLength, readerText!!.length)
val chunk = readerText!!.substring(startIndex, endIndex)
tts?.speak(chunk, TextToSpeech.QUEUE_ADD, null, null)
startIndex += maxChunkLength
}
}
}
} else ttsPlaying = false
updateAppearance()
} else ttsPlaying = false
updateAppearance()
}
return true
}
R.id.share_notes -> {
if (currentItem == null) return false
val notes = currentItem!!.description
val notes = readerhtml
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()
@ -201,7 +225,7 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
Log.d(TAG, "onDestroyView")
_binding = null
disposable?.dispose()
tts.shutdown()
tts?.shutdown()
}
@UnstableApi private fun updateAppearance() {
@ -214,12 +238,14 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
}
companion object {
private const val TAG = "EpisodeWebviewFragment"
private const val TAG = "EpisodeHomeFragment"
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 currentItem: FeedItem? = null
var currentItem: FeedItem? = null
@JvmStatic
fun newInstance(item: FeedItem): EpisodeHomeFragment {
@ -229,7 +255,9 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
if (item.itemIdentifier != currentItem?.itemIdentifier) {
currentItem = item
cleanedNotes = null
textContent = null
readerText = null
} else {
currentItem?.feed = item.feed
}
// args.putSerializable(ARG_FEEDITEM, item)
// 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.ShownotesCleaner
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.util.Converter
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.PlayerStatusEvent
import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent
import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent
import android.os.Build
import android.os.Bundle
import android.text.Html
@ -71,6 +73,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var _binding: EpisodeInfoFragmentBinding? = null
private val binding get() = _binding!!
private var homeFragment: EpisodeHomeFragment? = null
private var itemsLoaded = false
private var item: FeedItem? = null
private var webviewData: String? = null
@ -141,7 +145,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
noMediaLabel = binding.noMediaLabel
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 {
@ -398,7 +403,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@UnstableApi private fun load() {
disposable?.dispose()
if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE
disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }

View File

@ -64,7 +64,7 @@ import org.greenrobot.eventbus.ThreadMode
*/
@UnstableApi
class PlayerDetailsFragment : Fragment() {
private lateinit var webvDescription: ShownotesWebView
private lateinit var shownoteView: ShownotesWebView
private var _binding: PlayerDetailsFragmentBinding? = null
private val binding get() = _binding!!
@ -79,8 +79,9 @@ class PlayerDetailsFragment : Fragment() {
private var webViewLoader: Disposable? = null
private var controller: PlaybackController? = null
private var showHomeText = false
var homeText: String? = null
internal var showHomeText = false
internal var homeText: String? = null
internal var readerhtml: String? = null
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Log.d(TAG, "fragment onCreateView")
@ -96,20 +97,20 @@ class PlayerDetailsFragment : Fragment() {
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
Log.d(TAG, "fragment onCreateView")
webvDescription = binding.webview
webvDescription.setTimecodeSelectedListener { time: Int? -> controller?.seekTo(time!!) }
webvDescription.setPageFinishedListener {
shownoteView = binding.webview
shownoteView.setTimecodeSelectedListener { time: Int? -> controller?.seekTo(time!!) }
shownoteView.setPageFinishedListener {
// 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 {
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)
}
})
registerForContextMenu(webvDescription)
registerForContextMenu(shownoteView)
controller = object : PlaybackController(requireActivity()) {
override fun loadMediaInfo() {
load()
@ -125,12 +126,12 @@ class PlayerDetailsFragment : Fragment() {
controller?.release()
controller = null
Log.d(TAG, "Fragment destroyed")
webvDescription.removeAllViews()
webvDescription.destroy()
shownoteView.removeAllViews()
shownoteView.destroy()
}
override fun onContextItemSelected(item: MenuItem): Boolean {
return webvDescription.onContextItemSelected(item)
return shownoteView.onContextItemSelected(item)
}
@UnstableApi private fun load() {
@ -168,7 +169,7 @@ class PlayerDetailsFragment : Fragment() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.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")
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
loadMediaInfo()
@ -198,20 +199,20 @@ class PlayerDetailsFragment : Fragment() {
val htmlSource = fetchHtmlSource(url)
val readability4J = Readability4J(item!!.link!!, htmlSource)
val article = readability4J.parse()
val readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (readerhtml != null) {
val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml, 0)
readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (!readerhtml.isNullOrEmpty()) {
val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml!!, 0)
homeText = shownotesCleaner.processShownotes()
}
}
}
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 {
val shownotesCleaner = ShownotesCleaner(requireContext(), item?.description ?: "", media?.getDuration()?:0)
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()
}
}

View File

@ -17,8 +17,7 @@ object IntentUtils {
*/
@JvmStatic
fun isCallable(context: Context, intent: Intent?): Boolean {
val list = context.packageManager.queryIntentActivities(intent!!,
PackageManager.MATCH_DEFAULT_ONLY)
val list = context.packageManager.queryIntentActivities(intent!!, PackageManager.MATCH_DEFAULT_ONLY)
for (info in list) {
if (info.activityInfo.exported) return true
}
@ -32,6 +31,7 @@ object IntentUtils {
@JvmStatic
fun openInBrowser(context: Context, url: String) {
Log.d(TAG, "url: $url")
try {
val myIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
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) {
enum class Action {
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">
</item>
<item
android:id="@+id/switchJS"
android:icon="@drawable/javascript_icon_245402"
android:title="@string/javasript_label"
custom:showAsAction="always">
</item>
<item
android:id="@+id/switch_home"
android:icon="@drawable/baseline_home_24"
@ -17,5 +24,10 @@
custom:showAsAction="always">
</item>
<item
android:id="@+id/share_notes"
android:title="@string/share_notes_label"
custom:showAsAction="never">
</item>
</menu>

View File

@ -59,6 +59,8 @@
<string name="statistics_counting_total">Played in total</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_denied">You denied the permission.</string>
@ -224,6 +226,8 @@
<item quantity="other">%d downloaded episodes deleted.</item>
</plurals>
<string name="javasript_label">JavaScript</string>
<string name="no_action_label">No action</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 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
## 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