4.9.5 commit
This commit is contained in:
parent
3566f60b3e
commit
45e5fbef88
|
@ -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 {
|
||||
|
|
|
@ -165,9 +165,11 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
Log.d(TAG, "Received statusUpdate Intent.")
|
||||
if (playbackService != null) {
|
||||
val info = playbackService!!.pSMPInfo
|
||||
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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
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()) {
|
||||
if (tts == null) tts = TextToSpeech(requireContext(), this)
|
||||
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 {
|
||||
}
|
||||
|
||||
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()
|
||||
Log.d(TAG, "text_speech selected: $readerText")
|
||||
if (tts != null) {
|
||||
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)
|
||||
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()
|
||||
}
|
||||
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
|
||||
|
|
|
@ -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() }
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue