From 40ec9deb06d069eebcabee59958cb4e6e2939bd4 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Wed, 3 Apr 2024 08:33:11 +0000 Subject: [PATCH] 4.5.2 commit --- README.md | 8 +- app/build.gradle | 4 +- .../podcini/feed/util/PlaybackSpeedUtils.kt | 1 + .../podcini/playback/PlaybackController.kt | 8 +- .../service/playback/PlaybackService.kt | 30 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 6 +- .../podcini/ui/dialog/VariableSpeedDialog.kt | 71 +- .../ui/fragment/AudioPlayerFragment.kt | 627 ++++++++++++------ .../ui/fragment/ExternalPlayerFragment.kt | 387 ----------- .../ui/fragment/FeedSettingsFragment.kt | 10 +- .../mdiq/podcini/ui/fragment/QueueFragment.kt | 17 +- .../PlaybackPreferencesFragment.kt | 2 +- .../main/res/layout/audioplayer_fragment.xml | 230 +------ ...gment.xml => internal_player_fragment.xml} | 8 +- .../main/res/layout/speed_select_dialog.xml | 31 +- app/src/main/res/values/strings.xml | 3 + changelog.md | 8 +- .../android/en-US/changelogs/3020122.txt | 6 + 18 files changed, 585 insertions(+), 872 deletions(-) delete mode 100644 app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt rename app/src/main/res/layout/{external_player_fragment.xml => internal_player_fragment.xml} (97%) create mode 100644 fastlane/metadata/android/en-US/changelogs/3020122.txt diff --git a/README.md b/README.md index a8cc3634..321829c5 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Other notable features and changes include: * A more convenient player control displayed on all pages * A revamped and more efficient expanded player view showing episode description on the front +* External player class is merged into the player * New and efficient ways of click and long-click operations on lists: * click on title area opens the podcast/episode * long-press on title area automatically enters in selection mode @@ -31,11 +32,8 @@ Other notable features and changes include: * Left and right swipe actions on lists now have telltales and can be configured on the spot * Played episodes have clearer markings * Sort dialog no longer dims the main view -* Play speed setting has been straightened up, three places to set the play speed: - * global setting at the preference - * setting for a feed: either use global or customized - * setting at the player: set for current playing and save for global - * customized feed setting takes precedence when playing an episode +* Play speed setting has been straightened up, three speed can be set separately or combined: current audio, podcast, and global + * customized podcast speed takes precedence when playing an episode * Added preference "Fast Forward Speed" under "Playback" in settings with default value of 0.0, dialog allows setting a float number (capped between 0.0 and 10.0) * The "Skip to next episode" button on the player * long-press moves to the next episode diff --git a/app/build.gradle b/app/build.gradle index 0584eb59..7fa8bdac 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -149,8 +149,8 @@ android { // Version code schema (not used): // "1.2.3-beta4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 3020121 - versionName "4.5.1" + versionCode 3020122 + versionName "4.5.2" def commit = "" try { diff --git a/app/src/main/java/ac/mdiq/podcini/feed/util/PlaybackSpeedUtils.kt b/app/src/main/java/ac/mdiq/podcini/feed/util/PlaybackSpeedUtils.kt index 59a0ca66..1913d49d 100644 --- a/app/src/main/java/ac/mdiq/podcini/feed/util/PlaybackSpeedUtils.kt +++ b/app/src/main/java/ac/mdiq/podcini/feed/util/PlaybackSpeedUtils.kt @@ -33,6 +33,7 @@ object PlaybackSpeedUtils { val feed = item.feed if (feed?.preferences != null) { playbackSpeed = feed.preferences!!.feedPlaybackSpeed + Log.d(TAG, "using feed speed $playbackSpeed") } else { Log.d(TAG, "Can not get feed specific playback speed: $feed") } 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 500561d1..96e7ba57 100644 --- a/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt +++ b/app/src/main/java/ac/mdiq/podcini/playback/PlaybackController.kt @@ -116,6 +116,10 @@ abstract class PlaybackController(private val activity: FragmentActivity) { } } + fun isPlaybackServiceReady() : Boolean { + return playbackService != null + } + private fun unbind() { try { activity.unbindService(mConnection) @@ -338,9 +342,9 @@ abstract class PlaybackController(private val activity: FragmentActivity) { playbackService?.setVideoSurface(holder) } - fun setPlaybackSpeed(speed: Float) { + fun setPlaybackSpeed(speed: Float, codeArray: Array? = null) { if (playbackService != null) { - playbackService!!.setSpeed(speed) + playbackService!!.setSpeed(speed, codeArray) } else { EventBus.getDefault().post(SpeedChangedEvent(speed)) } diff --git a/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt b/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt index a6798ef2..5df0ba5f 100644 --- a/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt +++ b/app/src/main/java/ac/mdiq/podcini/service/playback/PlaybackService.kt @@ -1617,15 +1617,41 @@ class PlaybackService : MediaBrowserServiceCompat() { val playable: Playable? get() = mediaPlayer?.getPlayable() - fun setSpeed(speed: Float) { + fun setSpeed(speed: Float, codeArray: Array? = null) { isSpeedForward = false isFallbackSpeed = false currentlyPlayingTemporaryPlaybackSpeed = speed + if (currentMediaType == MediaType.VIDEO) { videoPlaybackSpeed = speed } else { - setPlaybackSpeed(speed) + if (codeArray != null && codeArray.size == 3) { + if (codeArray[2] == 1) setPlaybackSpeed(speed) + if (codeArray[1] == 1 && playable is FeedMedia) { + var item = (playable as FeedMedia).item + if (item == null) { + val itemId = (playable as FeedMedia).itemId + item = DBReader.getFeedItem(itemId) + } + if (item != null) { + var feed = item.feed + if (feed == null) { + feed = DBReader.getFeed(item.feedId) + } + if (feed != null) { + val feedPreferences = feed.preferences + if (feedPreferences != null) { + feedPreferences.feedPlaybackSpeed = speed + Log.d(TAG, "setSpeed ${feed.title} $speed") + DBWriter.setFeedPreferences(feedPreferences) + EventBus.getDefault().post( + SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id)) + } + } + } + } + } } mediaPlayer?.setPlaybackParams(speed, isSkipSilence) diff --git a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt index d47116a7..a7eb2989 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/activity/MainActivity.kt @@ -340,7 +340,9 @@ class MainActivity : CastEnabledActivity() { bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom } - fun setPlayerVisible(visible: Boolean) { + fun setPlayerVisible(visible_: Boolean?) { + val visible = if (visible_ != null) visible_ else if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) false else true + bottomSheet.setLocked(!visible) if (visible) { bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility @@ -353,7 +355,7 @@ class MainActivity : CastEnabledActivity() { params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0)) mainView.layoutParams = params - val playerView = findViewById(R.id.playerFragment) + val playerView = findViewById(R.id.playerFragment1) val playerParams = playerView.layoutParams as MarginLayoutParams playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0) playerView.layoutParams = playerParams diff --git a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt index 6a237bb7..111abf67 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/dialog/VariableSpeedDialog.kt @@ -15,6 +15,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.CompoundButton +import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -28,6 +29,7 @@ import java.text.DecimalFormatSymbols import java.util.* open class VariableSpeedDialog : BottomSheetDialogFragment() { + private lateinit var adapter: SpeedSelectionAdapter private lateinit var speedSeekBar: PlaybackSpeedSeekBar private lateinit var addCurrentSpeedChip: Chip @@ -38,6 +40,8 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { private var controller: PlaybackController? = null private val selectedSpeeds: MutableList + private val settingCode: Array = Array(3) { 0 } + init { val format = DecimalFormatSymbols(Locale.US) format.decimalSeparator = '.' @@ -46,12 +50,14 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { @UnstableApi override fun onStart() { super.onStart() - controller = object : PlaybackController(requireActivity()) { - override fun loadMediaInfo() { - if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) + if (controller == null) { + controller = object : PlaybackController(requireActivity()) { + override fun loadMediaInfo() { + if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) + } } + controller?.init() } - controller?.init() EventBus.getDefault().register(this) if (controller != null) updateSpeed(SpeedChangedEvent(controller!!.currentPlaybackSpeedMultiplier)) } @@ -73,6 +79,21 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { savedInstanceState: Bundle? ): View? { _binding = SpeedSelectDialogBinding.inflate(inflater) + + val argument = arguments?.getString("default_setting") + + when (argument) { + null, "Current" -> { + binding.currentAudio.isChecked = true + } + "Feed" -> { + binding.currentPodcast.isChecked = true + } + else -> { + binding.global.isChecked = true + } + } + speedSeekBar = binding.speedSeekBar speedSeekBar.setProgressChangedListener { multiplier: Float -> controller?.setPlaybackSpeed(multiplier) @@ -95,15 +116,24 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { skipSilence.isChecked = isSkipSilence skipSilence.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> isSkipSilence = isChecked - controller!!.setSkipSilence(isChecked) + controller?.setSkipSilence(isChecked) } + return binding.root } + @OptIn(UnstableApi::class) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) +// if (controller == null || !controller!!.isPlaybackServiceReady()) { +// binding.currentPodcast.visibility = View.INVISIBLE +// } else binding.currentPodcast.visibility = View.VISIBLE + } + override fun onDestroyView() { super.onDestroyView() _binding = null } + private fun addCurrentSpeed() { val newSpeed = speedSeekBar.currentSpeed if (selectedSpeeds.contains(newSpeed)) { @@ -134,15 +164,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { notifyDataSetChanged() true } - holder.chip.setOnClickListener { - Handler(Looper.getMainLooper()).postDelayed( - { - if (controller != null) { - dismiss() - controller!!.setPlaybackSpeed(speed) - } - }, 200) - } + holder.chip.setOnClickListener { Handler(Looper.getMainLooper()).postDelayed({ + if (binding.currentAudio.isChecked) settingCode[0] = 1 + if (binding.currentPodcast.isChecked) settingCode[1] = 1 + if (binding.global.isChecked) settingCode[2] = 1 + + if (controller != null) { + dismiss() + controller!!.setPlaybackSpeed(speed, settingCode) + } + }, 200) } } override fun getItemCount(): Int { @@ -156,4 +187,16 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() { inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder( chip) } + + companion object { + fun newInstance(argument: String? = null): VariableSpeedDialog { + val dialog = VariableSpeedDialog() + if (argument != null) { + val args = Bundle() + args.putString("default_setting", argument) + dialog.arguments = args + } + return dialog + } + } } 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 55887939..5f63cacc 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 @@ -2,16 +2,23 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R import ac.mdiq.podcini.databinding.AudioplayerFragmentBinding +import ac.mdiq.podcini.databinding.InternalPlayerFragmentBinding +import ac.mdiq.podcini.feed.util.ImageResourceUtils import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils import ac.mdiq.podcini.playback.PlaybackController import ac.mdiq.podcini.playback.base.PlayerStatus import ac.mdiq.podcini.playback.cast.CastEnabledActivity -import ac.mdiq.podcini.playback.event.* +import ac.mdiq.podcini.playback.event.PlaybackPositionEvent +import ac.mdiq.podcini.playback.event.PlaybackServiceEvent +import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent +import ac.mdiq.podcini.playback.event.SpeedChangedEvent import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.receiver.MediaButtonReceiver +import ac.mdiq.podcini.service.playback.PlaybackService import ac.mdiq.podcini.storage.model.feed.Chapter import ac.mdiq.podcini.storage.model.feed.FeedItem import ac.mdiq.podcini.storage.model.feed.FeedMedia +import ac.mdiq.podcini.storage.model.playback.MediaType import ac.mdiq.podcini.storage.model.playback.Playable import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView @@ -27,7 +34,6 @@ import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.TimeSpeedConverter import ac.mdiq.podcini.util.event.FavoritesEvent import ac.mdiq.podcini.util.event.PlayerErrorEvent -import ac.mdiq.podcini.util.event.UnreadItemsUpdateEvent import android.annotation.SuppressLint import android.app.Activity import android.content.Intent @@ -37,15 +43,18 @@ import android.text.Html import android.util.Log import android.view.* import android.widget.ImageButton -import android.widget.ProgressBar +import android.widget.ImageView import android.widget.SeekBar import android.widget.TextView +import androidx.annotation.OptIn import androidx.appcompat.widget.Toolbar import androidx.cardview.widget.CardView import androidx.core.app.ShareCompat import androidx.fragment.app.Fragment import androidx.interpolator.view.animation.FastOutSlowInInterpolator import androidx.media3.common.util.UnstableApi +import com.bumptech.glide.Glide +import com.bumptech.glide.request.RequestOptions import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.elevation.SurfaceColors @@ -70,35 +79,25 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar var _binding: AudioplayerFragmentBinding? = null private val binding get() = _binding!! - lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView - lateinit var txtvPlaybackSpeed: TextView - - private lateinit var episodeTitle: TextView private lateinit var itemDesrView: View - private lateinit var txtvPosition: TextView - private lateinit var txtvLength: TextView - private lateinit var sbPosition: ChapterSeekBar - private lateinit var butRev: ImageButton - private lateinit var txtvRev: TextView - private lateinit var butPlay: PlayButton - private lateinit var butFF: ImageButton - private lateinit var txtvFF: TextView - private lateinit var butSkip: ImageButton - private lateinit var txtvSkip: TextView + private lateinit var toolbar: MaterialToolbar - private lateinit var playerFragment: View - - private lateinit var progressIndicator: ProgressBar + private var playerFragment1: InternalPlayerFragment? = null + private var playerFragment2: InternalPlayerFragment? = null + private lateinit var playerView1: View + private lateinit var playerView2: View + private lateinit var cardViewSeek: CardView private lateinit var txtvSeek: TextView private var controller: PlaybackController? = null private var disposable: Disposable? = null - private var showTimeLeft = false private var seekedToChapterStart = false private var currentChapterIndex = -1 private var duration = 0 + private var currentMedia: Playable? = null + @SuppressLint("WrongConstant") override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, @@ -120,50 +119,39 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } toolbar.setOnMenuItemClickListener(this) - val externalPlayerFragment = ExternalPlayerFragment() + controller = newPlaybackController() + controller!!.init() + + playerFragment1 = InternalPlayerFragment.newInstance(controller!!) childFragmentManager.beginTransaction() - .replace(R.id.playerFragment, externalPlayerFragment, ExternalPlayerFragment.TAG) + .replace(R.id.playerFragment1, playerFragment1!!, InternalPlayerFragment.TAG) .commit() -// playerFragment = binding.playerFragment - playerFragment = binding.root.findViewById(R.id.playerFragment) - playerFragment.setBackgroundColor( + playerView1 = binding.root.findViewById(R.id.playerFragment1) + playerView1.setBackgroundColor( + SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density)) + + playerFragment2 = InternalPlayerFragment.newInstance(controller!!) + childFragmentManager.beginTransaction() + .replace(R.id.playerFragment2, playerFragment2!!, InternalPlayerFragment.TAG) + .commit() + playerView2 = binding.root.findViewById(R.id.playerFragment2) + playerView2.setBackgroundColor( SurfaceColors.getColorForElevation(requireContext(), 8 * resources.displayMetrics.density)) itemDesrView = binding.itemDescription - episodeTitle = binding.titleView - butPlaybackSpeed = binding.butPlaybackSpeed - txtvPlaybackSpeed = binding.txtvPlaybackSpeed - sbPosition = binding.sbPosition - txtvPosition = binding.txtvPosition - txtvLength = binding.txtvLength - butRev = binding.butRev - txtvRev = binding.txtvRev - butPlay = binding.butPlay - butFF = binding.butFF - txtvFF = binding.txtvFF - butSkip = binding.butSkip - txtvSkip = binding.txtvSkip - progressIndicator = binding.progLoading cardViewSeek = binding.cardViewSeek txtvSeek = binding.txtvSeek - setupLengthTextView() - setupControlButtons() - butPlaybackSpeed.setOnClickListener { - VariableSpeedDialog().show(childFragmentManager, null) - } - sbPosition.setOnSeekBarChangeListener(this) - val fm = requireActivity().supportFragmentManager val transaction = fm.beginTransaction() val itemDescFrag = PlayerDetailsFragment() transaction.replace(R.id.itemDescription, itemDescFrag).commit() - controller = newPlaybackController() - controller?.init() - loadMediaInfo(false) +// controller = externalPlayerFragment1.controller +// loadMediaInfo(false) EventBus.getDefault().register(this) +// updateUi(controller?.getMedia()) return binding.root } @@ -189,62 +177,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar dividerPos[i] = chapters[i].start / duration.toFloat() } } - - sbPosition.setDividerPos(dividerPos) } - private fun setupControlButtons() { - butRev.setOnClickListener { - if (controller != null) { - val curr: Int = controller!!.position - controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000) - } - } - butRev.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) - true - } - butPlay.setOnClickListener { - controller?.init() - controller?.playPause() - } - butPlay.setOnLongClickListener { - if (controller != null && controller!!.status == PlayerStatus.PLAYING) { - val fallbackSpeed = UserPreferences.fallbackSpeed - if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed) - } - true - } - butFF.setOnClickListener { - if (controller != null) { - val curr: Int = controller!!.position - controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000) - } - } - butFF.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) - true - } - butSkip.setOnClickListener { - if (controller != null && controller!!.status == PlayerStatus.PLAYING) { - val speedForward = UserPreferences.speedforwardSpeed - if (speedForward > 0.1f) controller!!.speedForward(speedForward) - } - } - butSkip.setOnLongClickListener { - activity?.sendBroadcast( - MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) - true - } - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) { - if (controller == null) return - updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) - } +// @Subscribe(threadMode = ThreadMode.MAIN) +// fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) { +// if (controller == null) return +// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) +// } @Subscribe(threadMode = ThreadMode.MAIN) fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { @@ -253,53 +192,62 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } } - private fun setupLengthTextView() { - showTimeLeft = UserPreferences.shouldShowRemainingTime() - txtvLength.setOnClickListener(View.OnClickListener { - if (controller == null) return@OnClickListener +// private fun setupLengthTextView() { +// showTimeLeft = UserPreferences.shouldShowRemainingTime() +// txtvLength.setOnClickListener(View.OnClickListener { +// if (controller == null) return@OnClickListener +// +// showTimeLeft = !showTimeLeft +// UserPreferences.setShowRemainTimeSetting(showTimeLeft) +// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) +// }) +// } - showTimeLeft = !showTimeLeft - UserPreferences.setShowRemainTimeSetting(showTimeLeft) - updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration)) - }) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { - val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) - txtvPlaybackSpeed.text = speedStr - butPlaybackSpeed.setSpeed(event.newSpeed) - } +// @Subscribe(threadMode = ThreadMode.MAIN) +// fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { +// val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) +// txtvPlaybackSpeed.text = speedStr +// butPlaybackSpeed.setSpeed(event.newSpeed) +// } private fun loadMediaInfo(includingChapters: Boolean) { Log.d(TAG, "loadMediaInfo called") - disposable?.dispose() - disposable = Maybe.create { emitter: MaybeEmitter -> - val media: Playable? = controller?.getMedia() - if (media != null) { - if (includingChapters) { - ChapterUtils.loadChapters(media, requireContext(), false) + + val theMedia = controller?.getMedia() ?: return + if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) { + Log.d(TAG, "loadMediaInfo loading details") + disposable?.dispose() + disposable = Maybe.create { emitter: MaybeEmitter -> + val media: Playable? = theMedia + if (media != null) { + if (includingChapters) { + ChapterUtils.loadChapters(media, requireContext(), false) + } + emitter.onSuccess(media) + } else { + emitter.onComplete() } - emitter.onSuccess(media) - } else { - emitter.onComplete() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ media: Playable -> + currentMedia = media + updateUi(media) + playerFragment1?.updateUi(media) + playerFragment2?.updateUi(media) + if (!includingChapters) { + loadMediaInfo(true) + } + }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }, + { updateUi(null) }) } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ media: Playable -> - updateUi(media) - if (!includingChapters) { - loadMediaInfo(true) - } - }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }, - { updateUi(null) }) } private fun newPlaybackController(): PlaybackController { return object : PlaybackController(requireActivity()) { override fun updatePlayButtonShowsPlay(showPlay: Boolean) { - butPlay.setIsShowPlay(showPlay) + playerFragment1?.butPlay?.setIsShowPlay(showPlay) + playerFragment2?.butPlay?.setIsShowPlay(showPlay) } override fun loadMediaInfo() { @@ -307,19 +255,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar } override fun onPlaybackEnd() { - (activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED + playerFragment1?.butPlay?.setIsShowPlay(true) + playerFragment2?.butPlay?.setIsShowPlay(true) + (activity as MainActivity).setPlayerVisible(null) } } } private fun updateUi(media: Playable?) { - if (controller != null) duration = controller!!.duration - if (media == null) return Log.d(TAG, "updateUi called") - - episodeTitle.text = media.getEpisodeTitle() - updatePosition(PlaybackPositionEvent(media.getPosition(), media.getDuration())) - updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) setChapterDividers(media) setupOptionsMenu(media) } @@ -339,73 +283,33 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar override fun onStart() { super.onStart() - txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) - txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) - txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) - if (UserPreferences.speedforwardSpeed > 0.1f) { - txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) - } else txtvSkip.visibility = View.GONE + loadMediaInfo(false) } override fun onStop() { super.onStop() - progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates +// progressIndicator.visibility = View.GONE // Controller released; we will not receive buffering updates disposable?.dispose() } - @Subscribe(threadMode = ThreadMode.MAIN) - @Suppress("unused") - fun bufferUpdate(event: BufferUpdateEvent) { - when { - event.hasStarted() -> { - progressIndicator.visibility = View.VISIBLE - } - event.hasEnded() -> { - progressIndicator.visibility = View.GONE - } - controller != null && controller!!.isStreaming -> { - sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt()) - } - else -> { - sbPosition.setSecondaryProgress(0) - } - } - } - - @UnstableApi - @Subscribe(threadMode = ThreadMode.MAIN) - fun updatePosition(event: PlaybackPositionEvent) { - if (controller == null) return - - val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) - val currentPosition: Int = converter.convert(event.position) - val duration: Int = converter.convert(event.duration) - val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt()) - currentChapterIndex = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), currentPosition) - // Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition)); - if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time $currentPosition $duration") - return - } - txtvPosition.text = Converter.getDurationStringLong(currentPosition) - txtvPosition.setContentDescription(getString(R.string.position, - Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) - showTimeLeft = UserPreferences.shouldShowRemainingTime() - if (showTimeLeft) { - txtvLength.setContentDescription(getString(R.string.remaining_time, - Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) - txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime) - } else { - txtvLength.setContentDescription(getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) - txtvLength.text = Converter.getDurationStringLong(duration) - } - - if (!sbPosition.isPressed && event.duration > 0) { - val progress: Float = (event.position.toFloat()) / event.duration - sbPosition.progress = (progress * sbPosition.max).toInt() - } - } +// @Subscribe(threadMode = ThreadMode.MAIN) +// @Suppress("unused") +// fun bufferUpdate(event: BufferUpdateEvent) { +// when { +// event.hasStarted() -> { +// progressIndicator.visibility = View.VISIBLE +// } +// event.hasEnded() -> { +// progressIndicator.visibility = View.GONE +// } +//// controller != null && controller!!.isStreaming -> { +//// sbPosition.setSecondaryProgress((event.progress * sbPosition.max).toInt()) +//// } +// else -> { +//// sbPosition.setSecondaryProgress(0) +// } +// } +// } @Subscribe(threadMode = ThreadMode.MAIN) fun favoritesChanged(event: FavoritesEvent?) { @@ -427,15 +331,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar var position: Int = converter.convert((prog * controller!!.duration).toInt()) val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(controller!!.getMedia(), position) if (newChapterIndex > -1) { - if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) { - currentChapterIndex = newChapterIndex - val media = controller!!.getMedia() - position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0 - seekedToChapterStart = true - controller!!.seekTo(position) - updateUi(controller!!.getMedia()) - sbPosition.highlightCurrentChapter() - } +// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) { +// currentChapterIndex = newChapterIndex +// val media = controller!!.getMedia() +// position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0 +// seekedToChapterStart = true +// controller!!.seekTo(position) +// updateUi(controller!!.getMedia()) +// sbPosition.highlightCurrentChapter() +// } txtvSeek.text = controller!!.getMedia()?.getChapters()?.get(newChapterIndex)?.title ?: ("" + "\n" + Converter.getDurationStringLong(position)) } else { @@ -538,7 +442,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar fun fadePlayerToToolbar(slideOffset: Float) { val playerFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.2f).toDouble())) / 0.2f).toFloat() - val player = playerFragment + val player = playerView1 player.alpha = 1 - playerFadeProgress player.visibility = if (playerFadeProgress > 0.99f) View.INVISIBLE else View.VISIBLE val toolbarFadeProgress = (max(0.0, min(0.2, (slideOffset - 0.6f).toDouble())) / 0.2f).toFloat() @@ -546,6 +450,297 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar toolbar.visibility = if (toolbarFadeProgress < 0.01f) View.INVISIBLE else View.VISIBLE } + class InternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener { + private var _binding: InternalPlayerFragmentBinding? = null + private val binding get() = _binding!! + + private lateinit var imgvCover: ImageView + lateinit var butPlay: PlayButton + + lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView + lateinit var txtvPlaybackSpeed: TextView + + private lateinit var episodeTitle: TextView + private lateinit var butRev: ImageButton + private lateinit var txtvRev: TextView + private lateinit var butFF: ImageButton + private lateinit var txtvFF: TextView + private lateinit var butSkip: ImageButton + private lateinit var txtvSkip: TextView + + private lateinit var txtvPosition: TextView + private lateinit var txtvLength: TextView + private lateinit var sbPosition: ChapterSeekBar + + private var showTimeLeft = false + + private var disposable: Disposable? = null + + @UnstableApi + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + _binding = InternalPlayerFragmentBinding.inflate(inflater) + Log.d(TAG, "fragment onCreateView") + + episodeTitle = binding.titleView + butPlaybackSpeed = binding.butPlaybackSpeed + txtvPlaybackSpeed = binding.txtvPlaybackSpeed + imgvCover = binding.imgvCover + butPlay = binding.butPlay + butRev = binding.butRev + txtvRev = binding.txtvRev + butFF = binding.butFF + txtvFF = binding.txtvFF + butSkip = binding.butSkip + txtvSkip = binding.txtvSkip + sbPosition = binding.sbPosition + txtvPosition = binding.txtvPosition + txtvLength = binding.txtvLength + + setupLengthTextView() + setupControlButtons() + butPlaybackSpeed.setOnClickListener { + VariableSpeedDialog.newInstance(null).show(childFragmentManager, null) + } + sbPosition.setOnSeekBarChangeListener(this) + + binding.internalPlayerFragment.setOnClickListener { + Log.d(TAG, "internalPlayerFragment was clicked") + val media = controller?.getMedia() + if (media != null) { + if (media.getMediaType() == MediaType.AUDIO) { + (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) + } else { + val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media) + startActivity(intent) + } + } + } + + EventBus.getDefault().register(this) + return binding.root + } + + @OptIn(UnstableApi::class) override fun onDestroyView() { + super.onDestroyView() + _binding = null + EventBus.getDefault().unregister(this) + } + + @UnstableApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + butPlay.setOnClickListener { + if (controller == null) return@setOnClickListener + + val media = controller!!.getMedia() + if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) { + controller!!.playPause() + requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media)) + } else { + controller!!.playPause() + } + } + } + + @OptIn(UnstableApi::class) private fun setupControlButtons() { + butRev.setOnClickListener { + if (controller != null) { + val curr: Int = controller!!.position + controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000) + } + } + butRev.setOnLongClickListener { + SkipPreferenceDialog.showSkipPreference(requireContext(), + SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) + true + } + butPlay.setOnClickListener { + controller?.init() + controller?.playPause() + } + butPlay.setOnLongClickListener { + if (controller != null && controller!!.status == PlayerStatus.PLAYING) { + val fallbackSpeed = UserPreferences.fallbackSpeed + if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed) + } + true + } + butFF.setOnClickListener { + if (controller != null) { + val curr: Int = controller!!.position + controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000) + } + } + butFF.setOnLongClickListener { + SkipPreferenceDialog.showSkipPreference(requireContext(), + SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) + true + } + butSkip.setOnClickListener { + if (controller != null && controller!!.status == PlayerStatus.PLAYING) { + val speedForward = UserPreferences.speedforwardSpeed + if (speedForward > 0.1f) controller!!.speedForward(speedForward) + } + } + butSkip.setOnLongClickListener { + activity?.sendBroadcast( + MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) + true + } + } + + @OptIn(UnstableApi::class) private fun setupLengthTextView() { + showTimeLeft = UserPreferences.shouldShowRemainingTime() + txtvLength.setOnClickListener(View.OnClickListener { + if (controller == null) { + return@OnClickListener + } + showTimeLeft = !showTimeLeft + UserPreferences.setShowRemainTimeSetting(showTimeLeft) + onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration)) + }) + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { + val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) + txtvPlaybackSpeed.text = speedStr + butPlaybackSpeed.setSpeed(event.newSpeed) + } + + @UnstableApi + @Subscribe(threadMode = ThreadMode.MAIN) + fun onPositionObserverUpdate(event: PlaybackPositionEvent) { + if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) { + return + } + val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) + val currentPosition: Int = converter.convert(event.position) + val duration: Int = converter.convert(event.duration) + val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt()) + if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { + Log.w(TAG, "Could not react to position observer update because of invalid time") + return + } + + txtvPosition.text = Converter.getDurationStringLong(currentPosition) + txtvPosition.setContentDescription(getString(R.string.position, + Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) + val showTimeLeft = UserPreferences.shouldShowRemainingTime() + if (showTimeLeft) { + txtvLength.setContentDescription(getString(R.string.remaining_time, + Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) + txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime) + } else { + txtvLength.setContentDescription(getString(R.string.chapter_duration, + Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) + txtvLength.text = Converter.getDurationStringLong(duration) + } + + if (!sbPosition.isPressed) { + val progress: Float = (event.position.toFloat()) / event.duration + sbPosition.progress = (progress * sbPosition.max).toInt() + } + } + + @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) + fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { + when (event.action) { + PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> { + (activity as MainActivity).setPlayerVisible(false) + } + PlaybackServiceEvent.Action.SERVICE_STARTED -> { + (activity as MainActivity).setPlayerVisible(true) + } + } + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "Fragment is about to be destroyed") + disposable?.dispose() + } + + @OptIn(UnstableApi::class) override fun onStart() { + super.onStart() + txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) + txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) + if (UserPreferences.speedforwardSpeed > 0.1f) { + txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) + } else txtvSkip.visibility = View.GONE + val media = controller?.getMedia() ?: return + updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) + } + + @UnstableApi + override fun onPause() { + super.onPause() + controller?.pause() + } + + @OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} + + override fun onStartTrackingTouch(seekBar: SeekBar) {} + + @OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) { + if (controller != null) { + val prog: Float = seekBar.progress / (seekBar.max.toFloat()) + controller!!.seekTo((prog * controller!!.duration).toInt()) + } + } + + @UnstableApi + fun updateUi(media: Playable?) { + if (media == null) return + Log.d(TAG, "updateUi called") + + episodeTitle.text = media.getEpisodeTitle() + (activity as MainActivity).setPlayerVisible(true) + onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration())) + + val options = RequestOptions() + .placeholder(R.color.light_gray) + .error(R.color.light_gray) + .fitCenter() + .dontAnimate() + + val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media) + val imgLocFB = ImageResourceUtils.getFallbackImageLocation(media) + when { + !imgLoc.isNullOrBlank() -> Glide.with(this) + .load(imgLoc) + .apply(options) + .into(imgvCover) + !imgLocFB.isNullOrBlank() -> Glide.with(this) + .load(imgLocFB) + .apply(options) + .into(imgvCover) + else -> imgvCover.setImageResource(R.mipmap.ic_launcher) + } + + if (controller?.isPlayingVideoLocally == true) { + (activity as MainActivity).bottomSheet.setLocked(true) + (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) + } else { + butPlay.visibility = View.VISIBLE + (activity as MainActivity).bottomSheet.setLocked(false) + } + } + + companion object { + const val TAG: String = "InternalPlayerFragment" + + var controller: PlaybackController? = null + + fun newInstance(controller_: PlaybackController) : InternalPlayerFragment { + controller = controller_ + return InternalPlayerFragment() + } + } + } + + companion object { const val TAG: String = "AudioPlayerFragment" } diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt deleted file mode 100644 index f21a8979..00000000 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/ExternalPlayerFragment.kt +++ /dev/null @@ -1,387 +0,0 @@ -package ac.mdiq.podcini.ui.fragment - -import ac.mdiq.podcini.R -import ac.mdiq.podcini.databinding.ExternalPlayerFragmentBinding -import ac.mdiq.podcini.feed.util.ImageResourceUtils.getEpisodeListImageLocation -import ac.mdiq.podcini.feed.util.ImageResourceUtils.getFallbackImageLocation -import ac.mdiq.podcini.feed.util.PlaybackSpeedUtils -import ac.mdiq.podcini.playback.PlaybackController -import ac.mdiq.podcini.playback.base.PlayerStatus -import ac.mdiq.podcini.playback.event.PlaybackPositionEvent -import ac.mdiq.podcini.playback.event.PlaybackServiceEvent -import ac.mdiq.podcini.playback.event.SpeedChangedEvent -import ac.mdiq.podcini.preferences.UserPreferences -import ac.mdiq.podcini.receiver.MediaButtonReceiver -import ac.mdiq.podcini.service.playback.PlaybackService.Companion.getPlayerActivityIntent -import ac.mdiq.podcini.storage.model.playback.MediaType -import ac.mdiq.podcini.storage.model.playback.Playable -import ac.mdiq.podcini.ui.activity.MainActivity -import ac.mdiq.podcini.ui.common.PlaybackSpeedIndicatorView -import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog -import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog -import ac.mdiq.podcini.ui.view.ChapterSeekBar -import ac.mdiq.podcini.ui.view.PlayButton -import ac.mdiq.podcini.util.Converter -import ac.mdiq.podcini.util.TimeSpeedConverter -import android.os.Bundle -import android.util.Log -import android.view.KeyEvent -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.ImageView -import android.widget.SeekBar -import android.widget.TextView -import androidx.annotation.OptIn -import androidx.fragment.app.Fragment -import androidx.media3.common.util.UnstableApi -import com.bumptech.glide.Glide -import com.bumptech.glide.request.RequestOptions -import com.google.android.material.bottomsheet.BottomSheetBehavior -import io.reactivex.Maybe -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.schedulers.Schedulers -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import java.text.DecimalFormat -import java.text.NumberFormat -import kotlin.math.max - -/** - * Fragment which is supposed to be displayed outside of the MediaplayerActivity. - */ -class ExternalPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener { - private var _binding: ExternalPlayerFragmentBinding? = null - private val binding get() = _binding!! - - private lateinit var imgvCover: ImageView - private lateinit var butPlay: PlayButton - - lateinit var butPlaybackSpeed: PlaybackSpeedIndicatorView - lateinit var txtvPlaybackSpeed: TextView - - private lateinit var episodeTitle: TextView - private lateinit var butRev: ImageButton - private lateinit var txtvRev: TextView - private lateinit var butFF: ImageButton - private lateinit var txtvFF: TextView - private lateinit var butSkip: ImageButton - private lateinit var txtvSkip: TextView - - private lateinit var txtvPosition: TextView - private lateinit var txtvLength: TextView - private lateinit var sbPosition: ChapterSeekBar - - private var showTimeLeft = false - - private var currentMedia: Playable? = null - - private var controller: PlaybackController? = null - private var disposable: Disposable? = null - - @UnstableApi - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - _binding = ExternalPlayerFragmentBinding.inflate(inflater) - Log.d(TAG, "fragment onCreateView") - - episodeTitle = binding.titleView - butPlaybackSpeed = binding.butPlaybackSpeed - txtvPlaybackSpeed = binding.txtvPlaybackSpeed - imgvCover = binding.imgvCover - butPlay = binding.butPlay - butRev = binding.butRev - txtvRev = binding.txtvRev - butFF = binding.butFF - txtvFF = binding.txtvFF - butSkip = binding.butSkip - txtvSkip = binding.txtvSkip - sbPosition = binding.sbPosition - txtvPosition = binding.txtvPosition - txtvLength = binding.txtvLength - - setupLengthTextView() - setupControlButtons() - butPlaybackSpeed.setOnClickListener { - VariableSpeedDialog().show(childFragmentManager, null) - } - sbPosition.setOnSeekBarChangeListener(this) - - binding.externalPlayerFragment.setOnClickListener { - Log.d(TAG, "externalPlayerFragment was clicked") - val media = controller?.getMedia() - if (media != null) { - if (media.getMediaType() == MediaType.AUDIO) { - (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED) - } else { - val intent = getPlayerActivityIntent(requireContext(), media) - startActivity(intent) - } - } - } - - controller = setupPlaybackController() - controller!!.init() - EventBus.getDefault().register(this) - return binding.root - } - - @OptIn(UnstableApi::class) override fun onDestroyView() { - super.onDestroyView() - _binding = null - controller?.release() - controller = null - EventBus.getDefault().unregister(this) - } - - @UnstableApi - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - butPlay.setOnClickListener { - if (controller == null) return@setOnClickListener - - val media = controller!!.getMedia() - if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) { - controller!!.playPause() - requireContext().startActivity(getPlayerActivityIntent(requireContext(), media)) - } else { - controller!!.playPause() - } - } - loadMediaInfo() - } - - @OptIn(UnstableApi::class) private fun setupControlButtons() { - butRev.setOnClickListener { - if (controller != null) { - val curr: Int = controller!!.position - controller!!.seekTo(curr - UserPreferences.rewindSecs * 1000) - } - } - butRev.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev) - true - } - butPlay.setOnClickListener { - controller?.init() - controller?.playPause() - } - butPlay.setOnLongClickListener { - if (controller != null && controller!!.status == PlayerStatus.PLAYING) { - val fallbackSpeed = UserPreferences.fallbackSpeed - if (fallbackSpeed > 0.1f) controller!!.fallbackSpeed(fallbackSpeed) - } - true - } - butFF.setOnClickListener { - if (controller != null) { - val curr: Int = controller!!.position - controller!!.seekTo(curr + UserPreferences.fastForwardSecs * 1000) - } - } - butFF.setOnLongClickListener { - SkipPreferenceDialog.showSkipPreference(requireContext(), - SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, txtvFF) - true - } - butSkip.setOnClickListener { - if (controller != null && controller!!.status == PlayerStatus.PLAYING) { - val speedForward = UserPreferences.speedforwardSpeed - if (speedForward > 0.1f) controller!!.speedForward(speedForward) - } - } - butSkip.setOnLongClickListener { - activity?.sendBroadcast( - MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) - true - } - } - - @UnstableApi - private fun setupPlaybackController(): PlaybackController { - return object : PlaybackController(requireActivity()) { - override fun updatePlayButtonShowsPlay(showPlay: Boolean) { - butPlay.setIsShowPlay(showPlay) - } - - override fun loadMediaInfo() { - Log.d(TAG, "setupPlaybackController loadMediaInfo called") - this@ExternalPlayerFragment.loadMediaInfo() - } - - override fun onPlaybackEnd() { - (activity as MainActivity).setPlayerVisible(false) - } - } - } - - @OptIn(UnstableApi::class) private fun setupLengthTextView() { - showTimeLeft = UserPreferences.shouldShowRemainingTime() - txtvLength.setOnClickListener(View.OnClickListener { - if (controller == null) { - return@OnClickListener - } - showTimeLeft = !showTimeLeft - UserPreferences.setShowRemainTimeSetting(showTimeLeft) - onPositionObserverUpdate(PlaybackPositionEvent(controller!!.position, controller!!.duration)) - }) - } - - @Subscribe(threadMode = ThreadMode.MAIN) - fun updatePlaybackSpeedButton(event: SpeedChangedEvent) { - val speedStr: String = DecimalFormat("0.00").format(event.newSpeed.toDouble()) - txtvPlaybackSpeed.text = speedStr - butPlaybackSpeed.setSpeed(event.newSpeed) - } - - @UnstableApi - @Subscribe(threadMode = ThreadMode.MAIN) - fun onPositionObserverUpdate(event: PlaybackPositionEvent) { - if (controller == null || controller!!.position == Playable.INVALID_TIME || controller!!.duration == Playable.INVALID_TIME) { - return - } - val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier) - val currentPosition: Int = converter.convert(event.position) - val duration: Int = converter.convert(event.duration) - val remainingTime: Int = converter.convert(max((event.duration - event.position).toDouble(), 0.0).toInt()) - if (currentPosition == Playable.INVALID_TIME || duration == Playable.INVALID_TIME) { - Log.w(TAG, "Could not react to position observer update because of invalid time") - return - } - - txtvPosition.text = Converter.getDurationStringLong(currentPosition) - txtvPosition.setContentDescription(getString(R.string.position, - Converter.getDurationStringLocalized(requireContext(), currentPosition.toLong()))) - val showTimeLeft = UserPreferences.shouldShowRemainingTime() - if (showTimeLeft) { - txtvLength.setContentDescription(getString(R.string.remaining_time, - Converter.getDurationStringLocalized(requireContext(), remainingTime.toLong()))) - txtvLength.text = (if (remainingTime > 0) "-" else "") + Converter.getDurationStringLong(remainingTime) - } else { - txtvLength.setContentDescription(getString(R.string.chapter_duration, - Converter.getDurationStringLocalized(requireContext(), duration.toLong()))) - txtvLength.text = Converter.getDurationStringLong(duration) - } - - if (!sbPosition.isPressed) { - val progress: Float = (event.position.toFloat()) / event.duration - sbPosition.progress = (progress * sbPosition.max).toInt() - } - } - - @UnstableApi @Subscribe(threadMode = ThreadMode.MAIN) - fun onPlaybackServiceChanged(event: PlaybackServiceEvent) { - when (event.action) { - PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN -> { - (activity as MainActivity).setPlayerVisible(false) - } - PlaybackServiceEvent.Action.SERVICE_STARTED -> { - (activity as MainActivity).setPlayerVisible(true) - } - } - } - - override fun onDestroy() { - super.onDestroy() - Log.d(TAG, "Fragment is about to be destroyed") - disposable?.dispose() - } - - @OptIn(UnstableApi::class) override fun onStart() { - super.onStart() - txtvRev.text = NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()) - txtvFF.text = NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()) - if (UserPreferences.speedforwardSpeed > 0.1f) { - txtvSkip.text = NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed) - } else txtvSkip.visibility = View.GONE - val media = controller?.getMedia() ?: return - updatePlaybackSpeedButton(SpeedChangedEvent(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media))) - } - - @UnstableApi - override fun onPause() { - super.onPause() - controller?.pause() - } - - @UnstableApi - private fun loadMediaInfo() { - Log.d(TAG, "loadMediaInfo called") - if (controller == null) { - Log.w(TAG, "loadMediaInfo was called while PlaybackController was null!") - return - } - val theMedia = controller?.getMedia() - if (currentMedia == null || theMedia?.getIdentifier() != currentMedia?.getIdentifier()) { - Log.d(TAG, "reloading media info") - disposable?.dispose() - disposable = Maybe.fromCallable { theMedia } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe({ media: Playable? -> - currentMedia = media - this.updateUi(media) }, - { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }, - { -// (activity as MainActivity).setPlayerVisible(false) - }) - } - } - - @OptIn(UnstableApi::class) override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {} - - override fun onStartTrackingTouch(seekBar: SeekBar) {} - - @OptIn(UnstableApi::class) override fun onStopTrackingTouch(seekBar: SeekBar) { - if (controller != null) { - val prog: Float = seekBar.progress / (seekBar.max.toFloat()) - controller!!.seekTo((prog * controller!!.duration).toInt()) - } - } - - @UnstableApi - private fun updateUi(media: Playable?) { - if (media == null) return - Log.d(TAG, "updateUi called") - - episodeTitle.text = media.getEpisodeTitle() - (activity as MainActivity).setPlayerVisible(true) - onPositionObserverUpdate(PlaybackPositionEvent(media.getPosition(), media.getDuration())) - - val options = RequestOptions() - .placeholder(R.color.light_gray) - .error(R.color.light_gray) - .fitCenter() - .dontAnimate() - - val imgLoc = getEpisodeListImageLocation(media) - val imgLocFB = getFallbackImageLocation(media) - when { - !imgLoc.isNullOrBlank() -> Glide.with(this) - .load(imgLoc) - .apply(options) - .into(imgvCover) - !imgLocFB.isNullOrBlank() -> Glide.with(this) - .load(imgLocFB) - .apply(options) - .into(imgvCover) - else -> imgvCover.setImageResource(R.mipmap.ic_launcher) - } - - if (controller?.isPlayingVideoLocally == true) { - (activity as MainActivity).bottomSheet.setLocked(true) - (activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED) - } else { - butPlay.visibility = View.VISIBLE - (activity as MainActivity).bottomSheet.setLocked(false) - } - } - - companion object { - const val TAG: String = "ExternalPlayerFragment" - } -} diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt index 4a1b50a7..5394acbb 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/FeedSettingsFragment.kt @@ -204,11 +204,9 @@ class FeedSettingsFragment : Fragment() { val feedPlaybackSpeedPreference = findPreference(PREF_FEED_PLAYBACK_SPEED) feedPlaybackSpeedPreference!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - val viewBinding = - PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater) + val viewBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(layoutInflater) viewBinding.seekBar.setProgressChangedListener { speed: Float? -> - viewBinding.currentSpeedLabel.text = String.format( - Locale.getDefault(), "%.2fx", speed) + viewBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) } viewBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> viewBinding.seekBar.isEnabled = !isChecked @@ -224,8 +222,8 @@ class FeedSettingsFragment : Fragment() { .setTitle(R.string.playback_speed) .setView(viewBinding.root) .setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> - val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked - ) FeedPreferences.SPEED_USE_GLOBAL else viewBinding.seekBar.currentSpeed + val newSpeed = if (viewBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL + else viewBinding.seekBar.currentSpeed feedPreferences!!.feedPlaybackSpeed = newSpeed if (feedPreferences != null) DBWriter.setFeedPreferences(feedPreferences!!) EventBus.getDefault().post( diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt index 3bc6797f..6179fe4f 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/QueueFragment.kt @@ -10,9 +10,7 @@ import ac.mdiq.podcini.playback.event.PlaybackPositionEvent import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.storage.DBReader import ac.mdiq.podcini.storage.DBWriter -import ac.mdiq.podcini.storage.model.feed.FeedItem -import ac.mdiq.podcini.storage.model.feed.FeedItemFilter -import ac.mdiq.podcini.storage.model.feed.SortOrder +import ac.mdiq.podcini.storage.model.feed.* import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.adapter.QueueRecyclerAdapter import ac.mdiq.podcini.ui.adapter.SelectableAdapter @@ -29,6 +27,7 @@ import ac.mdiq.podcini.ui.view.viewholder.EpisodeItemViewHolder import ac.mdiq.podcini.util.Converter import ac.mdiq.podcini.util.FeedItemUtil import ac.mdiq.podcini.util.event.* +import ac.mdiq.podcini.util.event.settings.SpeedPresetChangedEvent import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences @@ -311,6 +310,18 @@ class QueueFragment : Fragment(), Toolbar.OnMenuItemClickListener, SelectableAda refreshToolbarState() } +// @Subscribe(threadMode = ThreadMode.MAIN) +// @Suppress("unused") +// fun speedPresetChanged(event: SpeedPresetChangedEvent) { +//// Log.d(TAG,"speedPresetChanged called") +//// for (item in queue) { +//// if (item.feed?.id == event.feedId && item.feed!!.preferences != null) { +//// Log.d(TAG, "speedPresetChanged ${item.feed!!.title} ${event.speed}") +//// item.feed!!.preferences!!.feedPlaybackSpeed = event.speed +//// } +//// } +// } + @Subscribe(threadMode = ThreadMode.MAIN) fun onSwipeActionsChanged(event: SwipeActionsChangedEvent?) { refreshSwipeTelltale() diff --git a/app/src/main/java/ac/mdiq/podcini/ui/fragment/preferences/PlaybackPreferencesFragment.kt b/app/src/main/java/ac/mdiq/podcini/ui/fragment/preferences/PlaybackPreferencesFragment.kt index a4a2dec3..24f7c893 100644 --- a/app/src/main/java/ac/mdiq/podcini/ui/fragment/preferences/PlaybackPreferencesFragment.kt +++ b/app/src/main/java/ac/mdiq/podcini/ui/fragment/preferences/PlaybackPreferencesFragment.kt @@ -39,7 +39,7 @@ class PlaybackPreferencesFragment : PreferenceFragmentCompat() { findPreference(PREF_PLAYBACK_SPEED_LAUNCHER)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { - VariableSpeedDialog().show(childFragmentManager, null) + VariableSpeedDialog.newInstance("Global").show(childFragmentManager, null) true } findPreference(PREF_PLAYBACK_REWIND_DELTA_LAUNCHER)!!.onPreferenceClickListener = diff --git a/app/src/main/res/layout/audioplayer_fragment.xml b/app/src/main/res/layout/audioplayer_fragment.xml index 7ad0c6cb..2d53e27f 100644 --- a/app/src/main/res/layout/audioplayer_fragment.xml +++ b/app/src/main/res/layout/audioplayer_fragment.xml @@ -8,7 +8,7 @@ android:layout_height="match_parent"> @@ -75,231 +75,13 @@ tools:text="1:06:29" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + tools:layout_height="@dimen/external_player_height" /> diff --git a/app/src/main/res/layout/external_player_fragment.xml b/app/src/main/res/layout/internal_player_fragment.xml similarity index 97% rename from app/src/main/res/layout/external_player_fragment.xml rename to app/src/main/res/layout/internal_player_fragment.xml index 7c4d004e..b2728ee3 100644 --- a/app/src/main/res/layout/external_player_fragment.xml +++ b/app/src/main/res/layout/internal_player_fragment.xml @@ -3,7 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/external_player_fragment" + android:id="@+id/internal_player_fragment" android:layout_width="match_parent" android:layout_height="@dimen/external_player_height" android:background="?attr/selectableItemBackground" @@ -25,7 +25,7 @@ android:id="@+id/sbPosition" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="5.dp" + android:layout_marginTop="5dp" android:clickable="true" android:max="500" tools:progress="100" /> @@ -126,6 +126,7 @@ android:layout_alignLeft="@id/butRev" android:layout_alignEnd="@id/butRev" android:layout_alignRight="@id/butRev" + android:layout_marginBottom="5dp" android:clickable="false" android:gravity="center" android:text="30" @@ -155,6 +156,7 @@ android:layout_alignLeft="@id/butPlaybackSpeed" android:layout_alignEnd="@id/butPlaybackSpeed" android:layout_alignRight="@id/butPlaybackSpeed" + android:layout_marginBottom="5dp" android:clickable="false" android:gravity="center" android:text="1.00" @@ -186,6 +188,7 @@ android:layout_alignLeft="@id/butFF" android:layout_alignEnd="@id/butFF" android:layout_alignRight="@id/butFF" + android:layout_marginBottom="5dp" android:clickable="false" android:gravity="center" android:text="30" @@ -217,6 +220,7 @@ android:layout_alignLeft="@id/butSkip" android:layout_alignEnd="@id/butSkip" android:layout_alignRight="@id/butSkip" + android:layout_marginBottom="5dp" android:clickable="false" android:gravity="center" android:textColor="?android:attr/textColorSecondary" diff --git a/app/src/main/res/layout/speed_select_dialog.xml b/app/src/main/res/layout/speed_select_dialog.xml index 4c595208..d32ba457 100644 --- a/app/src/main/res/layout/speed_select_dialog.xml +++ b/app/src/main/res/layout/speed_select_dialog.xml @@ -58,12 +58,33 @@ android:text="@string/All" /> - + android:orientation="horizontal"> + + + + + + + Years Notifications Current + Current + Podcast + Global Podcini Echo Podcini Echo %d diff --git a/changelog.md b/changelog.md index 897c824d..b7879ca6 100644 --- a/changelog.md +++ b/changelog.md @@ -202,4 +202,10 @@ ## 4.5.1 -* fixed bug in subscription sorting \ No newline at end of file +* fixed bug in subscription sorting + +## 4.5.2 + +* revamped audio player class, merged external player in +* speed setting now allows setting with three options: current audio, podcast, and global. +* added a bit bottom margin for the numbers in player \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/3020122.txt b/fastlane/metadata/android/en-US/changelogs/3020122.txt new file mode 100644 index 00000000..9c5acbf1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020122.txt @@ -0,0 +1,6 @@ + +Version 4.5.2 brings several changes: + +* revamped audio player class, merged external player in +* speed setting now allows setting with three options: current audio, podcast, and global. +* added a bit bottom margin for the numbers in player