4.9.0 commit
|
@ -82,6 +82,9 @@ Even so, the database remains backward compatible, and AntennaPod's db can be ea
|
|||
|
||||
* New share notes menu option on various episode views
|
||||
* Feed info view offers a link for direct search of feeds related to author
|
||||
* New episode home view with two display modes: webpage or reader
|
||||
* Text-to-Speech is enabled in reader's mode
|
||||
* RSS feeds with no playable media can be subscribed and read/listened
|
||||
|
||||
### Online feed
|
||||
|
||||
|
|
|
@ -149,8 +149,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020130
|
||||
versionName "4.8.0"
|
||||
versionCode 3020131
|
||||
versionName "4.9.0"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
@ -227,10 +227,11 @@ dependencies {
|
|||
implementation "androidx.fragment:fragment-ktx:1.6.2"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.media:media:1.7.0"
|
||||
implementation "androidx.media3:media3-exoplayer:1.2.1"
|
||||
implementation "androidx.media3:media3-ui:1.2.1"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:1.2.1"
|
||||
implementation "androidx.media3:media3-common:1.2.1"
|
||||
implementation "androidx.media3:media3-exoplayer:1.3.1"
|
||||
implementation "androidx.media3:media3-ui:1.3.1"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:1.3.1"
|
||||
implementation "androidx.media3:media3-common:1.3.1"
|
||||
implementation "androidx.media3:media3-session:1.3.1"
|
||||
implementation "androidx.palette:palette-ktx:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
|
@ -269,12 +270,15 @@ dependencies {
|
|||
implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3'
|
||||
implementation "com.annimon:stream:1.2.2"
|
||||
|
||||
implementation "net.dankito.readability4j:readability4j:1.0.8"
|
||||
|
||||
// Non-free dependencies:
|
||||
playImplementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
compileOnly "com.google.android.wearable:wearable:2.9.0"
|
||||
|
||||
// this one can not be updated?
|
||||
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.5.1"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.5.1"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-intents:3.5.1"
|
||||
|
|
|
@ -9,9 +9,7 @@ import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
|
|||
*/
|
||||
object CastPsmp {
|
||||
@JvmStatic
|
||||
fun getInstanceIfConnected(context: Context,
|
||||
callback: PSMPCallback
|
||||
): PlaybackServiceMediaPlayer? {
|
||||
fun getInstanceIfConnected(context: Context, callback: PSMPCallback): PlaybackServiceMediaPlayer? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.service.playback
|
||||
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.media3.session.MediaSession
|
||||
|
||||
internal object WearMediaSession {
|
||||
/**
|
||||
|
@ -11,7 +11,7 @@ internal object WearMediaSession {
|
|||
// no-op
|
||||
}
|
||||
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat?) {
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSession?) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
tools:ignore="ExportedService">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaLibraryService"/>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
|
||||
</intent-filter>
|
||||
|
|
|
@ -20,8 +20,8 @@ import kotlin.concurrent.Volatile
|
|||
* Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
|
||||
* and remote (cast devices) playback.
|
||||
*/
|
||||
abstract class PlaybackServiceMediaPlayer protected constructor(protected val context: Context,
|
||||
protected val callback: PSMPCallback) {
|
||||
abstract class PlaybackServiceMediaPlayer protected constructor(protected val context: Context, protected val callback: PSMPCallback) {
|
||||
|
||||
@Volatile
|
||||
private var oldPlayerStatus: PlayerStatus? = null
|
||||
|
||||
|
@ -46,6 +46,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
}
|
||||
|
||||
abstract fun isStartWhenPrepared(): Boolean
|
||||
|
||||
abstract fun setStartWhenPrepared(startWhenPrepared: Boolean)
|
||||
|
||||
abstract fun getPlayable(): Playable?
|
||||
|
@ -68,6 +69,8 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
|
||||
abstract fun getSelectedAudioTrack(): Int
|
||||
|
||||
abstract fun createMediaPlayer()
|
||||
|
||||
/**
|
||||
* Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
|
||||
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will
|
||||
|
@ -98,11 +101,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
* for playback immediately (see 'prepareImmediately' parameter for more details)
|
||||
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
|
||||
*/
|
||||
abstract fun playMediaObject(playable: Playable,
|
||||
stream: Boolean,
|
||||
startWhenPrepared: Boolean,
|
||||
prepareImmediately: Boolean
|
||||
)
|
||||
abstract fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean)
|
||||
|
||||
/**
|
||||
* Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state.
|
||||
|
@ -279,9 +278,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
*
|
||||
* @return a Future, just for the purpose of tracking its execution.
|
||||
*/
|
||||
protected abstract fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean,
|
||||
shouldContinue: Boolean, toStoppedState: Boolean
|
||||
)
|
||||
protected abstract fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean)
|
||||
|
||||
/**
|
||||
* @return `true` if the WifiLock feature should be used, `false` otherwise.
|
||||
|
@ -327,9 +324,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
* Will be ignored if given the value of [Playable.INVALID_TIME].
|
||||
*/
|
||||
@Synchronized
|
||||
protected fun setPlayerStatus(newStatus: PlayerStatus,
|
||||
newMedia: Playable?, position: Int
|
||||
) {
|
||||
protected fun setPlayerStatus(newStatus: PlayerStatus, newMedia: Playable?, position: Int) {
|
||||
Log.d(TAG, this.javaClass.simpleName + ": Setting player status to " + newStatus)
|
||||
|
||||
this.oldPlayerStatus = playerStatus
|
||||
|
|
|
@ -44,9 +44,6 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
private val bufferUpdateInterval = 5L
|
||||
|
||||
private val bufferingUpdateDisposable: Disposable
|
||||
private lateinit var exoPlayer: ExoPlayer
|
||||
private lateinit var trackSelector: DefaultTrackSelector
|
||||
|
||||
private var loudnessEnhancer: LoudnessEnhancer? = null
|
||||
private var mediaSource: MediaSource? = null
|
||||
private var audioSeekCompleteListener: Runnable? = null
|
||||
|
@ -58,36 +55,18 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
|
||||
init {
|
||||
createPlayer()
|
||||
playbackParameters = exoPlayer.playbackParameters
|
||||
playbackParameters = exoPlayer!!.playbackParameters
|
||||
bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
bufferingUpdateListener?.accept(exoPlayer.bufferedPercentage)
|
||||
bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createPlayer() {
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
loadControl.setBufferDurationsMs(30000, 120000,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
|
||||
loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true)
|
||||
trackSelector = DefaultTrackSelector(context)
|
||||
val audioOffloadPreferences = AudioOffloadPreferences.Builder()
|
||||
.setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed
|
||||
.setIsGaplessSupportRequired(true)
|
||||
.setIsSpeedChangeSupportRequired(true)
|
||||
.build()
|
||||
exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context))
|
||||
.setTrackSelector(trackSelector)
|
||||
.setLoadControl(loadControl.build())
|
||||
.build()
|
||||
exoPlayer.setSeekParameters(SeekParameters.EXACT)
|
||||
exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setAudioOffloadPreferences(audioOffloadPreferences)
|
||||
.build()
|
||||
exoPlayer.addListener(object : Player.Listener {
|
||||
if (exoPlayer == null) createStaticPlayer(context)
|
||||
|
||||
exoPlayer?.addListener(object : Player.Listener {
|
||||
override fun onPlaybackStateChanged(playbackState: @Player.State Int) {
|
||||
when {
|
||||
audioCompletionListener != null && playbackState == Player.STATE_ENDED -> {
|
||||
|
@ -119,10 +98,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(oldPosition: PositionInfo,
|
||||
newPosition: PositionInfo,
|
||||
reason: @DiscontinuityReason Int
|
||||
) {
|
||||
override fun onPositionDiscontinuity(oldPosition: PositionInfo, newPosition: PositionInfo, reason: @DiscontinuityReason Int) {
|
||||
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||
audioSeekCompleteListener?.run()
|
||||
}
|
||||
|
@ -133,40 +109,37 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
}
|
||||
})
|
||||
|
||||
initLoudnessEnhancer(exoPlayer.audioSessionId)
|
||||
initLoudnessEnhancer(exoPlayer!!.audioSessionId)
|
||||
}
|
||||
|
||||
val currentPosition: Int
|
||||
get() = exoPlayer.currentPosition.toInt()
|
||||
get() = exoPlayer!!.currentPosition.toInt()
|
||||
|
||||
val currentSpeedMultiplier: Float
|
||||
get() = playbackParameters.speed
|
||||
|
||||
val duration: Int
|
||||
get() {
|
||||
if (exoPlayer.duration == C.TIME_UNSET) {
|
||||
return Playable.INVALID_TIME
|
||||
}
|
||||
return exoPlayer.duration.toInt()
|
||||
if (exoPlayer?.duration == C.TIME_UNSET) return Playable.INVALID_TIME
|
||||
return exoPlayer!!.duration.toInt()
|
||||
}
|
||||
|
||||
val isPlaying: Boolean
|
||||
get() = exoPlayer.playWhenReady
|
||||
get() = exoPlayer!!.playWhenReady
|
||||
|
||||
fun pause() {
|
||||
exoPlayer.pause()
|
||||
exoPlayer?.pause()
|
||||
}
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
fun prepare() {
|
||||
if (mediaSource == null) return
|
||||
exoPlayer.setMediaSource(mediaSource!!, false)
|
||||
exoPlayer.prepare()
|
||||
exoPlayer?.setMediaSource(mediaSource!!, false)
|
||||
exoPlayer?.prepare()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
bufferingUpdateDisposable.dispose()
|
||||
exoPlayer.release()
|
||||
|
||||
audioSeekCompleteListener = null
|
||||
audioCompletionListener = null
|
||||
|
@ -175,23 +148,22 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
fun reset() {
|
||||
exoPlayer.release()
|
||||
createPlayer()
|
||||
}
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
fun seekTo(i: Int) {
|
||||
exoPlayer.seekTo(i.toLong())
|
||||
exoPlayer?.seekTo(i.toLong())
|
||||
audioSeekCompleteListener?.run()
|
||||
}
|
||||
|
||||
fun setAudioStreamType(i: Int) {
|
||||
val a = exoPlayer.audioAttributes
|
||||
val a = exoPlayer!!.audioAttributes
|
||||
val b = AudioAttributes.Builder()
|
||||
b.setContentType(i)
|
||||
b.setFlags(a.flags)
|
||||
b.setUsage(a.usage)
|
||||
exoPlayer.setAudioAttributes(b.build(), false)
|
||||
exoPlayer?.setAudioAttributes(b.build(), false)
|
||||
}
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class)
|
||||
|
@ -201,9 +173,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
// Call.Factory callFactory = PodciniHttpClient.getHttpClient(); // Assuming it returns OkHttpClient
|
||||
// OkHttpDataSource.Factory httpDataSourceFactory = new OkHttpDataSource.Factory(callFactory)
|
||||
// .setUserAgent(ClientConfig.USER_AGENT);
|
||||
val httpDataSourceFactory =
|
||||
OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
|
||||
.setUserAgent(ClientConfig.USER_AGENT)
|
||||
val httpDataSourceFactory = OkHttpDataSource.Factory(PodciniHttpClient.getHttpClient() as okhttp3.Call.Factory)
|
||||
.setUserAgent(ClientConfig.USER_AGENT)
|
||||
|
||||
if (!user.isNullOrEmpty() && !password.isNullOrEmpty()) {
|
||||
val requestProperties = HashMap<String, String>()
|
||||
|
@ -225,35 +196,35 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
fun setDisplay(sh: SurfaceHolder?) {
|
||||
exoPlayer.setVideoSurfaceHolder(sh)
|
||||
exoPlayer?.setVideoSurfaceHolder(sh)
|
||||
}
|
||||
|
||||
fun setPlaybackParams(speed: Float, skipSilence: Boolean) {
|
||||
Log.d(TAG, "setPlaybackParams speed=$speed pitch=${playbackParameters.pitch} skipSilence=$skipSilence")
|
||||
playbackParameters = PlaybackParameters(speed, playbackParameters.pitch)
|
||||
exoPlayer.skipSilenceEnabled = skipSilence
|
||||
exoPlayer.playbackParameters = playbackParameters
|
||||
exoPlayer!!.skipSilenceEnabled = skipSilence
|
||||
exoPlayer!!.playbackParameters = playbackParameters
|
||||
}
|
||||
|
||||
fun setVolume(v: Float, v1: Float) {
|
||||
if (v > 1) {
|
||||
exoPlayer.volume = 1f
|
||||
exoPlayer!!.volume = 1f
|
||||
loudnessEnhancer?.setEnabled(true)
|
||||
loudnessEnhancer?.setTargetGain((1000 * (v - 1)).toInt())
|
||||
} else {
|
||||
exoPlayer.volume = v
|
||||
exoPlayer!!.volume = v
|
||||
loudnessEnhancer?.setEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
fun start() {
|
||||
exoPlayer.play()
|
||||
exoPlayer?.play()
|
||||
// Can't set params when paused - so always set it on start in case they changed
|
||||
exoPlayer.playbackParameters = playbackParameters
|
||||
exoPlayer!!.playbackParameters = playbackParameters
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
exoPlayer.stop()
|
||||
exoPlayer?.stop()
|
||||
}
|
||||
|
||||
val audioTracks: List<String>
|
||||
|
@ -269,8 +240,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
private val formats: List<Format>
|
||||
get() {
|
||||
val formats: MutableList<Format> = arrayListOf()
|
||||
val trackInfo = trackSelector.currentMappedTrackInfo
|
||||
?: return emptyList()
|
||||
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return emptyList()
|
||||
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
|
||||
for (i in 0 until trackGroups.length) {
|
||||
formats.add(trackGroups[i].getFormat(0))
|
||||
|
@ -279,28 +249,25 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
}
|
||||
|
||||
fun setAudioTrack(track: Int) {
|
||||
val trackInfo = trackSelector.currentMappedTrackInfo ?: return
|
||||
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return
|
||||
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
|
||||
val override = SelectionOverride(track, 0)
|
||||
val rendererIndex = audioRendererIndex
|
||||
val params = trackSelector.buildUponParameters()
|
||||
.setSelectionOverride(rendererIndex, trackGroups, override)
|
||||
trackSelector.setParameters(params)
|
||||
val params = trackSelector!!.buildUponParameters().setSelectionOverride(rendererIndex, trackGroups, override)
|
||||
trackSelector!!.setParameters(params)
|
||||
}
|
||||
|
||||
private val audioRendererIndex: Int
|
||||
get() {
|
||||
for (i in 0 until exoPlayer.rendererCount) {
|
||||
if (exoPlayer.getRendererType(i) == C.TRACK_TYPE_AUDIO) {
|
||||
return i
|
||||
}
|
||||
for (i in 0 until exoPlayer!!.rendererCount) {
|
||||
if (exoPlayer?.getRendererType(i) == C.TRACK_TYPE_AUDIO) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
val selectedAudioTrack: Int
|
||||
get() {
|
||||
val trackSelections = exoPlayer.currentTrackSelections
|
||||
val trackSelections = exoPlayer!!.currentTrackSelections
|
||||
val availableFormats = formats
|
||||
for (i in 0 until trackSelections.length) {
|
||||
val track = trackSelections[i] as ExoTrackSelection? ?: continue
|
||||
|
@ -325,12 +292,12 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
|
||||
val videoWidth: Int
|
||||
get() {
|
||||
return exoPlayer.videoFormat?.width ?: 0
|
||||
return exoPlayer?.videoFormat?.width ?: 0
|
||||
}
|
||||
|
||||
val videoHeight: Int
|
||||
get() {
|
||||
return exoPlayer.videoFormat?.height ?: 0
|
||||
return exoPlayer?.videoFormat?.height ?: 0
|
||||
}
|
||||
|
||||
fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer<Int>?) {
|
||||
|
@ -356,5 +323,33 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
|
|||
const val BUFFERING_ENDED: Int = -2
|
||||
private const val TAG = "ExoPlayerWrapper"
|
||||
const val ERROR_CODE_OFFSET: Int = 1000
|
||||
|
||||
private var trackSelector: DefaultTrackSelector? = null
|
||||
var exoPlayer: ExoPlayer? = null
|
||||
|
||||
fun createStaticPlayer(context: Context) {
|
||||
val loadControl = DefaultLoadControl.Builder()
|
||||
loadControl.setBufferDurationsMs(30000, 120000, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
|
||||
loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true)
|
||||
trackSelector = DefaultTrackSelector(context)
|
||||
val audioOffloadPreferences = AudioOffloadPreferences.Builder()
|
||||
.setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed
|
||||
.setIsGaplessSupportRequired(true)
|
||||
.setIsSpeedChangeSupportRequired(true)
|
||||
.build()
|
||||
Log.d(TAG, "createPlayer creating exoPlayer_")
|
||||
|
||||
exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context))
|
||||
.setTrackSelector(trackSelector!!)
|
||||
.setLoadControl(loadControl.build())
|
||||
.build()
|
||||
|
||||
exoPlayer?.setSeekParameters(SeekParameters.EXACT)
|
||||
exoPlayer!!.trackSelectionParameters = exoPlayer!!.trackSelectionParameters
|
||||
.buildUpon()
|
||||
.setAudioOffloadPreferences(audioOffloadPreferences)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,11 +98,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
* for playback immediately (see 'prepareImmediately' parameter for more details)
|
||||
* @param prepareImmediately Set to true if the method should also prepare the episode for playback.
|
||||
*/
|
||||
override fun playMediaObject(playable: Playable,
|
||||
stream: Boolean,
|
||||
startWhenPrepared: Boolean,
|
||||
prepareImmediately: Boolean
|
||||
) {
|
||||
override fun playMediaObject(playable: Playable, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) {
|
||||
Log.d(TAG, "playMediaObject(...)")
|
||||
try {
|
||||
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately)
|
||||
|
@ -121,12 +117,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
*
|
||||
* @see .playMediaObject
|
||||
*/
|
||||
private fun playMediaObject(playable: Playable,
|
||||
forceReset: Boolean,
|
||||
stream: Boolean,
|
||||
startWhenPrepared: Boolean,
|
||||
prepareImmediately: Boolean
|
||||
) {
|
||||
private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) {
|
||||
if (media != null) {
|
||||
if (!forceReset && media!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) {
|
||||
// episode is already playing -> ignore method call
|
||||
|
@ -161,7 +152,6 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
try {
|
||||
callback.ensureMediaInfoLoaded(media!!)
|
||||
callback.onMediaChanged(false)
|
||||
// TODO: speed
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence)
|
||||
when {
|
||||
stream -> {
|
||||
|
@ -169,10 +159,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (streamurl != null) {
|
||||
if (playable is FeedMedia && playable.item?.feed?.preferences != null) {
|
||||
val preferences = playable.item!!.feed!!.preferences!!
|
||||
mediaPlayer?.setDataSource(
|
||||
streamurl,
|
||||
preferences.username,
|
||||
preferences.password)
|
||||
mediaPlayer?.setDataSource(streamurl, preferences.username, preferences.password)
|
||||
} else {
|
||||
mediaPlayer?.setDataSource(streamurl)
|
||||
}
|
||||
|
@ -221,14 +208,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
Log.d(TAG, "Audiofocus successfully requested")
|
||||
Log.d(TAG, "Resuming/Starting playback")
|
||||
acquireWifiLockIfNecessary()
|
||||
// TODO: speed
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence)
|
||||
setVolume(1.0f, 1.0f)
|
||||
|
||||
if (media != null && playerStatus == PlayerStatus.PREPARED && media!!.getPosition() > 0) {
|
||||
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
|
||||
media!!.getPosition(),
|
||||
media!!.getLastPlayedTime())
|
||||
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(media!!.getPosition(), media!!.getLastPlayedTime())
|
||||
seekTo(newPosition)
|
||||
}
|
||||
mediaPlayer?.start()
|
||||
|
@ -502,9 +486,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (mediaPlayer != null) {
|
||||
try {
|
||||
clearMediaPlayerListeners()
|
||||
if (mediaPlayer!!.isPlaying) {
|
||||
mediaPlayer!!.stop()
|
||||
}
|
||||
if (mediaPlayer!!.isPlaying) mediaPlayer!!.stop()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
@ -571,7 +553,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
return mediaPlayer?.selectedAudioTrack?:0
|
||||
}
|
||||
|
||||
private fun createMediaPlayer() {
|
||||
override fun createMediaPlayer() {
|
||||
mediaPlayer?.release()
|
||||
|
||||
if (media == null) {
|
||||
|
@ -650,9 +632,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
.build()
|
||||
}
|
||||
|
||||
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean,
|
||||
shouldContinue: Boolean, toStoppedState: Boolean
|
||||
) {
|
||||
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean, toStoppedState: Boolean) {
|
||||
releaseWifiLockIfNecessary()
|
||||
|
||||
val isPlaying = playerStatus == PlayerStatus.PLAYING
|
||||
|
@ -720,13 +700,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
|
||||
private fun setMediaPlayerListeners(mp: ExoPlayerWrapper?) {
|
||||
if (mp == null || media == null) {
|
||||
return
|
||||
}
|
||||
mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true,
|
||||
wasSkipped = false,
|
||||
shouldContinue = true,
|
||||
toStoppedState = true) })
|
||||
if (mp == null || media == null) return
|
||||
|
||||
mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true) })
|
||||
mp.setOnSeekCompleteListener(Runnable { this.genericSeekCompleteListener() })
|
||||
mp.setOnBufferingUpdateListener(Consumer { percent: Int ->
|
||||
when (percent) {
|
||||
|
|
|
@ -11,11 +11,9 @@ import ac.mdiq.podcini.playback.base.PlayerStatus
|
|||
import ac.mdiq.podcini.playback.cast.CastPsmp
|
||||
import ac.mdiq.podcini.playback.cast.CastStateListener
|
||||
import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager.PSTMCallback
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.clearCurrentlyPlayingTemporaryPlaybackSpeed
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceFromPreferences
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentEpisodeIsVideo
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentPlayerStatus
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingTemporaryPlaybackSpeed
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeMediaPlaying
|
||||
|
@ -26,8 +24,6 @@ import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableFrom
|
|||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.autoEnableTo
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.timerMillis
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.allEpisodesSortOrder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.downloadsSortedOrder
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.getPlaybackSpeed
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.hardwareForwardButton
|
||||
|
@ -39,7 +35,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
|
|||
import ac.mdiq.podcini.preferences.UserPreferences.isSkipSilence
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.playbackSpeedArray
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setPlaybackSpeed
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldFavoriteKeepEpisode
|
||||
|
@ -52,8 +47,10 @@ 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.FeedSearcher
|
||||
import ac.mdiq.podcini.storage.model.feed.*
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedPreferences
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedPreferences.AutoDeleteAction
|
||||
import ac.mdiq.podcini.storage.model.playback.MediaType
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
|
@ -61,7 +58,6 @@ import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
|
|||
import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
|
||||
import ac.mdiq.podcini.util.ChapterUtils.getCurrentChapterIndex
|
||||
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.util.FeedUtil.shouldAutoDeleteItemsOnThatFeed
|
||||
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
||||
|
@ -76,15 +72,16 @@ import android.Manifest
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.UiModeManager
|
||||
import android.bluetooth.BluetoothA2dp
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.os.Binder
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.IBinder
|
||||
import android.os.Vibrator
|
||||
import android.service.quicksettings.TileService
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
|
@ -96,7 +93,6 @@ import android.util.Log
|
|||
import android.util.Pair
|
||||
import android.view.KeyEvent
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.ViewConfiguration
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
|
@ -104,10 +100,13 @@ import androidx.annotation.StringRes
|
|||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import io.reactivex.*
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.SingleEmitter
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
@ -123,7 +122,7 @@ import kotlin.math.max
|
|||
* Controls the MediaPlayer that plays a FeedMedia-file
|
||||
*/
|
||||
@UnstableApi
|
||||
class PlaybackService : MediaBrowserServiceCompat() {
|
||||
class PlaybackService : MediaLibraryService() {
|
||||
private var mediaPlayer: PlaybackServiceMediaPlayer? = null
|
||||
private var positionEventTimer: Disposable? = null
|
||||
|
||||
|
@ -133,8 +132,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
private lateinit var castStateListener: CastStateListener
|
||||
|
||||
private var autoSkippedFeedMediaId: String? = null
|
||||
private var clickCount = 0
|
||||
private val clickHandler = Handler(Looper.getMainLooper())
|
||||
// private var clickCount = 0
|
||||
// private val clickHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var isSpeedForward = false
|
||||
private var normalSpeed = 1.0f
|
||||
|
@ -145,7 +144,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
/**
|
||||
* Used for Lollipop notifications, Android Wear, and Android Auto.
|
||||
*/
|
||||
private var mediaSession: MediaSessionCompat? = null
|
||||
private var mediaSession: MediaSession? = null
|
||||
|
||||
private val mBinder: IBinder = LocalBinder()
|
||||
|
||||
|
@ -191,36 +190,14 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
fun recreateMediaSessionIfNeeded() {
|
||||
if (mediaSession != null) {
|
||||
// Media session was not destroyed, so we can re-use it.
|
||||
if (!mediaSession!!.isActive) {
|
||||
mediaSession!!.isActive = true
|
||||
}
|
||||
return
|
||||
}
|
||||
val eventReceiver = ComponentName(applicationContext, MediaButtonReceiver::class.java)
|
||||
val mediaButtonIntent = Intent(Intent.ACTION_MEDIA_BUTTON)
|
||||
mediaButtonIntent.setComponent(eventReceiver)
|
||||
val buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))
|
||||
if (mediaSession != null) return
|
||||
|
||||
mediaSession = MediaSessionCompat(applicationContext, TAG, eventReceiver, buttonReceiverIntent)
|
||||
sessionToken = mediaSession!!.sessionToken
|
||||
|
||||
try {
|
||||
mediaSession!!.setCallback(sessionCallback)
|
||||
mediaSession!!.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
||||
} catch (npe: NullPointerException) {
|
||||
// on some devices (Huawei) setting active can cause a NullPointerException
|
||||
// even with correct use of the api.
|
||||
// See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat
|
||||
// and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d
|
||||
Log.e(TAG, "NullPointerException while setting up MediaSession")
|
||||
npe.printStackTrace()
|
||||
}
|
||||
if (ExoPlayerWrapper.exoPlayer == null) ExoPlayerWrapper.createStaticPlayer(applicationContext)
|
||||
mediaSession = MediaSession.Builder(applicationContext, ExoPlayerWrapper.exoPlayer!!)
|
||||
.setCallback(sessionCallback)
|
||||
.build()
|
||||
|
||||
recreateMediaPlayer()
|
||||
mediaSession!!.isActive = true
|
||||
}
|
||||
|
||||
fun recreateMediaPlayer() {
|
||||
|
@ -234,7 +211,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback)
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = LocalPSMP(this, mediaPlayerCallback) // Cast not supported or not connected
|
||||
mediaPlayer = LocalPSMP(applicationContext, mediaPlayerCallback) // Cast not supported or not connected
|
||||
}
|
||||
if (media != null) {
|
||||
mediaPlayer!!.playMediaObject(media, !media.localFileAvailable(), wasPlaying, true)
|
||||
|
@ -271,8 +248,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
castStateListener.destroy()
|
||||
|
||||
cancelPositionObserver()
|
||||
mediaSession?.release()
|
||||
mediaSession = null
|
||||
mediaSession?.run {
|
||||
player.release()
|
||||
release()
|
||||
mediaSession = null
|
||||
}
|
||||
ExoPlayerWrapper.exoPlayer?.release()
|
||||
ExoPlayerWrapper.exoPlayer = null
|
||||
mediaPlayer?.shutdown()
|
||||
|
||||
unregisterReceiver(autoStateUpdated)
|
||||
|
@ -284,18 +266,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot {
|
||||
Log.d(TAG, "OnGetRoot: clientPackageName=" + clientPackageName +
|
||||
"; clientUid=" + clientUid + " ; rootHints=" + rootHints)
|
||||
if (rootHints != null && rootHints.getBoolean(BrowserRoot.EXTRA_RECENT)) {
|
||||
val extras = Bundle()
|
||||
extras.putBoolean(BrowserRoot.EXTRA_RECENT, true)
|
||||
Log.d(TAG, "OnGetRoot: Returning BrowserRoot " + R.string.current_playing_episode)
|
||||
return BrowserRoot(resources.getString(R.string.current_playing_episode), extras)
|
||||
}
|
||||
|
||||
// Name visible in Android Auto
|
||||
return BrowserRoot(resources.getString(R.string.app_name), null)
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun loadQueueForMediaSession() {
|
||||
|
@ -311,139 +283,141 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ queueItems: List<MediaSessionCompat.QueueItem>? -> mediaSession?.setQueue(queueItems) },
|
||||
.subscribe({ queueItems: List<MediaSessionCompat.QueueItem>? ->
|
||||
// mediaSession?.setQueue(queueItems)
|
||||
},
|
||||
{ obj: Throwable -> obj.printStackTrace() })
|
||||
}
|
||||
|
||||
private fun createBrowsableMediaItem(
|
||||
@StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int
|
||||
): MediaBrowserCompat.MediaItem {
|
||||
val uri = Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
.authority(resources.getResourcePackageName(icon))
|
||||
.appendPath(resources.getResourceTypeName(icon))
|
||||
.appendPath(resources.getResourceEntryName(icon))
|
||||
.build()
|
||||
// private fun createBrowsableMediaItem(
|
||||
// @StringRes title: Int, @DrawableRes icon: Int, numEpisodes: Int
|
||||
// ): MediaBrowserCompat.MediaItem {
|
||||
// val uri = Uri.Builder()
|
||||
// .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
// .authority(resources.getResourcePackageName(icon))
|
||||
// .appendPath(resources.getResourceTypeName(icon))
|
||||
// .appendPath(resources.getResourceEntryName(icon))
|
||||
// .build()
|
||||
//
|
||||
// val description = MediaDescriptionCompat.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)
|
||||
// }
|
||||
|
||||
val description = MediaDescriptionCompat.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)
|
||||
}
|
||||
// private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem {
|
||||
// val builder = MediaDescriptionCompat.Builder()
|
||||
// .setMediaId("FeedId:" + feed.id)
|
||||
// .setTitle(feed.title)
|
||||
// .setDescription(feed.description)
|
||||
// .setSubtitle(feed.getCustomTitle())
|
||||
// if (feed.imageUrl != null) {
|
||||
// builder.setIconUri(Uri.parse(feed.imageUrl))
|
||||
// }
|
||||
// if (feed.link != null) {
|
||||
// builder.setMediaUri(Uri.parse(feed.link))
|
||||
// }
|
||||
// val description = builder.build()
|
||||
// return MediaBrowserCompat.MediaItem(description,
|
||||
// MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
// }
|
||||
|
||||
private fun createBrowsableMediaItemForFeed(feed: Feed): MediaBrowserCompat.MediaItem {
|
||||
val builder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId("FeedId:" + feed.id)
|
||||
.setTitle(feed.title)
|
||||
.setDescription(feed.description)
|
||||
.setSubtitle(feed.getCustomTitle())
|
||||
if (feed.imageUrl != null) {
|
||||
builder.setIconUri(Uri.parse(feed.imageUrl))
|
||||
}
|
||||
if (feed.link != null) {
|
||||
builder.setMediaUri(Uri.parse(feed.link))
|
||||
}
|
||||
val description = builder.build()
|
||||
return MediaBrowserCompat.MediaItem(description,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
// override fun onLoadChildren(parentId: String,
|
||||
// result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||
// ) {
|
||||
// Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId")
|
||||
// result.detach()
|
||||
//
|
||||
// Completable.create { emitter: CompletableEmitter ->
|
||||
// result.sendResult(loadChildrenSynchronous(parentId))
|
||||
// emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// {}, { e: Throwable ->
|
||||
// e.printStackTrace()
|
||||
// result.sendResult(null)
|
||||
// })
|
||||
// }
|
||||
|
||||
override fun onLoadChildren(parentId: String,
|
||||
result: Result<List<MediaBrowserCompat.MediaItem>>
|
||||
) {
|
||||
Log.d(TAG, "OnLoadChildren: parentMediaId=$parentId")
|
||||
result.detach()
|
||||
// private fun loadChildrenSynchronous(parentId: String): List<MediaBrowserCompat.MediaItem>? {
|
||||
// val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
// if (parentId == resources.getString(R.string.app_name)) {
|
||||
// val currentlyPlaying = currentPlayerStatus.toLong()
|
||||
// if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING.toLong()
|
||||
// || currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED.toLong()) {
|
||||
// mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1))
|
||||
// }
|
||||
// mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black,
|
||||
// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.QUEUED))))
|
||||
// mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black,
|
||||
// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED))))
|
||||
// mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black,
|
||||
// DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.UNPLAYED))))
|
||||
// val feeds = DBReader.getFeedList()
|
||||
// for (feed in feeds) {
|
||||
// mediaItems.add(createBrowsableMediaItemForFeed(feed))
|
||||
// }
|
||||
// return mediaItems
|
||||
// }
|
||||
//
|
||||
// val feedItems: List<FeedItem?>
|
||||
// when {
|
||||
// parentId == resources.getString(R.string.queue_label) -> {
|
||||
// feedItems = DBReader.getQueue()
|
||||
// }
|
||||
// parentId == resources.getString(R.string.downloads_label) -> {
|
||||
// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
|
||||
// FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder)
|
||||
// }
|
||||
// parentId == resources.getString(R.string.episodes_label) -> {
|
||||
// feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
|
||||
// FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder)
|
||||
// }
|
||||
// parentId.startsWith("FeedId:") -> {
|
||||
// val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong()
|
||||
// val feed = DBReader.getFeed(feedId)
|
||||
// feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf()
|
||||
// }
|
||||
// parentId == getString(R.string.current_playing_episode) -> {
|
||||
// val playable = createInstanceFromPreferences(this)
|
||||
// if (playable is FeedMedia) {
|
||||
// feedItems = listOf(playable.item)
|
||||
// } else {
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
// else -> {
|
||||
// Log.e(TAG, "Parent ID not found: $parentId")
|
||||
// return null
|
||||
// }
|
||||
// }
|
||||
// var count = 0
|
||||
// for (feedItem in feedItems) {
|
||||
// if (feedItem?.media != null) {
|
||||
// mediaItems.add(feedItem.media!!.mediaItem)
|
||||
// if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return mediaItems
|
||||
// }
|
||||
|
||||
Completable.create { emitter: CompletableEmitter ->
|
||||
result.sendResult(loadChildrenSynchronous(parentId))
|
||||
emitter.onComplete()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{}, { e: Throwable ->
|
||||
e.printStackTrace()
|
||||
result.sendResult(null)
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadChildrenSynchronous(parentId: String): List<MediaBrowserCompat.MediaItem>? {
|
||||
val mediaItems: MutableList<MediaBrowserCompat.MediaItem> = ArrayList()
|
||||
if (parentId == resources.getString(R.string.app_name)) {
|
||||
val currentlyPlaying = currentPlayerStatus.toLong()
|
||||
if (currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PLAYING.toLong()
|
||||
|| currentlyPlaying == PlaybackPreferences.PLAYER_STATUS_PAUSED.toLong()) {
|
||||
mediaItems.add(createBrowsableMediaItem(R.string.current_playing_episode, R.drawable.ic_play_48dp, 1))
|
||||
}
|
||||
mediaItems.add(createBrowsableMediaItem(R.string.queue_label, R.drawable.ic_playlist_play_black,
|
||||
DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.QUEUED))))
|
||||
mediaItems.add(createBrowsableMediaItem(R.string.downloads_label, R.drawable.ic_download_black,
|
||||
DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED))))
|
||||
mediaItems.add(createBrowsableMediaItem(R.string.episodes_label, R.drawable.ic_feed_black,
|
||||
DBReader.getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.UNPLAYED))))
|
||||
val feeds = DBReader.getFeedList()
|
||||
for (feed in feeds) {
|
||||
mediaItems.add(createBrowsableMediaItemForFeed(feed))
|
||||
}
|
||||
return mediaItems
|
||||
}
|
||||
|
||||
val feedItems: List<FeedItem?>
|
||||
when {
|
||||
parentId == resources.getString(R.string.queue_label) -> {
|
||||
feedItems = DBReader.getQueue()
|
||||
}
|
||||
parentId == resources.getString(R.string.downloads_label) -> {
|
||||
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
|
||||
FeedItemFilter(FeedItemFilter.DOWNLOADED), downloadsSortedOrder)
|
||||
}
|
||||
parentId == resources.getString(R.string.episodes_label) -> {
|
||||
feedItems = DBReader.getEpisodes(0, MAX_ANDROID_AUTO_EPISODES_PER_FEED,
|
||||
FeedItemFilter(FeedItemFilter.UNPLAYED), allEpisodesSortOrder)
|
||||
}
|
||||
parentId.startsWith("FeedId:") -> {
|
||||
val feedId = parentId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].toLong()
|
||||
val feed = DBReader.getFeed(feedId)
|
||||
feedItems = if (feed != null) DBReader.getFeedItemList(feed, FeedItemFilter.unfiltered(), feed.sortOrder) else listOf()
|
||||
}
|
||||
parentId == getString(R.string.current_playing_episode) -> {
|
||||
val playable = createInstanceFromPreferences(this)
|
||||
if (playable is FeedMedia) {
|
||||
feedItems = listOf(playable.item)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.e(TAG, "Parent ID not found: $parentId")
|
||||
return null
|
||||
}
|
||||
}
|
||||
var count = 0
|
||||
for (feedItem in feedItems) {
|
||||
if (feedItem?.media != null) {
|
||||
mediaItems.add(feedItem.media!!.mediaItem)
|
||||
if (++count >= MAX_ANDROID_AUTO_EPISODES_PER_FEED) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return mediaItems
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
Log.d(TAG, "Received onBind event")
|
||||
return if (intent.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) {
|
||||
return if (intent?.action != null && TextUtils.equals(intent.action, SERVICE_INTERFACE)) {
|
||||
super.onBind(intent)
|
||||
} else {
|
||||
mBinder
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
Log.d(TAG, "OnStartCommand called")
|
||||
|
||||
|
@ -451,10 +425,10 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
val notificationManager = NotificationManagerCompat.from(this)
|
||||
notificationManager.cancel(R.id.notification_streaming_confirmation)
|
||||
|
||||
val keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1)
|
||||
val customAction = intent.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
||||
val hardwareButton = intent.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false)
|
||||
val playable = intent.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
|
||||
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
||||
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
||||
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
|
||||
val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
|
||||
if (keycode == -1 && playable == null && customAction == null) {
|
||||
Log.e(TAG, "PlaybackService was started with no arguments")
|
||||
stateManager.stopService()
|
||||
|
@ -483,10 +457,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
playable != null -> {
|
||||
stateManager.validStartCommandWasReceived()
|
||||
val allowStreamThisTime = intent.getBooleanExtra(
|
||||
PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false)
|
||||
val allowStreamAlways = intent.getBooleanExtra(
|
||||
PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false)
|
||||
val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false)
|
||||
val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false)
|
||||
sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0)
|
||||
if (allowStreamAlways) {
|
||||
isAllowMobileStreaming = true
|
||||
|
@ -510,7 +482,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
return START_NOT_STICKY
|
||||
}
|
||||
else -> {
|
||||
mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null)
|
||||
// mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -541,8 +513,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
@SuppressLint("LaunchActivityFromNotification")
|
||||
private fun displayStreamingNotAllowedNotification(originalIntent: Intent) {
|
||||
if (EventBus.getDefault().hasSubscriberForEvent(MessageEvent::class.java)) {
|
||||
EventBus.getDefault().post(MessageEvent(
|
||||
getString(R.string.confirm_mobile_streaming_notification_message)))
|
||||
EventBus.getDefault().post(MessageEvent(getString(R.string.confirm_mobile_streaming_notification_message)))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -550,24 +521,22 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
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.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)
|
||||
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.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)
|
||||
PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(this,
|
||||
|
@ -579,12 +548,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
.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)
|
||||
.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 = NotificationManagerCompat.from(this)
|
||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this,
|
||||
|
@ -725,8 +690,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
|
||||
private fun startPlayingFromPreferences() {
|
||||
Observable.fromCallable {
|
||||
createInstanceFromPreferences(
|
||||
applicationContext)
|
||||
createInstanceFromPreferences(applicationContext)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -856,8 +820,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
|
||||
TileService.requestListeningState(applicationContext,
|
||||
ComponentName(applicationContext, QuickSettingsTileService::class.java))
|
||||
TileService.requestListeningState(applicationContext, ComponentName(applicationContext, QuickSettingsTileService::class.java))
|
||||
}
|
||||
|
||||
sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED)
|
||||
|
@ -878,9 +841,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
updateNotificationAndMediaSession(this@PlaybackService.playable)
|
||||
}
|
||||
|
||||
override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean,
|
||||
playingNext: Boolean
|
||||
) {
|
||||
override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
this@PlaybackService.onPostPlayback(media, ended, skipped, playingNext)
|
||||
}
|
||||
|
||||
|
@ -1088,12 +1049,10 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
|
||||
if (media is FeedMedia) {
|
||||
if (ended || smartMarkAsPlayed) {
|
||||
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(
|
||||
applicationContext, media, true)
|
||||
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, true)
|
||||
media.onPlaybackCompleted(applicationContext)
|
||||
} else {
|
||||
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(
|
||||
applicationContext, media, false)
|
||||
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, media, false)
|
||||
media.onPlaybackPause(applicationContext)
|
||||
}
|
||||
}
|
||||
|
@ -1108,12 +1067,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
val action = item.feed?.preferences?.currentAutoDelete
|
||||
val shouldAutoDelete = (action == AutoDeleteAction.ALWAYS
|
||||
|| (action == AutoDeleteAction.GLOBAL && item.feed != null && shouldAutoDeleteItemsOnThatFeed(item.feed!!)))
|
||||
if (media is FeedMedia && shouldAutoDelete &&
|
||||
(!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) {
|
||||
if (media is FeedMedia && shouldAutoDelete && (!item.isTagged(FeedItem.TAG_FAVORITE) || !shouldFavoriteKeepEpisode())) {
|
||||
DBWriter.deleteFeedMediaOfItem(this@PlaybackService, media.id)
|
||||
Log.d(TAG, "Episode Deleted")
|
||||
}
|
||||
notifyChildrenChanged(getString(R.string.queue_label))
|
||||
// notifyChildrenChanged(getString(R.string.queue_label))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1153,13 +1111,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
val skipEndMS = skipEnd * 1000
|
||||
// Log.d(TAG, "skipEndingIfNecessary: checking " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed)
|
||||
if (skipEnd > 0 && skipEndMS < this.duration && (remainingTime - skipEndMS < 0)) {
|
||||
Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining " + remainingTime + " " + skipEndMS + " speed " + currentPlaybackSpeed)
|
||||
Log.d(TAG, "skipEndingIfNecessary: Skipping the remaining $remainingTime $skipEndMS speed $currentPlaybackSpeed")
|
||||
val context = applicationContext
|
||||
val skipMesg = context.getString(R.string.pref_feed_skip_ending_toast, skipEnd)
|
||||
val toast = Toast.makeText(context, skipMesg, Toast.LENGTH_LONG)
|
||||
toast.show()
|
||||
|
||||
this.autoSkippedFeedMediaId = item?.identifyingValue
|
||||
this.autoSkippedFeedMediaId = item.identifyingValue
|
||||
mediaPlayer?.skip()
|
||||
}
|
||||
}
|
||||
|
@ -1199,51 +1157,35 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
|
||||
// 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
|
||||
)
|
||||
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
|
||||
)
|
||||
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()
|
||||
)
|
||||
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())
|
||||
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()
|
||||
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())
|
||||
// mediaSession!!.setPlaybackState(sessionState.build())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1253,9 +1195,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
}
|
||||
|
||||
private fun updateMediaSessionMetadata(p: Playable?) {
|
||||
if (p == null || mediaSession == null) {
|
||||
return
|
||||
}
|
||||
if (p == null || mediaSession == null) return
|
||||
|
||||
val builder = MediaMetadataCompat.Builder()
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle())
|
||||
|
@ -1294,13 +1234,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
|
||||
getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
|
||||
or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0)))
|
||||
try {
|
||||
mediaSession!!.setMetadata(builder.build())
|
||||
} catch (e: OutOfMemoryError) {
|
||||
Log.e(TAG, "Setting media session metadata", e)
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null)
|
||||
mediaSession!!.setMetadata(builder.build())
|
||||
}
|
||||
// try {
|
||||
// mediaSession!!.setMetadata(builder.build())
|
||||
// } catch (e: OutOfMemoryError) {
|
||||
// Log.e(TAG, "Setting media session metadata", e)
|
||||
// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null)
|
||||
// mediaSession!!.setMetadata(builder.build())
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1327,7 +1267,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
|
||||
val playerStatus = mediaPlayer!!.playerStatus
|
||||
notificationBuilder.setPlayable(playable)
|
||||
if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.sessionToken)
|
||||
if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken())
|
||||
notificationBuilder.playerStatus = playerStatus
|
||||
notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
|
||||
|
||||
|
@ -1646,8 +1586,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
feedPreferences.feedPlaybackSpeed = speed
|
||||
Log.d(TAG, "setSpeed ${feed.title} $speed")
|
||||
DBWriter.setFeedPreferences(feedPreferences)
|
||||
EventBus.getDefault().post(
|
||||
SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id))
|
||||
EventBus.getDefault().post(SpeedPresetChangedEvent(feedPreferences.feedPlaybackSpeed, feed.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1776,188 +1715,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
if (playable is FeedMedia) {
|
||||
val itemId = playable.item?.id ?: return
|
||||
DBWriter.addQueueItem(this, false, true, itemId)
|
||||
notifyChildrenChanged(getString(R.string.queue_label))
|
||||
// notifyChildrenChanged(getString(R.string.queue_label))
|
||||
}
|
||||
}
|
||||
|
||||
private val sessionCallback: MediaSessionCompat.Callback = object : MediaSessionCompat.Callback() {
|
||||
private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback {
|
||||
private val TAG = "MediaSessionCompat"
|
||||
|
||||
override fun onPlay() {
|
||||
Log.d(TAG, "onPlay()")
|
||||
val status: PlayerStatus = this@PlaybackService.status
|
||||
when (status) {
|
||||
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
|
||||
resume()
|
||||
}
|
||||
PlayerStatus.INITIALIZED -> {
|
||||
this@PlaybackService.isStartWhenPrepared = true
|
||||
prepare()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayFromMediaId(mediaId: String, extras: Bundle) {
|
||||
Log.d(TAG, "onPlayFromMediaId: mediaId: $mediaId extras: $extras")
|
||||
val p = DBReader.getFeedMedia(mediaId.toLong())
|
||||
if (p != null) {
|
||||
startPlaying(p, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayFromSearch(query: String, extras: Bundle) {
|
||||
Log.d(TAG, "onPlayFromSearch query=$query extras=$extras")
|
||||
|
||||
if (query == "") {
|
||||
Log.d(TAG, "onPlayFromSearch called with empty query, resuming from the last position")
|
||||
startPlayingFromPreferences()
|
||||
return
|
||||
}
|
||||
|
||||
val results = FeedSearcher.searchFeedItems(query, 0)
|
||||
if (results.isNotEmpty() && results[0].media != null) {
|
||||
val media = results[0].media
|
||||
startPlaying(media, false)
|
||||
return
|
||||
}
|
||||
onPlay()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
Log.d(TAG, "onPause()")
|
||||
if (this@PlaybackService.status == PlayerStatus.PLAYING) {
|
||||
pause(!isPersistNotify, false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
Log.d(TAG, "onStop()")
|
||||
mediaPlayer?.stopPlayback(true)
|
||||
}
|
||||
|
||||
override fun onSkipToPrevious() {
|
||||
Log.d(TAG, "onSkipToPrevious()")
|
||||
seekDelta(-rewindSecs * 1000)
|
||||
}
|
||||
|
||||
override fun onRewind() {
|
||||
Log.d(TAG, "onRewind()")
|
||||
seekDelta(-rewindSecs * 1000)
|
||||
}
|
||||
|
||||
fun onNextChapter() {
|
||||
val chapters = mediaPlayer?.getPlayable()?.getChapters() ?: listOf()
|
||||
if (chapters.isEmpty()) {
|
||||
// No chapters, just fallback to next episode
|
||||
mediaPlayer?.skip()
|
||||
return
|
||||
}
|
||||
|
||||
val nextChapter = getCurrentChapterIndex(mediaPlayer?.getPlayable(), (mediaPlayer?.getPosition()?:0)) + 1
|
||||
|
||||
if (chapters.size < nextChapter + 1) {
|
||||
// We are on the last chapter, just fallback to the next episode
|
||||
mediaPlayer?.skip()
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayer?.seekTo(chapters[nextChapter].start.toInt())
|
||||
}
|
||||
|
||||
override fun onFastForward() {
|
||||
Log.d(TAG, "onFastForward()")
|
||||
// speedForward(2.5f)
|
||||
seekDelta(fastForwardSecs * 1000)
|
||||
}
|
||||
|
||||
override fun onSkipToNext() {
|
||||
Log.d(TAG, "onSkipToNext()")
|
||||
val uiModeManager = applicationContext.getSystemService(UI_MODE_SERVICE) as UiModeManager
|
||||
if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
|| uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
|
||||
mediaPlayer?.skip()
|
||||
} else {
|
||||
seekDelta(fastForwardSecs * 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onSeekTo(pos: Long) {
|
||||
Log.d(TAG, "onSeekTo()")
|
||||
seekTo(pos.toInt())
|
||||
}
|
||||
|
||||
override fun onSetPlaybackSpeed(speed: Float) {
|
||||
Log.d(TAG, "onSetPlaybackSpeed()")
|
||||
setSpeed(speed)
|
||||
}
|
||||
|
||||
override fun onMediaButtonEvent(mediaButton: Intent): Boolean {
|
||||
Log.d(TAG, "onMediaButtonEvent($mediaButton)")
|
||||
val keyEvent = mediaButton.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
|
||||
val keyCode = keyEvent.keyCode
|
||||
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
|
||||
clickCount++
|
||||
clickHandler.removeCallbacksAndMessages(null)
|
||||
clickHandler.postDelayed({
|
||||
when (clickCount) {
|
||||
1 -> {
|
||||
handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
|
||||
}
|
||||
2 -> {
|
||||
onFastForward()
|
||||
}
|
||||
3 -> {
|
||||
onRewind()
|
||||
}
|
||||
}
|
||||
clickCount = 0
|
||||
}, ViewConfiguration.getDoubleTapTimeout().toLong())
|
||||
return true
|
||||
} else {
|
||||
return handleKeycode(keyCode, false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onCustomAction(action: String, extra: Bundle) {
|
||||
Log.d(TAG, "onCustomAction($action)")
|
||||
when (action) {
|
||||
CUSTOM_ACTION_FAST_FORWARD -> {
|
||||
onFastForward()
|
||||
}
|
||||
CUSTOM_ACTION_REWIND -> {
|
||||
onRewind()
|
||||
}
|
||||
CUSTOM_ACTION_SKIP_TO_NEXT -> {
|
||||
mediaPlayer?.skip()
|
||||
}
|
||||
CUSTOM_ACTION_NEXT_CHAPTER -> {
|
||||
onNextChapter()
|
||||
}
|
||||
CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED -> {
|
||||
val selectedSpeeds = playbackSpeedArray
|
||||
|
||||
// If the list has zero or one element, there's nothing we can do to change the playback speed.
|
||||
if (selectedSpeeds.size > 1) {
|
||||
val speedPosition = selectedSpeeds.indexOf(mediaPlayer?.getPlaybackSpeed()?:0f)
|
||||
|
||||
val newSpeed = if (speedPosition == selectedSpeeds.size - 1) {
|
||||
// This is the last element. Wrap instead of going over the size of the list.
|
||||
selectedSpeeds[0]
|
||||
} else {
|
||||
// If speedPosition is still -1 (the user isn't using a preset), use the first preset in the
|
||||
// list.
|
||||
selectedSpeeds[speedPosition + 1]
|
||||
}
|
||||
onSetPlaybackSpeed(newSpeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: not used now with media3
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -1976,8 +1740,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
private const val CUSTOM_ACTION_SKIP_TO_NEXT = "action.ac.mdiq.podcini.service.skipToNext"
|
||||
private const val CUSTOM_ACTION_FAST_FORWARD = "action.ac.mdiq.podcini.service.fastForward"
|
||||
private const val CUSTOM_ACTION_REWIND = "action.ac.mdiq.podcini.service.rewind"
|
||||
private const val CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED =
|
||||
"action.ac.mdiq.podcini.service.changePlaybackSpeed"
|
||||
private const val CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED = "action.ac.mdiq.podcini.service.changePlaybackSpeed"
|
||||
const val CUSTOM_ACTION_NEXT_CHAPTER: String = "action.ac.mdiq.podcini.service.next_chapter"
|
||||
|
||||
/**
|
||||
|
@ -2030,15 +1793,16 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
|
||||
/**
|
||||
* Same as [.getPlayerActivityIntent], but here the type of activity
|
||||
* depends on the FeedMedia that is provided as an argument.
|
||||
* depends on the medaitype that is provided as an argument.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getPlayerActivityIntent(context: Context, media: Playable): Intent {
|
||||
return if (media.getMediaType() == MediaType.VIDEO && !isCasting) {
|
||||
fun getPlayerActivityIntent(context: Context, mediaType: MediaType?): Intent {
|
||||
return if (mediaType == MediaType.VIDEO && !isCasting) {
|
||||
VideoPlayerActivityStarter(context).intent
|
||||
} else {
|
||||
MainActivityStarter(context).withOpenPlayer().getIntent()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,43 +130,33 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
|
||||
private val playerActivityPendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity,
|
||||
PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT
|
||||
or PendingIntent.FLAG_IMMUTABLE)
|
||||
PlaybackService.getPlayerActivityIntent(context), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
private fun addActions(notification: NotificationCompat.Builder, mediaSessionToken: MediaSessionCompat.Token?,
|
||||
playerStatus: PlayerStatus?
|
||||
) {
|
||||
playerStatus: PlayerStatus?) {
|
||||
val compactActionList = ArrayList<Int>()
|
||||
|
||||
var numActions = 0 // we start and 0 and then increment by 1 for each call to addAction
|
||||
|
||||
val rewindButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_REWIND, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label),
|
||||
rewindButtonPendingIntent)
|
||||
val rewindButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_REWIND, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), rewindButtonPendingIntent)
|
||||
compactActionList.add(numActions)
|
||||
numActions++
|
||||
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
val pauseButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_pause, //pause action
|
||||
context.getString(R.string.pause_label),
|
||||
pauseButtonPendingIntent)
|
||||
val pauseButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PAUSE, numActions)
|
||||
//pause action
|
||||
notification.addAction(R.drawable.ic_notification_pause, context.getString(R.string.pause_label), pauseButtonPendingIntent)
|
||||
} else {
|
||||
val playButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_play, //play action
|
||||
context.getString(R.string.play_label),
|
||||
playButtonPendingIntent)
|
||||
val playButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PLAY, numActions)
|
||||
//play action
|
||||
notification.addAction(R.drawable.ic_notification_play, context.getString(R.string.play_label), playButtonPendingIntent)
|
||||
}
|
||||
compactActionList.add(numActions++)
|
||||
|
||||
// ff follows play, then we have skip (if it's present)
|
||||
val ffButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label),
|
||||
ffButtonPendingIntent)
|
||||
val ffButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), ffButtonPendingIntent)
|
||||
compactActionList.add(numActions)
|
||||
numActions++
|
||||
|
||||
|
@ -177,15 +167,12 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
}
|
||||
|
||||
if (UserPreferences.showSkipOnFullNotification()) {
|
||||
val skipButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label),
|
||||
skipButtonPendingIntent)
|
||||
val skipButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_NEXT, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), skipButtonPendingIntent)
|
||||
numActions++
|
||||
}
|
||||
|
||||
val stopButtonPendingIntent = getPendingIntentForMediaAction(
|
||||
KeyEvent.KEYCODE_MEDIA_STOP, numActions)
|
||||
val stopButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_STOP, numActions)
|
||||
notification.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSessionToken)
|
||||
.setShowActionsInCompactView(*ArrayUtils.toPrimitive(compactActionList.toTypedArray<Int>()))
|
||||
|
@ -199,11 +186,9 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -213,11 +198,9 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
intent.putExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION, action)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, requestCode, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
or PendingIntent.FLAG_IMMUTABLE)
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,8 +213,7 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
private var defaultIcon: Bitmap? = null
|
||||
|
||||
private fun getBitmap(vectorDrawable: VectorDrawable): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth,
|
||||
vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
vectorDrawable.draw(canvas)
|
||||
|
|
|
@ -7,8 +7,7 @@ import ac.mdiq.podcini.playback.base.PlayerStatus
|
|||
|
||||
internal class PlaybackVolumeUpdater {
|
||||
fun updateVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long,
|
||||
volumeAdaptionSetting: VolumeAdaptionSetting
|
||||
) {
|
||||
volumeAdaptionSetting: VolumeAdaptionSetting) {
|
||||
val playable = mediaPlayer.getPlayable()
|
||||
|
||||
if (playable is FeedMedia) {
|
||||
|
@ -17,8 +16,7 @@ internal class PlaybackVolumeUpdater {
|
|||
}
|
||||
|
||||
private fun updateFeedMediaVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long,
|
||||
volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia
|
||||
) {
|
||||
volumeAdaptionSetting: VolumeAdaptionSetting, feedMedia: FeedMedia) {
|
||||
if (feedMedia.item?.feed?.id == feedId) {
|
||||
val preferences = feedMedia.item!!.feed!!.preferences
|
||||
if (preferences != null) preferences.volumeAdaptionSetting = volumeAdaptionSetting
|
||||
|
|
|
@ -91,8 +91,7 @@ class PodDBAdapter private constructor() {
|
|||
feed.id = db.insert(TABLE_NAME_FEEDS, null, values)
|
||||
} else {
|
||||
Log.d(this.toString(), "Updating existing Feed in db")
|
||||
db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?",
|
||||
arrayOf(feed.id.toString()))
|
||||
db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(feed.id.toString()))
|
||||
}
|
||||
return feed.id
|
||||
}
|
||||
|
@ -116,14 +115,12 @@ class PodDBAdapter private constructor() {
|
|||
values.put(KEY_FEED_SKIP_ENDING, prefs.feedSkipEnding)
|
||||
values.put(KEY_EPISODE_NOTIFICATION, prefs.showEpisodeNotification)
|
||||
values.put(KEY_NEW_EPISODES_ACTION, prefs.newEpisodesAction!!.code)
|
||||
db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(
|
||||
prefs.feedID.toString()))
|
||||
db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(prefs.feedID.toString()))
|
||||
}
|
||||
|
||||
fun setFeedItemFilter(feedId: Long, filterValues: Set<String?>?) {
|
||||
val valuesList = TextUtils.join(",", filterValues!!)
|
||||
Log.d(TAG, String.format(Locale.US,
|
||||
"setFeedItemFilter() called with: feedId = [%d], filterValues = [%s]", feedId, valuesList))
|
||||
Log.d(TAG, String.format(Locale.US, "setFeedItemFilter() called with: feedId = [%d], filterValues = [%s]", feedId, valuesList))
|
||||
val values = ContentValues()
|
||||
values.put(KEY_HIDE, valuesList)
|
||||
db.update(TABLE_NAME_FEEDS, values, "$KEY_ID=?", arrayOf(feedId.toString()))
|
||||
|
@ -163,8 +160,7 @@ class PodDBAdapter private constructor() {
|
|||
if (media.id == 0L) {
|
||||
media.id = db.insert(TABLE_NAME_FEED_MEDIA, null, values)
|
||||
} else {
|
||||
db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?",
|
||||
arrayOf(media.id.toString()))
|
||||
db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", arrayOf(media.id.toString()))
|
||||
}
|
||||
return media.id
|
||||
}
|
||||
|
@ -176,8 +172,7 @@ class PodDBAdapter private constructor() {
|
|||
values.put(KEY_DURATION, media.getDuration())
|
||||
values.put(KEY_PLAYED_DURATION, media.playedDuration)
|
||||
values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime())
|
||||
db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?",
|
||||
arrayOf(media.id.toString()))
|
||||
db.update(TABLE_NAME_FEED_MEDIA, values, "$KEY_ID=?", arrayOf(media.id.toString()))
|
||||
} else {
|
||||
Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0")
|
||||
}
|
||||
|
@ -317,8 +312,7 @@ class PodDBAdapter private constructor() {
|
|||
if (item.id == 0L) {
|
||||
item.id = db.insert(TABLE_NAME_FEED_ITEMS, null, values)
|
||||
} else {
|
||||
db.update(TABLE_NAME_FEED_ITEMS, values, "$KEY_ID=?",
|
||||
arrayOf(item.id.toString()))
|
||||
db.update(TABLE_NAME_FEED_ITEMS, values, "$KEY_ID=?", arrayOf(item.id.toString()))
|
||||
}
|
||||
if (item.media != null) {
|
||||
setMedia(item.media)
|
||||
|
@ -329,9 +323,7 @@ class PodDBAdapter private constructor() {
|
|||
return item.id
|
||||
}
|
||||
|
||||
fun setFeedItemRead(played: Int, itemId: Long, mediaId: Long,
|
||||
resetMediaPosition: Boolean
|
||||
) {
|
||||
fun setFeedItemRead(played: Int, itemId: Long, mediaId: Long, resetMediaPosition: Boolean) {
|
||||
try {
|
||||
db.beginTransactionNonExclusive()
|
||||
val values = ContentValues()
|
||||
|
@ -387,8 +379,7 @@ class PodDBAdapter private constructor() {
|
|||
if (chapter.id == 0L) {
|
||||
chapter.id = db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values)
|
||||
} else {
|
||||
db.update(TABLE_NAME_SIMPLECHAPTERS, values, "$KEY_ID=?",
|
||||
arrayOf(chapter.id.toString()))
|
||||
db.update(TABLE_NAME_SIMPLECHAPTERS, values, "$KEY_ID=?", arrayOf(chapter.id.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -428,8 +419,7 @@ class PodDBAdapter private constructor() {
|
|||
if (status.id == 0L) {
|
||||
status.id = db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values)
|
||||
} else {
|
||||
db.update(TABLE_NAME_DOWNLOAD_LOG, values, "$KEY_ID=?",
|
||||
arrayOf(status.id.toString()))
|
||||
db.update(TABLE_NAME_DOWNLOAD_LOG, values, "$KEY_ID=?", arrayOf(status.id.toString()))
|
||||
}
|
||||
return status.id
|
||||
}
|
||||
|
@ -470,16 +460,12 @@ class PodDBAdapter private constructor() {
|
|||
}
|
||||
|
||||
fun removeFavoriteItem(item: FeedItem) {
|
||||
val deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s",
|
||||
TABLE_NAME_FAVORITES,
|
||||
KEY_FEEDITEM, item.id,
|
||||
KEY_FEED, item.feedId)
|
||||
val deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s", TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id, KEY_FEED, item.feedId)
|
||||
db.execSQL(deleteClause)
|
||||
}
|
||||
|
||||
private fun isItemInFavorites(item: FeedItem): Boolean {
|
||||
val query = String.format(Locale.US, "SELECT %s from %s WHERE %s=%d",
|
||||
KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id)
|
||||
val query = String.format(Locale.US, "SELECT %s from %s WHERE %s=%d", KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.id)
|
||||
val c = db.rawQuery(query, null)
|
||||
val count = c.count
|
||||
c.close()
|
||||
|
@ -557,8 +543,7 @@ class PodDBAdapter private constructor() {
|
|||
db.delete(TABLE_NAME_DOWNLOAD_LOG, "$KEY_FEEDFILE=? AND $KEY_FEEDFILETYPE=?",
|
||||
arrayOf(feed.id.toString(), Feed.FEEDFILETYPE_FEED.toString()))
|
||||
|
||||
db.delete(TABLE_NAME_FEEDS, "$KEY_ID=?",
|
||||
arrayOf(feed.id.toString()))
|
||||
db.delete(TABLE_NAME_FEEDS, "$KEY_ID=?", arrayOf(feed.id.toString()))
|
||||
db.setTransactionSuccessful()
|
||||
} catch (e: SQLException) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
|
@ -717,8 +702,7 @@ class PodDBAdapter private constructor() {
|
|||
val orderByQuery = generateFrom(sortOrder)
|
||||
val filterQuery = generateFrom(filter!!)
|
||||
val whereClause = if ("" == filterQuery) "" else " WHERE $filterQuery"
|
||||
val query = (SELECT_FEED_ITEMS_AND_MEDIA + whereClause
|
||||
+ "ORDER BY " + orderByQuery + " LIMIT " + offset + ", " + limit)
|
||||
val query = (SELECT_FEED_ITEMS_AND_MEDIA + whereClause + "ORDER BY " + orderByQuery + " LIMIT " + offset + ", " + limit)
|
||||
return db.rawQuery(query, null)
|
||||
}
|
||||
|
||||
|
@ -771,8 +755,7 @@ class PodDBAdapter private constructor() {
|
|||
get() = DatabaseUtils.queryNumEntries(db, TABLE_NAME_FEED_MEDIA, "$KEY_PLAYBACK_COMPLETION_DATE> 0")
|
||||
|
||||
fun getSingleFeedMediaCursor(id: Long): Cursor {
|
||||
val query = ("SELECT " + KEYS_FEED_MEDIA + " FROM " + TABLE_NAME_FEED_MEDIA
|
||||
+ " WHERE " + KEY_ID + "=" + id)
|
||||
val query = ("SELECT " + KEYS_FEED_MEDIA + " FROM " + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_ID + "=" + id)
|
||||
return db.rawQuery(query, null)
|
||||
}
|
||||
|
||||
|
@ -918,8 +901,7 @@ class PodDBAdapter private constructor() {
|
|||
fun getFeedCounters(setting: FeedCounter?, vararg feedIds: Long): Map<Long, Int> {
|
||||
val whereRead = when (setting) {
|
||||
// FeedCounter.SHOW_NEW -> KEY_READ + "=" + FeedItem.NEW
|
||||
FeedCounter.SHOW_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW
|
||||
+ " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")")
|
||||
FeedCounter.SHOW_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")")
|
||||
FeedCounter.SHOW_DOWNLOADED -> "$KEY_DOWNLOADED=1"
|
||||
FeedCounter.SHOW_DOWNLOADED_UNPLAYED -> ("(" + KEY_READ + "=" + FeedItem.NEW
|
||||
+ " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")"
|
||||
|
@ -1044,13 +1026,11 @@ class PodDBAdapter private constructor() {
|
|||
"1 = 1"
|
||||
}
|
||||
|
||||
val queryStart = (SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION
|
||||
+ " WHERE " + queryFeedId + " AND (")
|
||||
val queryStart = (SELECT_FEED_ITEMS_AND_MEDIA_WITH_DESCRIPTION + " WHERE " + queryFeedId + " AND (")
|
||||
val sb = StringBuilder(queryStart)
|
||||
|
||||
for (i in queryWords.indices) {
|
||||
sb
|
||||
.append("(")
|
||||
sb.append("(")
|
||||
.append("$KEY_DESCRIPTION LIKE '%").append(queryWords[i])
|
||||
.append("%' OR ")
|
||||
.append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
|
||||
|
@ -1078,8 +1058,7 @@ class PodDBAdapter private constructor() {
|
|||
val sb = StringBuilder(queryStart)
|
||||
|
||||
for (i in queryWords.indices) {
|
||||
sb
|
||||
.append("(")
|
||||
sb.append("(")
|
||||
.append(KEY_TITLE).append(" LIKE '%").append(queryWords[i])
|
||||
.append("%' OR ")
|
||||
.append(KEY_CUSTOM_TITLE).append(" LIKE '%").append(queryWords[i])
|
||||
|
|
|
@ -3,9 +3,9 @@ package ac.mdiq.podcini.ui.actions.actionbutton
|
|||
import android.content.Context
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.storage.DBTasks
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.playback.MediaType
|
||||
import ac.mdiq.podcini.util.event.playback.StartPlayEvent
|
||||
|
@ -33,7 +33,7 @@ class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
|
|||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class PlayLocalActionButton(item: FeedItem?) : ItemActionButton(item!!) {
|
|||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ class StreamActionButton(item: FeedItem) : ItemActionButton(item) {
|
|||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
context.startActivity(getPlayerActivityIntent(context, MediaType.VIDEO))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,9 +85,8 @@ object FeedItemMenuHandler {
|
|||
* @param visibility The new visibility status of given menu item
|
||||
*/
|
||||
private fun setItemVisibility(menu: Menu?, menuId: Int, visibility: Boolean) {
|
||||
if (menu == null) {
|
||||
return
|
||||
}
|
||||
if (menu == null) return
|
||||
|
||||
val item = menu.findItem(menuId)
|
||||
item?.setVisible(visibility)
|
||||
}
|
||||
|
@ -112,9 +111,8 @@ object FeedItemMenuHandler {
|
|||
*/
|
||||
@UnstableApi
|
||||
fun onPrepareMenu(menu: Menu?, selectedItem: FeedItem?, vararg excludeIds: Int): Boolean {
|
||||
if (menu == null || selectedItem == null) {
|
||||
return false
|
||||
}
|
||||
if (menu == null || selectedItem == null) return false
|
||||
|
||||
val rc = onPrepareMenu(menu, selectedItem)
|
||||
if (rc && excludeIds.isNotEmpty()) {
|
||||
for (id in excludeIds) {
|
||||
|
|
|
@ -274,6 +274,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
Log.d(TAG, "bottomSheet onStateChanged $state")
|
||||
when (state) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
onSlide(view,0.0f)
|
||||
|
@ -610,8 +611,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false) -> {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
// bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
// bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
else -> {
|
||||
handleDeeplink(intent.data)
|
||||
|
|
|
@ -40,39 +40,45 @@ import org.greenrobot.eventbus.EventBus
|
|||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
||||
|
||||
/**
|
||||
* Activity for playing video files.
|
||||
*/
|
||||
@UnstableApi
|
||||
class VideoplayerActivity : CastEnabledActivity() {
|
||||
|
||||
enum class VideoMode(val mode: Int) {
|
||||
None(0,),
|
||||
WINDOW_VIEW(1),
|
||||
FULL_SCREEN_VIEW(2),
|
||||
AUDIO_ONLY(3)
|
||||
}
|
||||
|
||||
private var _binding: VideoplayerActivityBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
lateinit var videoEpisodeFragment: VideoEpisodeFragment
|
||||
|
||||
var videoMode = 0
|
||||
var switchToAudioOnly = false
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
|
||||
videoMode = intent.getIntExtra("fullScreenMode",0)
|
||||
if (videoMode == 0) {
|
||||
videoMode = videoPlayMode
|
||||
if (videoMode == AUDIO_ONLY) {
|
||||
videoMode = (intent.getSerializableExtra(VIDEO_MODE) as? VideoMode) ?: VideoMode.None
|
||||
if (videoMode == VideoMode.None) {
|
||||
videoMode = VideoMode.entries.toTypedArray().getOrElse(videoPlayMode) { VideoMode.WINDOW_VIEW }
|
||||
if (videoMode == VideoMode.AUDIO_ONLY) {
|
||||
switchToAudioOnly = true
|
||||
finish()
|
||||
}
|
||||
if (videoMode != FULL_SCREEN_VIEW && videoMode != WINDOW_VIEW) {
|
||||
if (videoMode != VideoMode.FULL_SCREEN_VIEW && videoMode != VideoMode.WINDOW_VIEW) {
|
||||
Log.i(TAG, "videoMode not selected, use window mode")
|
||||
videoMode = WINDOW_VIEW
|
||||
videoMode = VideoMode.WINDOW_VIEW
|
||||
}
|
||||
}
|
||||
|
||||
when (videoMode) {
|
||||
FULL_SCREEN_VIEW -> {
|
||||
VideoMode.FULL_SCREEN_VIEW -> {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
// has to be called before setting layout content
|
||||
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
|
||||
|
@ -81,13 +87,14 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
}
|
||||
WINDOW_VIEW -> {
|
||||
VideoMode.WINDOW_VIEW -> {
|
||||
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
|
||||
setTheme(R.style.Theme_Podcini_VideoEpisode)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
@ -156,7 +163,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
|
||||
fun toggleViews() {
|
||||
val newIntent = Intent(this, VideoplayerActivity::class.java)
|
||||
newIntent.putExtra("fullScreenMode", if (videoMode == FULL_SCREEN_VIEW) WINDOW_VIEW else FULL_SCREEN_VIEW)
|
||||
newIntent.putExtra(VIDEO_MODE, if (videoMode == VideoMode.FULL_SCREEN_VIEW) VideoMode.WINDOW_VIEW else VideoMode.FULL_SCREEN_VIEW)
|
||||
finish()
|
||||
startActivity(newIntent)
|
||||
}
|
||||
|
@ -233,7 +240,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
menu.findItem(R.id.playback_speed).setVisible(true)
|
||||
menu.findItem(R.id.player_show_chapters).setVisible(true)
|
||||
|
||||
if (videoMode == WINDOW_VIEW) {
|
||||
if (videoMode == VideoMode.WINDOW_VIEW) {
|
||||
menu.findItem(R.id.add_to_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
menu.findItem(R.id.set_sleeptimer_item).setShowAsAction(SHOW_AS_ACTION_NEVER)
|
||||
|
@ -316,7 +323,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
|
||||
private fun compatEnterPictureInPicture() {
|
||||
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
if (videoMode == FULL_SCREEN_VIEW) supportActionBar?.hide()
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) supportActionBar?.hide()
|
||||
videoEpisodeFragment.hideVideoControls(false)
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
|
@ -381,9 +388,9 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
|
||||
companion object {
|
||||
private const val TAG = "VideoplayerActivity"
|
||||
const val WINDOW_VIEW = 1
|
||||
const val FULL_SCREEN_VIEW = 2
|
||||
const val AUDIO_ONLY = 3
|
||||
const val VIDEO_MODE = "Video_Mode"
|
||||
|
||||
var videoMode = VideoMode.None
|
||||
|
||||
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
||||
return when {
|
||||
|
|
|
@ -2,21 +2,25 @@ package ac.mdiq.podcini.ui.activity.appstartintent
|
|||
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.VIDEO_MODE
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
/**
|
||||
* Launches the video player activity of the app with specific arguments.
|
||||
* Does not require a dependency on the actual implementation of the activity.
|
||||
*/
|
||||
class VideoPlayerActivityStarter(private val context: Context) {
|
||||
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
|
||||
val intent: Intent = Intent(INTENT)
|
||||
|
||||
init {
|
||||
intent.setPackage(context.packageName)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
|
||||
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent
|
||||
|
|
|
@ -6,36 +6,35 @@ 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.PlaybackController.Companion
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
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.actions.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
|
||||
import ac.mdiq.podcini.ui.activity.appstartintent.VideoPlayerActivityStarter
|
||||
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.actions.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.AUDIO_ONLY
|
||||
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||
import ac.mdiq.podcini.ui.view.PlayButton
|
||||
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
|
||||
import ac.mdiq.podcini.util.ChapterUtils
|
||||
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.playback.*
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
|
@ -114,10 +113,13 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
toolbar = binding.toolbar
|
||||
toolbar.title = ""
|
||||
toolbar.setNavigationOnClickListener {
|
||||
val bottomSheet = (activity as MainActivity).bottomSheet
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED)
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
else bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
// val mtype = controller?.getMedia()?.getMediaType()
|
||||
// if (mtype == MediaType.AUDIO || (mtype == MediaType.VIDEO && videoPlayMode == VideoMode.AUDIO_ONLY)) {
|
||||
val bottomSheet = (activity as MainActivity).bottomSheet
|
||||
// if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED)
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
// else bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
// }
|
||||
}
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
|
@ -400,6 +402,9 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
if (item == null && isFeedMedia) item = (media as FeedMedia).item
|
||||
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item)
|
||||
|
||||
val mediaType = controller?.getMedia()?.getMediaType()
|
||||
toolbar.menu?.findItem(R.id.show_video)?.setVisible(mediaType == MediaType.VIDEO)
|
||||
|
||||
if (controller != null) {
|
||||
toolbar.menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive())
|
||||
toolbar.menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive())
|
||||
|
@ -419,6 +424,11 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
|
||||
val itemId = menuItem.itemId
|
||||
when (itemId) {
|
||||
R.id.show_video -> {
|
||||
controller!!.playPause()
|
||||
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
||||
return true
|
||||
}
|
||||
R.id.disable_sleeptimer_item, R.id.set_sleeptimer_item -> {
|
||||
SleepTimerDialog().show(childFragmentManager, "SleepTimerDialog")
|
||||
return true
|
||||
|
@ -523,13 +533,15 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
Log.d(TAG, "internalPlayerFragment was clicked")
|
||||
val media = controller?.getMedia()
|
||||
if (media != null) {
|
||||
if (media.getMediaType() == MediaType.AUDIO || videoPlayMode == AUDIO_ONLY) {
|
||||
val mediaType = media.getMediaType()
|
||||
if (mediaType == MediaType.AUDIO ||
|
||||
(mediaType == MediaType.VIDEO && (videoPlayMode == VideoMode.AUDIO_ONLY.mode || videoMode == VideoMode.AUDIO_ONLY))) {
|
||||
controller!!.ensureService()
|
||||
(activity as MainActivity).bottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED)
|
||||
} else {
|
||||
controller?.playPause()
|
||||
// controller!!.ensureService()
|
||||
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), media)
|
||||
val intent = PlaybackService.getPlayerActivityIntent(requireContext(), mediaType)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -554,7 +566,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
val media = controller!!.getMedia()
|
||||
if (media?.getMediaType() == MediaType.VIDEO && controller!!.status != PlayerStatus.PLAYING) {
|
||||
controller!!.playPause()
|
||||
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media))
|
||||
requireContext().startActivity(PlaybackService.getPlayerActivityIntent(requireContext(), media?.getMediaType()))
|
||||
} else {
|
||||
controller!!.playPause()
|
||||
}
|
||||
|
@ -569,8 +581,7 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
}
|
||||
}
|
||||
butRev.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(),
|
||||
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, txtvRev)
|
||||
true
|
||||
}
|
||||
butPlay.setOnClickListener {
|
||||
|
|
|
@ -0,0 +1,234 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.app.ShareCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import io.reactivex.disposables.Disposable
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Displays information about an Episode (FeedItem) and actions.
|
||||
*/
|
||||
class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToSpeech.OnInitListener {
|
||||
private var _binding: EpisodeHomeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var item: FeedItem? = null
|
||||
|
||||
private lateinit var tts: TextToSpeech
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
private var readerhtml: String? = null
|
||||
private var textContent: 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)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
||||
_binding = EpisodeHomeFragmentBinding.inflate(inflater, container, false)
|
||||
Log.d(TAG, "fragment onCreateView")
|
||||
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = ""
|
||||
toolbar.inflateMenu(R.menu.episode_home)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
if (item?.link != null) {
|
||||
showContent()
|
||||
}
|
||||
updateAppearance()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun switchMode() {
|
||||
readMode = !readMode
|
||||
showContent()
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
override fun onInit(status: Int) {
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
// TTS initialization successful
|
||||
Log.i(TAG, "TTS init success with Locale: ${item?.feed?.language}")
|
||||
if (item?.feed?.language != null) {
|
||||
val result = tts.setLanguage(Locale(item!!.feed!!.language))
|
||||
// val result = tts.setLanguage(Locale.UK)
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TTS initialization failed
|
||||
// Handle the error or fallback to default behavior
|
||||
Log.w(TAG, "TTS init failed")
|
||||
}
|
||||
}
|
||||
|
||||
private fun showContent() {
|
||||
if (readMode) {
|
||||
if (readerhtml == null) {
|
||||
runBlocking {
|
||||
val url = item!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val readability4J = Readability4J(item?.link!!, htmlSource)
|
||||
val article = readability4J.parse()
|
||||
textContent = article.textContent
|
||||
// Log.d(TAG, "readability4J: ${article.textContent}")
|
||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
}
|
||||
}
|
||||
if (readerhtml != null) binding.webView.loadDataWithBaseURL(item!!.link!!, readerhtml!!, "text/html", "UTF-8", null)
|
||||
} else {
|
||||
if (item?.link != null) binding.webView.loadUrl(item!!.link!!)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchHtmlSource(urlString: String): String = withContext(Dispatchers.IO) {
|
||||
val url = URL(urlString)
|
||||
val connection = url.openConnection()
|
||||
val inputStream = connection.getInputStream()
|
||||
val bufferedReader = BufferedReader(InputStreamReader(inputStream))
|
||||
|
||||
val stringBuilder = StringBuilder()
|
||||
var line: String?
|
||||
while (bufferedReader.readLine().also { line = it } != null) {
|
||||
stringBuilder.append(line)
|
||||
}
|
||||
|
||||
bufferedReader.close()
|
||||
inputStream.close()
|
||||
|
||||
stringBuilder.toString()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val textSpeech = menu.findItem(R.id.text_speech)
|
||||
textSpeech.isVisible = readMode
|
||||
if (readMode) {
|
||||
if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause)
|
||||
else textSpeech.setIcon(R.drawable.ic_play_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi override fun onMenuItemClick(menuItem: MenuItem): Boolean {
|
||||
when (menuItem.itemId) {
|
||||
R.id.switch_home -> {
|
||||
Log.d(TAG, "switch_home selected")
|
||||
switchMode()
|
||||
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)
|
||||
}
|
||||
}
|
||||
} else ttsPlaying = false
|
||||
|
||||
updateAppearance()
|
||||
return true
|
||||
}
|
||||
R.id.share_notes -> {
|
||||
if (item == null) return false
|
||||
val notes = item!!.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()
|
||||
val context = requireContext()
|
||||
val intent = ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setText(shareText)
|
||||
.setChooserTitle(R.string.share_notes_label)
|
||||
.createChooserIntent()
|
||||
context.startActivity(intent)
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
if (item == null) return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi override fun onResume() {
|
||||
super.onResume()
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
Log.d(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
disposable?.dispose()
|
||||
tts.shutdown()
|
||||
}
|
||||
|
||||
@UnstableApi private fun updateAppearance() {
|
||||
if (item == null) {
|
||||
Log.d(TAG, "updateAppearance item is null")
|
||||
return
|
||||
}
|
||||
onPrepareOptionsMenu(toolbar.menu)
|
||||
// FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.switch_home)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EpisodeWebviewFragment"
|
||||
private const val ARG_FEEDITEM = "feeditem"
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(item: FeedItem): EpisodeHomeFragment {
|
||||
val fragment = EpisodeHomeFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(ARG_FEEDITEM, item)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,12 +85,9 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
private lateinit var imgvCover: ImageView
|
||||
private lateinit var progbarDownload: CircularProgressBar
|
||||
private lateinit var progbarLoading: ProgressBar
|
||||
private lateinit var butAction1Text: TextView
|
||||
private lateinit var butAction2Text: TextView
|
||||
private lateinit var butAction1Icon: ImageView
|
||||
private lateinit var butAction2Icon: ImageView
|
||||
private lateinit var butAction1: View
|
||||
private lateinit var butAction2: View
|
||||
private lateinit var butAction0: View
|
||||
private lateinit var butAction1: ImageView
|
||||
private lateinit var butAction2: ImageView
|
||||
private lateinit var noMediaLabel: View
|
||||
|
||||
private var actionButton1: ItemActionButton? = null
|
||||
|
@ -142,14 +139,15 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
imgvCover.setOnClickListener { openPodcast() }
|
||||
progbarDownload = binding.circularProgressBar
|
||||
progbarLoading = binding.progbarLoading
|
||||
butAction0 = binding.butAction0
|
||||
butAction1 = binding.butAction1
|
||||
butAction2 = binding.butAction2
|
||||
butAction1Icon = binding.butAction1Icon
|
||||
butAction2Icon = binding.butAction2Icon
|
||||
butAction1Text = binding.butAction1Text
|
||||
butAction2Text = binding.butAction2Text
|
||||
noMediaLabel = binding.noMediaLabel
|
||||
|
||||
butAction0.setOnClickListener(View.OnClickListener {
|
||||
if (item?.link != null) (activity as MainActivity).loadChildFragment(EpisodeHomeFragment.newInstance(item!!))
|
||||
})
|
||||
|
||||
butAction1.setOnClickListener(View.OnClickListener {
|
||||
when {
|
||||
actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload
|
||||
|
@ -272,6 +270,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (webviewData != null && !itemsLoaded) {
|
||||
webvDescription.loadDataWithBaseURL("https://127.0.0.1", webviewData!!, "text/html", "utf-8", "about:blank")
|
||||
}
|
||||
// if (item?.link != null) binding.webView.loadUrl(item!!.link!!)
|
||||
updateAppearance()
|
||||
}
|
||||
|
||||
|
@ -298,8 +297,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
val options: RequestOptions = RequestOptions()
|
||||
.error(R.color.light_gray)
|
||||
.transform(FitCenter(),
|
||||
RoundedCorners((8 * resources.displayMetrics.density).toInt()))
|
||||
.transform(FitCenter(), RoundedCorners((8 * resources.displayMetrics.density).toInt()))
|
||||
.dontAnimate()
|
||||
|
||||
val imgLocFB = ImageResourceUtils.getFallbackImageLocation(item!!)
|
||||
|
@ -337,8 +335,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
noMediaLabel.visibility = View.GONE
|
||||
if (media.getDuration() > 0) {
|
||||
txtvDuration.text = Converter.getDurationStringLong(media.getDuration())
|
||||
txtvDuration.setContentDescription(
|
||||
Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||
txtvDuration.setContentDescription(Converter.getDurationStringLocalized(requireContext(), media.getDuration().toLong()))
|
||||
}
|
||||
if (item != null) {
|
||||
actionButton1 = when {
|
||||
|
@ -377,17 +374,17 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
if (actionButton1 != null) {
|
||||
butAction1Text.setText(actionButton1!!.getLabel())
|
||||
butAction1Icon.setImageResource(actionButton1!!.getDrawable())
|
||||
// butAction1Text.setText(actionButton1!!.getLabel())
|
||||
butAction1.setImageResource(actionButton1!!.getDrawable())
|
||||
}
|
||||
butAction1Text.transformationMethod = null
|
||||
// butAction1Text.transformationMethod = null
|
||||
if (actionButton1 != null) butAction1.visibility = actionButton1!!.visibility
|
||||
|
||||
if (actionButton2 != null) {
|
||||
butAction2Text.setText(actionButton2!!.getLabel())
|
||||
butAction2Icon.setImageResource(actionButton2!!.getDrawable())
|
||||
// butAction2Text.setText(actionButton2!!.getLabel())
|
||||
butAction2.setImageResource(actionButton2!!.getDrawable())
|
||||
}
|
||||
butAction2Text.transformationMethod = null
|
||||
// butAction2Text.transformationMethod = null
|
||||
if (actionButton2 != null) butAction2.visibility = actionButton2!!.visibility
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.PlayerDetailsFragmentBinding
|
||||
import ac.mdiq.podcini.feed.util.ImageResourceUtils
|
||||
import ac.mdiq.podcini.playback.PlaybackController
|
||||
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.model.feed.Chapter
|
||||
import ac.mdiq.podcini.storage.model.feed.EmbeddedChapterImage
|
||||
|
@ -16,6 +15,7 @@ import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
|||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.util.ChapterUtils
|
||||
import ac.mdiq.podcini.util.DateFormatter
|
||||
import ac.mdiq.podcini.util.event.playback.PlaybackPositionEvent
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.AnimatorSet
|
||||
|
@ -44,6 +44,7 @@ import com.bumptech.glide.RequestBuilder
|
|||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.MaybeEmitter
|
||||
|
@ -212,13 +213,9 @@ class PlayerDetailsFragment : Fragment() {
|
|||
val lines = binding.txtvEpisodeTitle.lineCount
|
||||
val animUnit = 1500
|
||||
if (lines > binding.txtvEpisodeTitle.maxLines) {
|
||||
val titleHeight = (binding.txtvEpisodeTitle.height
|
||||
- binding.txtvEpisodeTitle.paddingTop
|
||||
- binding.txtvEpisodeTitle.paddingBottom)
|
||||
val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(
|
||||
binding.txtvEpisodeTitle, "scrollY", 0,
|
||||
(lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines))
|
||||
.setDuration((lines * animUnit).toLong())
|
||||
val titleHeight = (binding.txtvEpisodeTitle.height - binding.txtvEpisodeTitle.paddingTop - binding.txtvEpisodeTitle.paddingBottom)
|
||||
val verticalMarquee: ObjectAnimator = ObjectAnimator.ofInt(binding.txtvEpisodeTitle, "scrollY", 0,
|
||||
(lines - binding.txtvEpisodeTitle.maxLines) * (titleHeight / binding.txtvEpisodeTitle.maxLines)).setDuration((lines * animUnit).toLong())
|
||||
val fadeOut: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 0f)
|
||||
fadeOut.setStartDelay(animUnit.toLong())
|
||||
fadeOut.addListener(object : AnimatorListenerAdapter() {
|
||||
|
@ -254,11 +251,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
val newVisibility = if (chapterControlVisible) View.VISIBLE else View.GONE
|
||||
if (binding.chapterButton.visibility != newVisibility) {
|
||||
binding.chapterButton.visibility = newVisibility
|
||||
ObjectAnimator.ofFloat(binding.chapterButton,
|
||||
"alpha",
|
||||
(if (chapterControlVisible) 0 else 1).toFloat(),
|
||||
(if (chapterControlVisible) 1 else 0).toFloat())
|
||||
.start()
|
||||
ObjectAnimator.ofFloat(binding.chapterButton, "alpha",
|
||||
(if (chapterControlVisible) 0 else 1).toFloat(), (if (chapterControlVisible) 1 else 0).toFloat()).start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,8 +273,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
if (media == null) return
|
||||
val options: RequestOptions = RequestOptions()
|
||||
.dontAnimate()
|
||||
.transform(FitCenter(),
|
||||
RoundedCorners((16 * resources.displayMetrics.density).toInt()))
|
||||
.transform(FitCenter(), RoundedCorners((16 * resources.displayMetrics.density).toInt()))
|
||||
|
||||
val cover: RequestBuilder<Drawable> = Glide.with(this)
|
||||
.load(media!!.getImageLocation())
|
||||
|
@ -308,9 +301,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
private val currentChapter: Chapter?
|
||||
get() {
|
||||
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) {
|
||||
return null
|
||||
}
|
||||
if (media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1) return null
|
||||
return media!!.getChapters()[displayedChapterIndex]
|
||||
}
|
||||
|
||||
|
@ -364,6 +355,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
|
||||
@UnstableApi private fun restoreFromPreference(): Boolean {
|
||||
if ((activity as MainActivity).bottomSheet.state != BottomSheetBehavior.STATE_EXPANDED) return false
|
||||
|
||||
Log.d(TAG, "Restoring from preferences")
|
||||
val activity: Activity? = activity
|
||||
if (activity != null) {
|
||||
|
@ -411,18 +404,14 @@ class PlayerDetailsFragment : Fragment() {
|
|||
// private fun configureForOrientation(newConfig: Configuration) {
|
||||
// val isPortrait = newConfig.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
//
|
||||
// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
|
||||
//// binding.coverFragment.orientation = if (isPortrait) LinearLayout.VERTICAL else LinearLayout.HORIZONTAL
|
||||
//
|
||||
// if (isPortrait) {
|
||||
// binding.coverHolder.layoutParams =
|
||||
// LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||
// binding.coverFragmentTextContainer.layoutParams =
|
||||
// LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)
|
||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
// } else {
|
||||
// binding.coverHolder.layoutParams =
|
||||
// LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
// binding.coverFragmentTextContainer.layoutParams =
|
||||
// LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
// binding.coverHolder.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
//// binding.coverFragmentTextContainer.layoutParams = LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f)
|
||||
// }
|
||||
//
|
||||
// (binding.episodeDetails.parent as ViewGroup).removeView(binding.episodeDetails)
|
||||
|
@ -442,8 +431,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
|
||||
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text))
|
||||
if (Build.VERSION.SDK_INT <= 32) {
|
||||
(requireActivity() as MainActivity).showSnackbarAbovePlayer(
|
||||
resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
(requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import ac.mdiq.podcini.storage.DBReader
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.activity.appstartintent.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||
|
@ -343,7 +344,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
if (videoControlsShowing) {
|
||||
hideVideoControls(false)
|
||||
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
|
||||
if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW)
|
||||
(activity as AppCompatActivity).supportActionBar?.hide()
|
||||
videoControlsShowing = false
|
||||
}
|
||||
|
@ -360,7 +361,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsShowing) {
|
||||
hideVideoControls(true)
|
||||
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
|
||||
if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW)
|
||||
(activity as AppCompatActivity).supportActionBar?.hide()
|
||||
} else {
|
||||
showVideoControls()
|
||||
|
@ -461,8 +462,8 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
if (videoControlsShowing) {
|
||||
Log.d(TAG, "Hiding video controls")
|
||||
hideVideoControls(true)
|
||||
if ((activity as VideoplayerActivity).videoMode == VideoplayerActivity.FULL_SCREEN_VIEW)
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.hide()
|
||||
if (videoMode == VideoplayerActivity.VideoMode.FULL_SCREEN_VIEW)
|
||||
(activity as? AppCompatActivity)?.supportActionBar?.hide()
|
||||
videoControlsShowing = false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,7 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
|
|||
init {
|
||||
val colorPrimary = colorToHtml(context, android.R.attr.textColorPrimary)
|
||||
val colorAccent = colorToHtml(context, R.attr.colorAccent)
|
||||
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f,
|
||||
context.resources.displayMetrics).toInt()
|
||||
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, context.resources.displayMetrics).toInt()
|
||||
var styleString: String? = ""
|
||||
try {
|
||||
val templateStream = context.assets.open("shownotes-style.css")
|
||||
|
@ -36,15 +35,13 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
|
|||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
webviewStyle = String.format(Locale.US, styleString!!, colorPrimary, colorAccent,
|
||||
margin, margin, margin, margin)
|
||||
webviewStyle = String.format(Locale.US, styleString!!, colorPrimary, colorAccent, margin, margin, margin, margin)
|
||||
}
|
||||
|
||||
private fun colorToHtml(context: Context, colorAttr: Int): String {
|
||||
val res = context.theme.obtainStyledAttributes(intArrayOf(colorAttr))
|
||||
@ColorInt val col = res.getColor(0, 0)
|
||||
val color = ("rgba(" + Color.red(col) + "," + Color.green(col) + ","
|
||||
+ Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")")
|
||||
val color = ("rgba(" + Color.red(col) + "," + Color.green(col) + "," + Color.blue(col) + "," + (Color.alpha(col) / 255.0) + ")")
|
||||
res.recycle()
|
||||
return color
|
||||
}
|
||||
|
|
|
@ -40,9 +40,8 @@ object WidgetUpdater {
|
|||
* Update the widgets with the given parameters. Must be called in a background thread.
|
||||
*/
|
||||
fun updateWidget(context: Context, widgetState: WidgetState?) {
|
||||
if (!isEnabled(context) || widgetState == null) {
|
||||
return
|
||||
}
|
||||
if (!isEnabled(context) || widgetState == null) return
|
||||
|
||||
val startMediaPlayer = if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO) {
|
||||
VideoPlayerActivityStarter(context).pendingIntent
|
||||
} else {
|
||||
|
@ -88,8 +87,7 @@ object WidgetUpdater {
|
|||
views.setViewVisibility(R.id.txtvTitle, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.txtNoPlaying, View.GONE)
|
||||
|
||||
val progressString = getProgressString(widgetState.position,
|
||||
widgetState.duration, widgetState.playbackSpeed)
|
||||
val progressString = getProgressString(widgetState.position, widgetState.duration, widgetState.playbackSpeed)
|
||||
if (progressString != null) {
|
||||
views.setViewVisibility(R.id.txtvProgress, View.VISIBLE)
|
||||
views.setTextViewText(R.id.txtvProgress, progressString)
|
||||
|
@ -106,22 +104,16 @@ object WidgetUpdater {
|
|||
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play)
|
||||
views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label))
|
||||
}
|
||||
views.setOnClickPendingIntent(R.id.butPlay,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setOnClickPendingIntent(R.id.butPlayExtended,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setOnClickPendingIntent(R.id.butRew,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND))
|
||||
views.setOnClickPendingIntent(R.id.butFastForward,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD))
|
||||
views.setOnClickPendingIntent(R.id.butSkip,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
views.setOnClickPendingIntent(R.id.butPlay, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setOnClickPendingIntent(R.id.butRew, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND))
|
||||
views.setOnClickPendingIntent(R.id.butFastForward, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD))
|
||||
views.setOnClickPendingIntent(R.id.butSkip, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
} else {
|
||||
// start the app if they click anything
|
||||
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
|
||||
views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer)
|
||||
views.setOnClickPendingIntent(R.id.butPlayExtended,
|
||||
createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
|
||||
views.setViewVisibility(R.id.txtvProgress, View.GONE)
|
||||
views.setViewVisibility(R.id.txtvTitle, View.GONE)
|
||||
views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE)
|
||||
|
@ -183,27 +175,18 @@ object WidgetUpdater {
|
|||
}
|
||||
|
||||
private fun getProgressString(position: Int, duration: Int, speed: Float): String? {
|
||||
if (position < 0 || duration <= 0) {
|
||||
return null
|
||||
}
|
||||
if (position < 0 || duration <= 0) return null
|
||||
|
||||
val converter = TimeSpeedConverter(speed)
|
||||
return if (shouldShowRemainingTime()) {
|
||||
(getDurationStringLong(converter.convert(position)) + " / -"
|
||||
+ getDurationStringLong(converter.convert(max(0.0,
|
||||
(duration - position).toDouble())
|
||||
.toInt())))
|
||||
("${getDurationStringLong(converter.convert(position))} / -${getDurationStringLong(converter.convert(max(0.0, (duration - position).toDouble()).toInt()))
|
||||
}")
|
||||
} else {
|
||||
(getDurationStringLong(converter.convert(position)) + " / "
|
||||
+ getDurationStringLong(converter.convert(duration)))
|
||||
(getDurationStringLong(converter.convert(position)) + " / " + getDurationStringLong(converter.convert(duration)))
|
||||
}
|
||||
}
|
||||
|
||||
class WidgetState(val media: Playable?,
|
||||
val status: PlayerStatus,
|
||||
val position: Int,
|
||||
val duration: Int,
|
||||
val playbackSpeed: Float
|
||||
) {
|
||||
class WidgetState(val media: Playable?, val status: PlayerStatus, val position: Int, val duration: Int, val playbackSpeed: Float) {
|
||||
constructor(status: PlayerStatus) : this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/action_icon_color" android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/action_icon_color" android:pathData="M1,11l0,10l5,0l0,-6l4,0l0,6l5,0l0,-10l-7,-5z"/>
|
||||
<path android:fillColor="?attr/action_icon_color" android:pathData="M10,3v1.97l7,5V11h2v2h-2v2h2v2h-2v4h6V3H10zM19,9h-2V7h2V9z"/>
|
||||
</vector>
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/episode_home_fragment"
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="?attr/actionBarTheme"
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -114,80 +114,53 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:gravity="center"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<LinearLayout
|
||||
<ImageView
|
||||
android:id="@+id/butAction1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center">
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/butAction0"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginLeft="40dp"
|
||||
android:layout_marginStart="40dp"
|
||||
android:layout_marginRight="30dp"
|
||||
android:layout_marginEnd="30dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:src="@drawable/baseline_home_work_24"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.CircularProgressBar
|
||||
android:id="@+id/circularProgressBar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
app:foregroundColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/butAction1Icon"
|
||||
android:id="@+id/butAction2"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginRight="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butAction1Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Button"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Button 1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/butAction2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.CircularProgressBar
|
||||
android:id="@+id/circularProgressBar"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
app:foregroundColor="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/butAction2Icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
tools:src="@drawable/ic_settings" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/butAction2Text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Button"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
tools:text="Button 2" />
|
||||
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
@ -210,10 +183,17 @@
|
|||
<ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
android:id="@+id/webvDescription"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/header"
|
||||
android:foreground="?android:windowContentOverlay" />
|
||||
|
||||
<!-- <WebView-->
|
||||
<!-- android:id="@+id/webView"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_below="@id/webvDescription"-->
|
||||
<!-- />-->
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/text_speech"
|
||||
android:icon="@drawable/ic_play_24dp"
|
||||
android:title="@string/play_label"
|
||||
android:visible="false"
|
||||
custom:showAsAction="always">
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/switch_home"
|
||||
android:icon="@drawable/baseline_home_24"
|
||||
android:title="@string/home_label"
|
||||
custom:showAsAction="always">
|
||||
</item>
|
||||
|
||||
|
||||
</menu>
|
|
@ -2,6 +2,14 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/show_video"
|
||||
android:icon="@drawable/baseline_fullscreen_24"
|
||||
android:title="@string/add_to_favorite_label"
|
||||
android:visible="false"
|
||||
custom:showAsAction="always">
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/add_to_favorites_item"
|
||||
android:icon="@drawable/ic_star_border"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
|
|
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 41 KiB |
|
@ -11,6 +11,8 @@
|
|||
<color name="feed_image_bg">#50000000</color>
|
||||
<color name="feed_text_bg">#55333333</color>
|
||||
|
||||
<color name="ic_launcher_background">#000000</color>
|
||||
|
||||
<!-- Theme colors -->
|
||||
<color name="background_light">#FFFFFF</color>
|
||||
<color name="background_elevated_light">#EFEEEE</color>
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
<item quantity="one">%d episode removed from queue.</item>
|
||||
<item quantity="other">%d episodes removed from queue.</item>
|
||||
</plurals>
|
||||
<string name="show_video_label">Play video</string>
|
||||
<string name="add_to_favorite_label">Add to favorites</string>
|
||||
<string name="remove_from_favorite_label">Remove from favorites</string>
|
||||
<string name="visit_website_label">Visit website</string>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.service.playback
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import androidx.media3.session.MediaSession
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.support.wearable.media.MediaControlConstants
|
||||
|
||||
|
@ -16,10 +16,10 @@ object WearMediaSession {
|
|||
actionBuilder.setExtras(actionExtras)
|
||||
}
|
||||
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat) {
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSession) {
|
||||
val sessionExtras = Bundle()
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, false)
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, false)
|
||||
mediaSession.setExtras(sessionExtras)
|
||||
mediaSession.setSessionExtras(sessionExtras)
|
||||
}
|
||||
}
|
||||
|
|
11
changelog.md
|
@ -272,4 +272,13 @@
|
|||
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
|
||||
* webkit updated to Androidx
|
||||
* fixed bug in setting speed to wrong categories
|
||||
* improved fetching of episode images when invalid addresses are given
|
||||
* improved fetching of episode images when invalid addresses are given
|
||||
|
||||
## 4.9.0
|
||||
|
||||
* fixed bug of player always expands when changing audio
|
||||
* migrated to media3's MediaSession and MediaLibraryService thought no new features added with this. some behavior might change or issues might arise, need to be mindful
|
||||
* when video mode is temporarily audio only, click on image on audio player on a video episode also brings up the normal player detailed view* added a menu action item in player detailed view to turn to fullscreen video for video episode
|
||||
* added episode home view accessible right from episode info view. episode home view has two display modes: webpage or reader.
|
||||
* added text-to-speech function in the reader mode. there is a play/pause button on the top action bar, when play is pressed, text-to-speech will be used to play the text. play features now are controlled by system setting of the TTS engine. Advanced operations in Podcini are expected to come later.
|
||||
* RSS feeds with no playable media can be subscribed and read/listened via the above two ways
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
Version 4.9.0 brings several changes:
|
||||
|
||||
* fixed bug of player always expands when changing audio
|
||||
* migrated to media3's MediaSession and MediaLibraryService thought no new features added with this. some behavior might change or issues might arise, need to be mindful
|
||||
* when video mode is temporarily audio only, click on image on audio player on a video episode also brings up the normal player detailed view* added a menu action item in player detailed view to turn to fullscreen video for video episode
|
||||
* added episode home view accessible right from episode info view. episode home view has two display modes: webpage or reader.
|
||||
* added text-to-speech function in the reader mode. there is a play/pause button on the top action bar, when play is pressed, text-to-speech will be used to play the text. play features now are controlled by system setting of the TTS engine. Advanced operations in Podcini are expected to come later.
|
||||
* RSS feeds with no playable media can be subscribed and read/listened via the above two ways
|
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 41 KiB |
BIN
images/black.png
Before Width: | Height: | Size: 70 KiB |