From 45e5fbef8869f18bc4f389b3b7f0eeadbe43e4ce Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Fri, 26 Apr 2024 22:36:37 +0100 Subject: [PATCH] 4.9.5 commit --- app/build.gradle | 4 +- .../podcini/playback/PlaybackController.kt | 8 +- .../playback/service/ExoPlayerWrapper.kt | 26 +- .../podcini/playback/service/LocalPSMP.kt | 23 +- .../playback/service/PlaybackService.kt | 279 +++++++++--------- .../actionbutton/VisitWebsiteActionButton.kt | 4 +- .../ui/fragment/AudioPlayerFragment.kt | 12 +- .../ui/fragment/EpisodeHomeFragment.kt | 152 ++++++---- .../ui/fragment/EpisodeInfoFragment.kt | 8 +- .../ui/fragment/PlayerDetailsFragment.kt | 37 +-- .../java/ac/mdiq/podcini/util/IntentUtils.kt | 4 +- .../event/playback/PlaybackServiceEvent.kt | 3 +- .../res/drawable/javascript_icon_245402.xml | 27 ++ app/src/main/res/menu/episode_home.xml | 12 + app/src/main/res/values/strings.xml | 4 + changelog.md | 11 +- .../android/en-US/changelogs/3020136.txt | 9 + 17 files changed, 361 insertions(+), 262 deletions(-) create mode 100644 app/src/main/res/drawable/javascript_icon_245402.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020136.txt diff --git a/app/build.gradle b/app/build.gradle index ceca22a3..407a02b7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt index 5f004db6..9fc52bca 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -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) { diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt index f9b573ae..539e4e4e 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/ExoPlayerWrapper.kt @@ -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 diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt index 8aec591c..1bd076f7 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/LocalPSMP.kt @@ -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)) }) } diff --git a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt index 24449236..4dafb920 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -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> -// ) { +// override fun onLoadChildren(parentId: String, result: Result>) { // 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(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() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt index 79bbd896..5a0fede1 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/actions/actionbutton/VisitWebsiteActionButton.kt @@ -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 } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt index e961ead2..5ec20112 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/AudioPlayerFragment.kt @@ -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 { emitter: MaybeEmitter -> @@ -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) } } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt index 18547460..838bd16d 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeHomeFragment.kt @@ -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 diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt index c0d42420..61ed2777 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/EpisodeInfoFragment.kt @@ -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 { this.loadInBackground() } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt index 1a8270c2..82bf91b8 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/PlayerDetailsFragment.kt @@ -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() } } diff --git a/app/src/main/java/ac/mdiq/podcini/util/IntentUtils.kt b/app/src/main/java/ac/mdiq/podcini/util/IntentUtils.kt index 825a50de..c8516cb7 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/IntentUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/IntentUtils.kt @@ -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) diff --git a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt b/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt index ebf94e9c..8fcea8eb 100644 --- a/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt +++ b/app/src/main/java/ac/mdiq/podcini/util/event/playback/PlaybackServiceEvent.kt @@ -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 } } diff --git a/app/src/main/res/drawable/javascript_icon_245402.xml b/app/src/main/res/drawable/javascript_icon_245402.xml new file mode 100644 index 00000000..005effa0 --- /dev/null +++ b/app/src/main/res/drawable/javascript_icon_245402.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/app/src/main/res/menu/episode_home.xml b/app/src/main/res/menu/episode_home.xml index 9ba29f01..a666e5c3 100644 --- a/app/src/main/res/menu/episode_home.xml +++ b/app/src/main/res/menu/episode_home.xml @@ -10,6 +10,13 @@ custom:showAsAction="always"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85ff81a2..7dd3e5f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,6 +59,8 @@ Played in total Web content is not available + The language is not supported by TTS + TTS init failed Since Android 13, top level notification is needed for normal refresh and playback. You may disallow notifications of sub-catergories at your wish. You denied the permission. @@ -224,6 +226,8 @@ %d downloaded episodes deleted. + JavaScript + No action Removed from inbox diff --git a/changelog.md b/changelog.md index 6ede50ed..196490fc 100644 --- a/changelog.md +++ b/changelog.md @@ -309,4 +309,13 @@ * enabled the function for auto downloading of feeds * 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 \ No newline at end of file + * 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 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020136.txt b/fastlane/metadata/android/en-US/changelogs/3020136.txt new file mode 100644 index 00000000..8a3cde28 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020136.txt @@ -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