4.9.1 commit
This commit is contained in:
parent
61c61f4df1
commit
c2fb0f4a31
|
@ -158,8 +158,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020131
|
||||
versionName "4.9.0"
|
||||
versionCode 3020132
|
||||
versionName "4.9.1"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
|
|
@ -57,8 +57,8 @@
|
|||
tools:ignore="ExportedService">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaLibraryService"/>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
<!-- <action android:name="android.media.browse.MediaBrowserService"/>-->
|
||||
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package ac.mdiq.podcini.playback.service
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.MediaNotification
|
||||
import androidx.media3.session.MediaSession
|
||||
import com.google.common.collect.ImmutableList
|
||||
|
||||
@UnstableApi
|
||||
class CustomMediaNotificationProvider(context: Context) : DefaultMediaNotificationProvider(context) {
|
||||
|
||||
override fun addNotificationActions(mediaSession: MediaSession, mediaButtons: ImmutableList<CommandButton>, builder: NotificationCompat.Builder, actionFactory: MediaNotification.ActionFactory): IntArray {
|
||||
|
||||
/* Retrieving notification default play/pause button from mediaButtons list. */
|
||||
val defaultPlayPauseCommandButton = mediaButtons.getOrNull(0)
|
||||
val notificationMediaButtons = if (defaultPlayPauseCommandButton != null) {
|
||||
/* Overriding received mediaButtons list to ensure required buttons order: [rewind15, play/pause, forward15]. */
|
||||
ImmutableList.builder<CommandButton>().apply {
|
||||
add(NotificationPlayerCustomCommandButton.REWIND.commandButton)
|
||||
add(defaultPlayPauseCommandButton)
|
||||
add(NotificationPlayerCustomCommandButton.FORWARD.commandButton)
|
||||
}.build()
|
||||
} else {
|
||||
/* Fallback option to handle nullability, in case retrieving default play/pause button fails for some reason (should never happen). */
|
||||
mediaButtons
|
||||
}
|
||||
return super.addNotificationActions(
|
||||
mediaSession,
|
||||
notificationMediaButtons,
|
||||
builder,
|
||||
actionFactory
|
||||
)
|
||||
}
|
||||
}
|
|
@ -125,13 +125,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
return
|
||||
} else {
|
||||
// stop playback of this episode
|
||||
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) {
|
||||
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED)
|
||||
mediaPlayer?.stop()
|
||||
}
|
||||
|
||||
// set temporarily to pause in order to update list with current position
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
callback.onPlaybackPause(media, getPosition())
|
||||
}
|
||||
if (playerStatus == PlayerStatus.PLAYING) callback.onPlaybackPause(media, getPosition())
|
||||
|
||||
if (media!!.getIdentifier() != playable.getIdentifier()) {
|
||||
val oldMedia: Playable = media!!
|
||||
|
@ -160,22 +158,17 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (playable is FeedMedia && playable.item?.feed?.preferences != null) {
|
||||
val preferences = playable.item!!.feed!!.preferences!!
|
||||
mediaPlayer?.setDataSource(streamurl, preferences.username, preferences.password)
|
||||
} else {
|
||||
mediaPlayer?.setDataSource(streamurl)
|
||||
}
|
||||
} else mediaPlayer?.setDataSource(streamurl)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val localMediaurl = media!!.getLocalMediaUrl()
|
||||
if (localMediaurl != null && File(localMediaurl).canRead()) {
|
||||
mediaPlayer?.setDataSource(localMediaurl)
|
||||
} else throw IOException("Unable to read local file $localMediaurl")
|
||||
if (localMediaurl != null && File(localMediaurl).canRead()) mediaPlayer?.setDataSource(localMediaurl)
|
||||
else throw IOException("Unable to read local file $localMediaurl")
|
||||
}
|
||||
}
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) {
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media)
|
||||
}
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, media)
|
||||
|
||||
if (prepareImmediately) {
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media)
|
||||
|
@ -250,9 +243,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
abandonAudioFocus()
|
||||
pausedBecauseOfTransientAudiofocusLoss = false
|
||||
}
|
||||
if (stream && reinit) {
|
||||
reinit()
|
||||
}
|
||||
if (stream && reinit) reinit()
|
||||
} else {
|
||||
Log.d(TAG, "Ignoring call to pause: Player is in $playerStatus state")
|
||||
}
|
||||
|
@ -285,15 +276,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
check(playerStatus == PlayerStatus.PREPARING) { "Player is not in PREPARING state" }
|
||||
Log.d(TAG, "Resource prepared")
|
||||
|
||||
if (mediaPlayer != null && mediaType == MediaType.VIDEO) {
|
||||
videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight)
|
||||
}
|
||||
if (mediaPlayer != null && mediaType == MediaType.VIDEO) videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight)
|
||||
|
||||
if (media != null) {
|
||||
// TODO this call has no effect!
|
||||
if (media!!.getPosition() > 0) {
|
||||
seekTo(media!!.getPosition())
|
||||
}
|
||||
if (media!!.getPosition() > 0) seekTo(media!!.getPosition())
|
||||
|
||||
if (media!!.getDuration() <= 0) {
|
||||
Log.d(TAG, "Setting duration of media")
|
||||
|
@ -302,9 +289,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
setPlayerStatus(PlayerStatus.PREPARED, media)
|
||||
|
||||
if (startWhenPrepared) {
|
||||
resume()
|
||||
}
|
||||
if (startWhenPrepared) resume()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -317,15 +302,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
Log.d(TAG, "reinit()")
|
||||
releaseWifiLockIfNecessary()
|
||||
when {
|
||||
media != null -> {
|
||||
playMediaObject(media!!, true, stream, startWhenPrepared.get(), false)
|
||||
}
|
||||
mediaPlayer != null -> {
|
||||
mediaPlayer!!.reset()
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null")
|
||||
}
|
||||
media != null -> playMediaObject(media!!, true, stream, startWhenPrepared.get(), false)
|
||||
mediaPlayer != null -> mediaPlayer!!.reset()
|
||||
else -> Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -338,9 +317,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
*/
|
||||
override fun seekTo(t0: Int) {
|
||||
var t = t0
|
||||
if (t < 0) {
|
||||
t = 0
|
||||
}
|
||||
if (t < 0) t = 0
|
||||
|
||||
if (t >= getDuration()) {
|
||||
Log.d(TAG, "Seek reached end of file, skipping to next episode")
|
||||
|
@ -361,9 +338,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
statusBeforeSeeking = playerStatus
|
||||
setPlayerStatus(PlayerStatus.SEEKING, media, getPosition())
|
||||
mediaPlayer?.seekTo(t)
|
||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) {
|
||||
media?.setPosition(t)
|
||||
}
|
||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) media?.setPosition(t)
|
||||
try {
|
||||
seekLatch!!.await(3, TimeUnit.SECONDS)
|
||||
} catch (e: InterruptedException) {
|
||||
|
@ -401,9 +376,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
|
||||
if (mediaPlayer != null) retVal = mediaPlayer!!.duration
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getDuration() > 0) {
|
||||
retVal = media!!.getDuration()
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getDuration() > 0) retVal = media!!.getDuration()
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
|
@ -415,9 +389,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) {
|
||||
if (mediaPlayer != null) retVal = mediaPlayer!!.currentPosition
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getPosition() >= 0) {
|
||||
retVal = media!!.getPosition()
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getPosition() >= 0) retVal = media!!.getPosition()
|
||||
|
||||
return retVal
|
||||
}
|
||||
|
||||
|
@ -521,9 +494,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
* invalid values.
|
||||
*/
|
||||
override fun getVideoSize(): Pair<Int, Int>? {
|
||||
if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO) {
|
||||
if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO)
|
||||
videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight)
|
||||
}
|
||||
|
||||
return videoSize
|
||||
}
|
||||
|
||||
|
@ -568,9 +541,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
|
||||
private val audioFocusChangeListener = OnAudioFocusChangeListener { focusChange ->
|
||||
if (isShutDown) {
|
||||
return@OnAudioFocusChangeListener
|
||||
}
|
||||
if (isShutDown) return@OnAudioFocusChangeListener
|
||||
|
||||
when {
|
||||
!PlaybackService.isRunning -> {
|
||||
abandonAudioFocus()
|
||||
|
@ -582,8 +554,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
pause(true, reinit = false)
|
||||
callback.shouldStop()
|
||||
}
|
||||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
|
||||
&& !UserPreferences.shouldPauseForFocusLoss() -> {
|
||||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK && !UserPreferences.shouldPauseForFocusLoss() -> {
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
Log.d(TAG, "Lost audio focus temporarily. Ducking...")
|
||||
setVolume(0.25f, 0.25f)
|
||||
|
@ -598,21 +569,17 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
|
||||
audioFocusCanceller.removeCallbacksAndMessages(null)
|
||||
audioFocusCanceller.postDelayed({
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) {
|
||||
// Still did not get back the audio focus. Now actually pause.
|
||||
pause(abandonFocus = true, reinit = false)
|
||||
}
|
||||
// Still did not get back the audio focus. Now actually pause.
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) pause(abandonFocus = true, reinit = false)
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
focusChange == AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
Log.d(TAG, "Gained audio focus")
|
||||
audioFocusCanceller.removeCallbacksAndMessages(null)
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now
|
||||
mediaPlayer?.start()
|
||||
} else { // we ducked => raise audio level back
|
||||
setVolume(1.0f, 1.0f)
|
||||
}
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) mediaPlayer?.start() // we paused => play now
|
||||
else setVolume(1.0f, 1.0f) // we ducked => raise audio level back
|
||||
|
||||
pausedBecauseOfTransientAudiofocusLoss = false
|
||||
}
|
||||
}
|
||||
|
@ -639,9 +606,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
|
||||
// we're relying on the position stored in the Playable object for post-playback processing
|
||||
val position = getPosition()
|
||||
if (position >= 0) {
|
||||
media?.setPosition(position)
|
||||
}
|
||||
if (position >= 0) media?.setPosition(position)
|
||||
|
||||
mediaPlayer?.reset()
|
||||
|
||||
|
@ -706,15 +671,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
mp.setOnSeekCompleteListener(Runnable { this.genericSeekCompleteListener() })
|
||||
mp.setOnBufferingUpdateListener(Consumer { percent: Int ->
|
||||
when (percent) {
|
||||
ExoPlayerWrapper.BUFFERING_STARTED -> {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.started())
|
||||
}
|
||||
ExoPlayerWrapper.BUFFERING_ENDED -> {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.ended())
|
||||
}
|
||||
else -> {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent))
|
||||
}
|
||||
ExoPlayerWrapper.BUFFERING_STARTED -> EventBus.getDefault().post(BufferUpdateEvent.started())
|
||||
ExoPlayerWrapper.BUFFERING_ENDED -> EventBus.getDefault().post(BufferUpdateEvent.ended())
|
||||
else -> EventBus.getDefault().post(BufferUpdateEvent.progressUpdate(0.01f * percent))
|
||||
}
|
||||
})
|
||||
mp.setOnErrorListener(Consumer { message: String ->
|
||||
|
@ -737,9 +696,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
if (media != null) callback.onPlaybackStart(media!!, getPosition())
|
||||
}
|
||||
if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) {
|
||||
setPlayerStatus(statusBeforeSeeking!!, media, getPosition())
|
||||
}
|
||||
if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) setPlayerStatus(statusBeforeSeeking!!, media, getPosition())
|
||||
}
|
||||
|
||||
override fun isCasting(): Boolean {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package ac.mdiq.podcini.playback.service
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import android.os.Bundle
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.SessionCommand
|
||||
|
||||
private const val CUSTOM_COMMAND_REWIND_ACTION_ID = "REWIND_15"
|
||||
private const val CUSTOM_COMMAND_FORWARD_ACTION_ID = "FAST_FWD_15"
|
||||
|
||||
enum class NotificationPlayerCustomCommandButton(val customAction: String, val commandButton: CommandButton) {
|
||||
REWIND(
|
||||
customAction = CUSTOM_COMMAND_REWIND_ACTION_ID,
|
||||
commandButton = CommandButton.Builder()
|
||||
.setDisplayName("Rewind")
|
||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_REWIND_ACTION_ID, Bundle()))
|
||||
.setIconResId(R.drawable.ic_notification_fast_rewind)
|
||||
.build(),
|
||||
),
|
||||
FORWARD(
|
||||
customAction = CUSTOM_COMMAND_FORWARD_ACTION_ID,
|
||||
commandButton = CommandButton.Builder()
|
||||
.setDisplayName("Forward")
|
||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_FORWARD_ACTION_ID, Bundle()))
|
||||
.setIconResId(R.drawable.ic_notification_fast_forward)
|
||||
.build(),
|
||||
);
|
||||
}
|
|
@ -47,7 +47,6 @@ import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
|||
import ac.mdiq.podcini.service.playback.WearMediaSession
|
||||
import ac.mdiq.podcini.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.DBWriter
|
||||
import ac.mdiq.podcini.storage.model.feed.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
|
||||
|
@ -76,15 +75,12 @@ import android.bluetooth.BluetoothA2dp
|
|||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
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
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
|
@ -95,15 +91,12 @@ import android.view.KeyEvent
|
|||
import android.view.SurfaceHolder
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.MediaSessionService
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.SingleEmitter
|
||||
|
@ -122,7 +115,7 @@ import kotlin.math.max
|
|||
* Controls the MediaPlayer that plays a FeedMedia-file
|
||||
*/
|
||||
@UnstableApi
|
||||
class PlaybackService : MediaLibraryService() {
|
||||
class PlaybackService : MediaSessionService() {
|
||||
private var mediaPlayer: PlaybackServiceMediaPlayer? = null
|
||||
private var positionEventTimer: Disposable? = null
|
||||
|
||||
|
@ -266,8 +259,8 @@ class PlaybackService : MediaLibraryService() {
|
|||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? {
|
||||
return null
|
||||
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
|
||||
return mediaSession
|
||||
}
|
||||
|
||||
private fun loadQueueForMediaSession() {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -39,9 +39,8 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
private var position: String? = null
|
||||
|
||||
fun setPlayable(playable: Playable) {
|
||||
if (playable !== this.playable) {
|
||||
clearCache()
|
||||
}
|
||||
if (playable !== this.playable) clearCache()
|
||||
|
||||
this.playable = playable
|
||||
}
|
||||
|
||||
|
@ -86,31 +85,22 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
|
||||
private val defaultIcon: Bitmap?
|
||||
get() {
|
||||
if (Companion.defaultIcon == null) {
|
||||
Companion.defaultIcon = getBitmap(context, R.mipmap.ic_launcher)
|
||||
}
|
||||
if (Companion.defaultIcon == null) Companion.defaultIcon = getBitmap(context, R.mipmap.ic_launcher)
|
||||
return Companion.defaultIcon
|
||||
}
|
||||
|
||||
fun build(): Notification {
|
||||
val notification = NotificationCompat.Builder(
|
||||
context,
|
||||
NotificationUtils.CHANNEL_ID_PLAYING)
|
||||
val notification = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_PLAYING)
|
||||
|
||||
if (playable != null) {
|
||||
notification.setContentTitle(playable!!.getFeedTitle())
|
||||
notification.setContentText(playable!!.getEpisodeTitle())
|
||||
addActions(notification, mediaSessionToken, playerStatus)
|
||||
|
||||
if (cachedIcon != null) {
|
||||
notification.setLargeIcon(cachedIcon)
|
||||
} else {
|
||||
notification.setLargeIcon(this.defaultIcon)
|
||||
}
|
||||
if (cachedIcon != null) notification.setLargeIcon(cachedIcon)
|
||||
else notification.setLargeIcon(this.defaultIcon)
|
||||
|
||||
if (Build.VERSION.SDK_INT < 29) {
|
||||
notification.setSubText(position)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < 29) notification.setSubText(position)
|
||||
} else {
|
||||
notification.setContentTitle(context.getString(R.string.app_name))
|
||||
notification.setContentText("Loading. If this does not go away, play any episode and contact us.")
|
||||
|
@ -129,11 +119,10 @@ 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)
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, PlaybackService.getPlayerActivityIntent(context),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
private fun addActions(notification: NotificationCompat.Builder, mediaSessionToken: MediaSessionCompat.Token?,
|
||||
playerStatus: PlayerStatus?) {
|
||||
private fun addActions(notification: NotificationCompat.Builder, mediaSessionToken: MediaSessionCompat.Token?, playerStatus: PlayerStatus?) {
|
||||
val compactActionList = ArrayList<Int>()
|
||||
|
||||
var numActions = 0 // we start and 0 and then increment by 1 for each call to addAction
|
||||
|
@ -173,11 +162,12 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
}
|
||||
|
||||
val stopButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_STOP, numActions)
|
||||
notification.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSessionToken)
|
||||
.setShowActionsInCompactView(*ArrayUtils.toPrimitive(compactActionList.toTypedArray<Int>()))
|
||||
.setShowCancelButton(true)
|
||||
.setCancelButtonIntent(stopButtonPendingIntent))
|
||||
notification
|
||||
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSessionToken)
|
||||
.setShowActionsInCompactView(*ArrayUtils.toPrimitive(compactActionList.toTypedArray<Int>()))
|
||||
.setShowCancelButton(true)
|
||||
.setCancelButtonIntent(stopButtonPendingIntent))
|
||||
}
|
||||
|
||||
private fun getPendingIntentForMediaAction(keycodeValue: Int, requestCode: Int): PendingIntent {
|
||||
|
@ -222,15 +212,9 @@ class PlaybackServiceNotificationBuilder(private val context: Context) {
|
|||
|
||||
private fun getBitmap(context: Context, drawableId: Int): Bitmap? {
|
||||
return when (val drawable = AppCompatResources.getDrawable(context, drawableId)) {
|
||||
is BitmapDrawable -> {
|
||||
drawable.bitmap
|
||||
}
|
||||
is VectorDrawable -> {
|
||||
getBitmap(drawable)
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
is BitmapDrawable -> drawable.bitmap
|
||||
is VectorDrawable -> getBitmap(drawable)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,7 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
}
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
Log.d(TAG, "onUpdate() called with: " + "context = [" + context + "], appWidgetManager = ["
|
||||
+ appWidgetManager + "], appWidgetIds = [" + appWidgetIds.contentToString() + "]")
|
||||
Log.d(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
||||
WidgetUpdaterWorker.enqueueWork(context)
|
||||
|
||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
|
|
@ -98,9 +98,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
DBReader.updateFeedList()
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0))
|
||||
}
|
||||
if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0))
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
_binding = MainActivityBinding.inflate(layoutInflater)
|
||||
|
@ -175,12 +174,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
var isRefreshingFeeds = false
|
||||
for (workInfo in workInfos) {
|
||||
when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
isRefreshingFeeds = true
|
||||
}
|
||||
WorkInfo.State.ENQUEUED -> {
|
||||
isRefreshingFeeds = true
|
||||
}
|
||||
WorkInfo.State.RUNNING -> isRefreshingFeeds = true
|
||||
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
|
||||
else -> {
|
||||
// Log.d(TAG, "workInfo.state ${workInfo.state}")
|
||||
}
|
||||
|
@ -199,20 +194,13 @@ class MainActivity : CastEnabledActivity() {
|
|||
downloadUrl = tag.substring(DownloadServiceInterface.WORK_TAG_EPISODE_URL.length)
|
||||
}
|
||||
}
|
||||
if (downloadUrl == null) {
|
||||
continue
|
||||
}
|
||||
if (downloadUrl == null) continue
|
||||
|
||||
var status: Int
|
||||
status = when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
DownloadStatus.STATE_RUNNING
|
||||
}
|
||||
WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> {
|
||||
DownloadStatus.STATE_QUEUED
|
||||
}
|
||||
WorkInfo.State.SUCCEEDED -> {
|
||||
DownloadStatus.STATE_COMPLETED
|
||||
}
|
||||
WorkInfo.State.RUNNING -> DownloadStatus.STATE_RUNNING
|
||||
WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> DownloadStatus.STATE_QUEUED
|
||||
WorkInfo.State.SUCCEEDED -> DownloadStatus.STATE_COMPLETED
|
||||
WorkInfo.State.FAILED -> {
|
||||
Log.e(TAG, "download failed $downloadUrl")
|
||||
DownloadStatus.STATE_COMPLETED
|
||||
|
@ -240,9 +228,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
if (isGranted) return@registerForActivityResult
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.notification_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> {} }
|
||||
|
@ -276,12 +263,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
override fun onStateChanged(view: View, state: Int) {
|
||||
Log.d(TAG, "bottomSheet onStateChanged $state")
|
||||
when (state) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
onSlide(view,0.0f)
|
||||
}
|
||||
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||
onSlide(view, 1.0f)
|
||||
}
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> onSlide(view,0.0f)
|
||||
BottomSheetBehavior.STATE_EXPANDED -> onSlide(view, 1.0f)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -299,18 +282,15 @@ class MainActivity : CastEnabledActivity() {
|
|||
// Tablet layout does not have a drawer
|
||||
when {
|
||||
drawerLayout != null -> {
|
||||
if (drawerToggle != null) {
|
||||
drawerLayout!!.removeDrawerListener(drawerToggle!!)
|
||||
}
|
||||
if (drawerToggle != null) drawerLayout!!.removeDrawerListener(drawerToggle!!)
|
||||
|
||||
drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar, R.string.drawer_open, R.string.drawer_close)
|
||||
drawerLayout!!.addDrawerListener(drawerToggle!!)
|
||||
drawerToggle!!.syncState()
|
||||
drawerToggle!!.isDrawerIndicatorEnabled = !displayUpArrow
|
||||
drawerToggle!!.toolbarNavigationClickListener = View.OnClickListener { supportFragmentManager.popBackStack() }
|
||||
}
|
||||
!displayUpArrow -> {
|
||||
toolbar.navigationIcon = null
|
||||
}
|
||||
!displayUpArrow -> toolbar.navigationIcon = null
|
||||
else -> {
|
||||
toolbar.setNavigationIcon(getDrawableFromAttr(this, R.attr.homeAsUpIndicator))
|
||||
toolbar.setNavigationOnClickListener { supportFragmentManager.popBackStack() }
|
||||
|
@ -348,12 +328,9 @@ class MainActivity : CastEnabledActivity() {
|
|||
val visible = if (visible_ != null) visible_ else if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) false else true
|
||||
|
||||
bottomSheet.setLocked(!visible)
|
||||
if (visible) {
|
||||
// Update toolbar visibility
|
||||
bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state)
|
||||
} else {
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
if (visible) bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility
|
||||
else bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
|
||||
// val mainView = findViewById<FragmentContainerView>(R.id.main_view)
|
||||
val params = mainView.layoutParams as MarginLayoutParams
|
||||
val externalPlayerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||
|
@ -388,18 +365,16 @@ class MainActivity : CastEnabledActivity() {
|
|||
args = null
|
||||
}
|
||||
}
|
||||
if (args != null) {
|
||||
fragment.arguments = args
|
||||
}
|
||||
if (args != null) fragment.arguments = args
|
||||
|
||||
NavDrawerFragment.saveLastNavFragment(this, tag)
|
||||
loadFragment(fragment)
|
||||
}
|
||||
|
||||
fun loadFeedFragmentById(feedId: Long, args: Bundle?) {
|
||||
val fragment: Fragment = FeedItemlistFragment.newInstance(feedId)
|
||||
if (args != null) {
|
||||
fragment.arguments = args
|
||||
}
|
||||
if (args != null) fragment.arguments = args
|
||||
|
||||
NavDrawerFragment.saveLastNavFragment(this, feedId.toString())
|
||||
loadFragment(fragment)
|
||||
}
|
||||
|
@ -461,9 +436,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
private fun setNavDrawerSize() {
|
||||
// Tablet layout does not have a drawer
|
||||
if (drawerLayout == null) {
|
||||
return
|
||||
}
|
||||
if (drawerLayout == null) return
|
||||
|
||||
val screenPercent = resources.getInteger(R.integer.nav_drawer_screen_size_percent) * 0.01f
|
||||
val width = (screenWidth * screenPercent).toInt()
|
||||
val maxWidth = resources.getDimension(R.dimen.nav_drawer_max_screen_size).toInt()
|
||||
|
@ -482,9 +456,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
|
||||
public override fun onStart() {
|
||||
|
@ -503,9 +475,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
finish()
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
if (hiddenDrawerItems.contains(NavDrawerFragment.getLastNavFragment(this))) {
|
||||
loadFragment(defaultPage, null)
|
||||
}
|
||||
if (hiddenDrawerItems.contains(NavDrawerFragment.getLastNavFragment(this))) loadFragment(defaultPage, null)
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
|
@ -531,39 +501,27 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
Log.d(TAG, "onOptionsItemSelected ${item.title}")
|
||||
if (drawerToggle != null && drawerToggle!!.onOptionsItemSelected(item)) {
|
||||
// Tablet layout does not have a drawer
|
||||
return true
|
||||
} else if (item.itemId == android.R.id.home) {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
when {
|
||||
drawerToggle != null && drawerToggle!!.onOptionsItemSelected(item) -> return true // Tablet layout does not have a drawer
|
||||
item.itemId == android.R.id.home -> {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) supportFragmentManager.popBackStack()
|
||||
return true
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item)
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
when {
|
||||
isDrawerOpen -> {
|
||||
drawerLayout?.closeDrawer(navDrawer)
|
||||
}
|
||||
bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED -> {
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
supportFragmentManager.backStackEntryCount != 0 -> {
|
||||
super.onBackPressed()
|
||||
}
|
||||
isDrawerOpen -> drawerLayout?.closeDrawer(navDrawer)
|
||||
bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED -> bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
supportFragmentManager.backStackEntryCount != 0 -> super.onBackPressed()
|
||||
else -> {
|
||||
val toPage = defaultPage
|
||||
if (NavDrawerFragment.getLastNavFragment(this) == toPage || UserPreferences.DEFAULT_PAGE_REMEMBER == toPage) {
|
||||
if (backButtonOpensDrawer()) {
|
||||
drawerLayout?.openDrawer(navDrawer)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
if (backButtonOpensDrawer()) drawerLayout?.openDrawer(navDrawer)
|
||||
else super.onBackPressed()
|
||||
} else {
|
||||
loadFragment(toPage, null)
|
||||
}
|
||||
|
@ -576,9 +534,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
Log.d(TAG, "onEvent($event)")
|
||||
|
||||
val snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG)
|
||||
if (event.action != null) {
|
||||
snackbar.setAction(event.actionText) { event.action.accept(this) }
|
||||
}
|
||||
if (event.action != null) snackbar.setAction(event.actionText) { event.action.accept(this) }
|
||||
}
|
||||
|
||||
private fun handleNavIntent() {
|
||||
|
@ -591,11 +547,8 @@ class MainActivity : CastEnabledActivity() {
|
|||
if (feedId > 0) {
|
||||
val startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false)
|
||||
val addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false)
|
||||
if (startedFromSearch || addToBackStack) {
|
||||
loadChildFragment(FeedItemlistFragment.newInstance(feedId))
|
||||
} else {
|
||||
loadFeedFragmentById(feedId, args)
|
||||
}
|
||||
if (startedFromSearch || addToBackStack) loadChildFragment(FeedItemlistFragment.newInstance(feedId))
|
||||
else loadFeedFragmentById(feedId, args)
|
||||
}
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
|
@ -614,14 +567,11 @@ class MainActivity : CastEnabledActivity() {
|
|||
// bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
// bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
else -> {
|
||||
handleDeeplink(intent.data)
|
||||
}
|
||||
else -> handleDeeplink(intent.data)
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) {
|
||||
drawerLayout?.open()
|
||||
}
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) drawerLayout?.open()
|
||||
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) {
|
||||
DownloadLogFragment().show(supportFragmentManager, null)
|
||||
}
|
||||
|
@ -643,9 +593,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
val s: Snackbar
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
s = Snackbar.make(mainView, text!!, duration)
|
||||
if (audioPlayerFragmentView.visibility == View.VISIBLE) {
|
||||
s.setAnchorView(audioPlayerFragmentView)
|
||||
}
|
||||
if (audioPlayerFragmentView.visibility == View.VISIBLE) s.setAnchorView(audioPlayerFragmentView)
|
||||
} else {
|
||||
s = Snackbar.make(binding.root, text!!, duration)
|
||||
}
|
||||
|
@ -671,7 +619,6 @@ class MainActivity : CastEnabledActivity() {
|
|||
when (uri.path) {
|
||||
"/deeplink/search" -> {
|
||||
val query = uri.getQueryParameter("query") ?: return
|
||||
|
||||
this.loadChildFragment(SearchFragment.newInstance(query))
|
||||
}
|
||||
"/deeplink/main" -> {
|
||||
|
@ -696,9 +643,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
//Hardware keyboard support
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
val currentFocus = currentFocus
|
||||
if (currentFocus is EditText) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
if (currentFocus is EditText) return super.onKeyUp(keyCode, event)
|
||||
|
||||
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
var customKeyCode: Int? = null
|
||||
|
@ -706,23 +651,18 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_P -> customKeyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> customKeyCode =
|
||||
KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> customKeyCode =
|
||||
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> customKeyCode = KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> customKeyCode = KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_W -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_S -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_M -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI)
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,10 +103,10 @@ class WidgetConfigActivity : AppCompatActivity() {
|
|||
|
||||
private fun setInitialState() {
|
||||
val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||
ckPlaybackSpeed.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false)
|
||||
ckRewind.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, false)
|
||||
ckFastForward.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, false)
|
||||
ckSkip.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, false)
|
||||
ckPlaybackSpeed.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, true)
|
||||
ckRewind.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, true)
|
||||
ckFastForward.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, true)
|
||||
ckSkip.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val color = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, PlayerWidget.DEFAULT_COLOR)
|
||||
val opacity = Color.alpha(color) * 100 / 0xFF
|
||||
|
|
|
@ -26,6 +26,8 @@ import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
|
|||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||
import ac.mdiq.podcini.ui.fragment.EpisodeHomeFragment.Companion.fetchHtmlSource
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ChapterSeekBar
|
||||
import ac.mdiq.podcini.ui.view.PlayButton
|
||||
import ac.mdiq.podcini.ui.view.PlaybackSpeedIndicatorView
|
||||
|
@ -63,6 +65,8 @@ import io.reactivex.MaybeEmitter
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
@ -424,6 +428,10 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
|
||||
val itemId = menuItem.itemId
|
||||
when (itemId) {
|
||||
R.id.show_home_reader_view -> {
|
||||
itemDescFrag.buildHomeReaderText()
|
||||
return true
|
||||
}
|
||||
R.id.show_video -> {
|
||||
controller!!.playPause()
|
||||
VideoPlayerActivityStarter(requireContext(), VideoMode.FULL_SCREEN_VIEW).start()
|
||||
|
|
|
@ -3,6 +3,7 @@ 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 ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -33,23 +34,22 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
private var _binding: EpisodeHomeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var item: FeedItem? = null
|
||||
// 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 readerhtml: String? = null
|
||||
private var readMode = false
|
||||
private var ttsPlaying = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
item = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) requireArguments().getSerializable(ARG_FEEDITEM, FeedItem::class.java)
|
||||
else requireArguments().getSerializable(ARG_FEEDITEM) as? FeedItem
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
@ -65,9 +65,8 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
if (item?.link != null) {
|
||||
showContent()
|
||||
}
|
||||
if (currentItem?.link != null) showContent()
|
||||
|
||||
updateAppearance()
|
||||
return binding.root
|
||||
}
|
||||
|
@ -81,9 +80,9 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
override fun onInit(status: Int) {
|
||||
if (status == TextToSpeech.SUCCESS) {
|
||||
// TTS initialization successful
|
||||
Log.i(TAG, "TTS init success with Locale: ${item?.feed?.language}")
|
||||
if (item?.feed?.language != null) {
|
||||
val result = tts.setLanguage(Locale(item!!.feed!!.language))
|
||||
Log.i(TAG, "TTS init success with Locale: ${currentItem?.feed?.language}")
|
||||
if (currentItem?.feed?.language != null) {
|
||||
val result = tts.setLanguage(Locale(currentItem!!.feed!!.language!!))
|
||||
// val result = tts.setLanguage(Locale.UK)
|
||||
if (result == TextToSpeech.LANG_MISSING_DATA || result == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "TTS language not supported")
|
||||
|
@ -100,47 +99,61 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
|
||||
private fun showContent() {
|
||||
if (readMode) {
|
||||
if (readerhtml == null) {
|
||||
var readerhtml: String? = null
|
||||
if (cleanedNotes == null) {
|
||||
runBlocking {
|
||||
val url = item!!.link!!
|
||||
val url = currentItem!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val readability4J = Readability4J(item?.link!!, htmlSource)
|
||||
val readability4J = Readability4J(currentItem?.link!!, htmlSource)
|
||||
val article = readability4J.parse()
|
||||
textContent = article.textContent
|
||||
// Log.d(TAG, "readability4J: ${article.textContent}")
|
||||
readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
if (readerhtml != null) {
|
||||
val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml!!, 0)
|
||||
cleanedNotes = shownotesCleaner.processShownotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (readerhtml != null) binding.webView.loadDataWithBaseURL(item!!.link!!, readerhtml!!, "text/html", "UTF-8", null)
|
||||
if (cleanedNotes != null) {
|
||||
binding.readerView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null)
|
||||
// binding.readerView.loadDataWithBaseURL(currentItem!!.link!!, readerhtml!!, "text/html", "UTF-8", null)
|
||||
binding.readerView.visibility = View.VISIBLE
|
||||
binding.webView.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
if (item?.link != null) binding.webView.loadUrl(item!!.link!!)
|
||||
if (currentItem?.link != null) {
|
||||
binding.webView.loadUrl(currentItem!!.link!!)
|
||||
binding.readerView.visibility = View.GONE
|
||||
binding.webView.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
// 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()
|
||||
// }
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
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)
|
||||
if (ttsPlaying) textSpeech.setIcon(R.drawable.ic_pause) else textSpeech.setIcon(R.drawable.ic_play_24dp)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -174,8 +187,8 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
return true
|
||||
}
|
||||
R.id.share_notes -> {
|
||||
if (item == null) return false
|
||||
val notes = item!!.description
|
||||
if (currentItem == null) return false
|
||||
val notes = currentItem!!.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()
|
||||
|
@ -190,8 +203,7 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
return true
|
||||
}
|
||||
else -> {
|
||||
if (item == null) return false
|
||||
return true
|
||||
return currentItem != null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -210,25 +222,54 @@ class EpisodeHomeFragment : Fragment(), Toolbar.OnMenuItemClickListener, TextToS
|
|||
}
|
||||
|
||||
@UnstableApi private fun updateAppearance() {
|
||||
if (item == null) {
|
||||
Log.d(TAG, "updateAppearance item is null")
|
||||
if (currentItem == null) {
|
||||
Log.d(TAG, "updateAppearance currentItem is null")
|
||||
return
|
||||
}
|
||||
onPrepareOptionsMenu(toolbar.menu)
|
||||
// FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.switch_home)
|
||||
// FeedItemMenuHandler.onPrepareMenu(toolbar.menu, currentItem, R.id.switch_home)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EpisodeWebviewFragment"
|
||||
private const val ARG_FEEDITEM = "feeditem"
|
||||
|
||||
private var textContent: String? = null
|
||||
private var cleanedNotes: String? = null
|
||||
private var currentItem: FeedItem? = null
|
||||
|
||||
@JvmStatic
|
||||
fun newInstance(item: FeedItem): EpisodeHomeFragment {
|
||||
val fragment = EpisodeHomeFragment()
|
||||
val args = Bundle()
|
||||
args.putSerializable(ARG_FEEDITEM, item)
|
||||
fragment.arguments = args
|
||||
// val args = Bundle()
|
||||
Log.d(TAG, "item.itemIdentifier ${item.itemIdentifier}")
|
||||
if (item.itemIdentifier != currentItem?.itemIdentifier) {
|
||||
currentItem = item
|
||||
cleanedNotes = null
|
||||
textContent = null
|
||||
}
|
||||
// args.putSerializable(ARG_FEEDITEM, item)
|
||||
// fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,8 +129,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
|
||||
controller!!.seekTo(time ?: 0)
|
||||
} else {
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
|
||||
Snackbar.LENGTH_LONG)
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
registerForContextMenu(webvDescription)
|
||||
|
@ -155,9 +154,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
showOnDemandConfigBalloon(true)
|
||||
return@OnClickListener
|
||||
}
|
||||
actionButton1 == null -> {
|
||||
return@OnClickListener // Not loaded yet
|
||||
}
|
||||
actionButton1 == null -> return@OnClickListener // Not loaded yet
|
||||
else -> actionButton1?.onClick(requireContext())
|
||||
}
|
||||
})
|
||||
|
@ -168,9 +165,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
showOnDemandConfigBalloon(false)
|
||||
return@OnClickListener
|
||||
}
|
||||
actionButton2 == null -> {
|
||||
return@OnClickListener // Not loaded yet
|
||||
}
|
||||
actionButton2 == null -> return@OnClickListener // Not loaded yet
|
||||
else -> actionButton2?.onClick(requireContext())
|
||||
}
|
||||
})
|
||||
|
@ -283,8 +278,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast)
|
||||
} else {
|
||||
// these are already available via button1 and button2
|
||||
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item,
|
||||
R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
|
||||
FeedItemMenuHandler.onPrepareMenu(toolbar.menu, item, R.id.open_podcast, R.id.mark_read_item, R.id.visit_website_item)
|
||||
}
|
||||
if (item!!.feed != null) txtvPodcast.text = item!!.feed!!.title
|
||||
txtvTitle.text = item!!.title
|
||||
|
@ -339,53 +333,30 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
if (item != null) {
|
||||
actionButton1 = when {
|
||||
media.getMediaType() == MediaType.FLASH -> {
|
||||
VisitWebsiteActionButton(item!!)
|
||||
}
|
||||
PlaybackStatus.isCurrentlyPlaying(media) -> {
|
||||
PauseActionButton(item!!)
|
||||
}
|
||||
item!!.feed != null && item!!.feed!!.isLocalFeed -> {
|
||||
PlayLocalActionButton(item)
|
||||
}
|
||||
media.isDownloaded() -> {
|
||||
PlayActionButton(item!!)
|
||||
}
|
||||
else -> {
|
||||
StreamActionButton(item!!)
|
||||
}
|
||||
media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(item!!)
|
||||
PlaybackStatus.isCurrentlyPlaying(media) -> PauseActionButton(item!!)
|
||||
item!!.feed != null && item!!.feed!!.isLocalFeed -> PlayLocalActionButton(item)
|
||||
media.isDownloaded() -> PlayActionButton(item!!)
|
||||
else -> StreamActionButton(item!!)
|
||||
}
|
||||
actionButton2 = when {
|
||||
media.getMediaType() == MediaType.FLASH -> {
|
||||
VisitWebsiteActionButton(item!!)
|
||||
}
|
||||
dls != null && media.download_url != null && dls.isDownloadingEpisode(media.download_url!!) -> {
|
||||
CancelDownloadActionButton(item!!)
|
||||
}
|
||||
!media.isDownloaded() -> {
|
||||
DownloadActionButton(item!!)
|
||||
}
|
||||
else -> {
|
||||
DeleteActionButton(item!!)
|
||||
}
|
||||
media.getMediaType() == MediaType.FLASH -> VisitWebsiteActionButton(item!!)
|
||||
dls != null && media.download_url != null && dls.isDownloadingEpisode(media.download_url!!) -> CancelDownloadActionButton(item!!)
|
||||
!media.isDownloaded() -> DownloadActionButton(item!!)
|
||||
else -> DeleteActionButton(item!!)
|
||||
}
|
||||
// if (actionButton2 != null && media.getMediaType() == MediaType.FLASH) actionButton2!!.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
if (actionButton1 != null) {
|
||||
// butAction1Text.setText(actionButton1!!.getLabel())
|
||||
butAction1.setImageResource(actionButton1!!.getDrawable())
|
||||
butAction1.visibility = actionButton1!!.visibility
|
||||
}
|
||||
// butAction1Text.transformationMethod = null
|
||||
if (actionButton1 != null) butAction1.visibility = actionButton1!!.visibility
|
||||
|
||||
if (actionButton2 != null) {
|
||||
// butAction2Text.setText(actionButton2!!.getLabel())
|
||||
butAction2.setImageResource(actionButton2!!.getDrawable())
|
||||
butAction2.visibility = actionButton2!!.visibility
|
||||
}
|
||||
// butAction2Text.transformationMethod = null
|
||||
if (actionButton2 != null) butAction2.visibility = actionButton2!!.visibility
|
||||
}
|
||||
|
||||
override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -431,9 +402,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@UnstableApi private fun load() {
|
||||
disposable?.dispose()
|
||||
|
||||
if (!itemsLoaded) {
|
||||
progbarLoading.visibility = View.VISIBLE
|
||||
}
|
||||
if (!itemsLoaded) progbarLoading.visibility = View.VISIBLE
|
||||
|
||||
disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
|
|
@ -11,6 +11,7 @@ import ac.mdiq.podcini.storage.model.feed.FeedItem
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.EpisodeHomeFragment.Companion.fetchHtmlSource
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.util.ChapterUtils
|
||||
|
@ -51,6 +52,8 @@ import io.reactivex.MaybeEmitter
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.dankito.readability4j.Readability4J
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
@ -75,39 +78,33 @@ class PlayerDetailsFragment : Fragment() {
|
|||
private var webViewLoader: Disposable? = null
|
||||
private var controller: PlaybackController? = null
|
||||
|
||||
private var showHomeText = false
|
||||
var homeText: String? = null
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
Log.d(TAG, "fragment onCreateView")
|
||||
_binding = PlayerDetailsFragmentBinding.inflate(inflater)
|
||||
|
||||
binding.imgvCover.setOnClickListener { onPlayPause() }
|
||||
|
||||
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(
|
||||
binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
|
||||
val colorFilter: ColorFilter? = BlendModeColorFilterCompat.createBlendModeColorFilterCompat(binding.txtvPodcastTitle.currentTextColor, BlendModeCompat.SRC_IN)
|
||||
binding.butNextChapter.colorFilter = colorFilter
|
||||
binding.butPrevChapter.colorFilter = colorFilter
|
||||
binding.chapterButton.setOnClickListener {
|
||||
ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG)
|
||||
}
|
||||
binding.chapterButton.setOnClickListener { ChaptersFragment().show(childFragmentManager, ChaptersFragment.TAG) }
|
||||
binding.butPrevChapter.setOnClickListener { seekToPrevChapter() }
|
||||
binding.butNextChapter.setOnClickListener { seekToNextChapter() }
|
||||
|
||||
Log.d(TAG, "fragment onCreateView")
|
||||
webvDescription = binding.webview
|
||||
webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
controller?.seekTo(time!!)
|
||||
}
|
||||
webvDescription.setTimecodeSelectedListener { time: Int? -> controller?.seekTo(time!!) }
|
||||
webvDescription.setPageFinishedListener {
|
||||
// Restoring the scroll position might not always work
|
||||
webvDescription.postDelayed({ this@PlayerDetailsFragment.restoreFromPreference() }, 50)
|
||||
}
|
||||
|
||||
binding.root.addOnLayoutChangeListener(object : OnLayoutChangeListener {
|
||||
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int,
|
||||
bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
|
||||
) {
|
||||
if (binding.root.measuredHeight != webvDescription.minimumHeight) {
|
||||
webvDescription.setMinimumHeight(binding.root.measuredHeight)
|
||||
}
|
||||
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||
if (binding.root.measuredHeight != webvDescription.minimumHeight) webvDescription.setMinimumHeight(binding.root.measuredHeight)
|
||||
binding.root.removeOnLayoutChangeListener(this)
|
||||
}
|
||||
})
|
||||
|
@ -148,7 +145,11 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}
|
||||
if (media is FeedMedia) {
|
||||
val feedMedia = media as FeedMedia
|
||||
item = feedMedia.item
|
||||
if (item?.itemIdentifier != feedMedia.item?.itemIdentifier) {
|
||||
item = feedMedia.item
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
}
|
||||
// Log.d(TAG, "webViewLoader ${item?.id} ${cleanedNotes==null} ${item!!.description==null} ${loadedMediaId == null} ${item?.media?.getIdentifier()} ${media?.getIdentifier()}")
|
||||
if (item != null) {
|
||||
|
@ -166,8 +167,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ data: String? ->
|
||||
webvDescription.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html",
|
||||
"utf-8", "about:blank")
|
||||
webvDescription.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html", "utf-8", "about:blank")
|
||||
Log.d(TAG, "Webview loaded")
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
loadMediaInfo()
|
||||
|
@ -178,11 +178,8 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
|
||||
media = controller?.getMedia()
|
||||
if (media != null) {
|
||||
emitter.onSuccess(media!!)
|
||||
} else {
|
||||
emitter.onComplete()
|
||||
}
|
||||
if (media != null) emitter.onSuccess(media!!)
|
||||
else emitter.onComplete()
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ media: Playable ->
|
||||
|
@ -191,12 +188,41 @@ class PlayerDetailsFragment : Fragment() {
|
|||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
fun buildHomeReaderText() {
|
||||
showHomeText = !showHomeText
|
||||
if (showHomeText) {
|
||||
if (homeText == null && item?.link != null) {
|
||||
runBlocking {
|
||||
val url = item!!.link!!
|
||||
val htmlSource = fetchHtmlSource(url)
|
||||
val readability4J = Readability4J(item!!.link!!, htmlSource)
|
||||
val article = readability4J.parse()
|
||||
val readerhtml = article.contentWithDocumentsCharsetOrUtf8
|
||||
if (readerhtml != null) {
|
||||
val shownotesCleaner = ShownotesCleaner(requireContext(), readerhtml, 0)
|
||||
homeText = shownotesCleaner.processShownotes()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (homeText != null)
|
||||
binding.webview.loadDataWithBaseURL("https://127.0.0.1", homeText!!, "text/html", "UTF-8", null)
|
||||
} else {
|
||||
val shownotesCleaner = ShownotesCleaner(requireContext(), item?.description ?: "", media?.getDuration()?:0)
|
||||
cleanedNotes = shownotesCleaner.processShownotes()
|
||||
if (cleanedNotes != null) binding.webview.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes!!, "text/html", "UTF-8", null)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun displayMediaInfo(media: Playable) {
|
||||
val pubDateStr = DateFormatter.formatAbbrev(context, media.getPubDate())
|
||||
binding.txtvPodcastTitle.text = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||
if (item == null || item!!.media?.getIdentifier() != media.getIdentifier()) {
|
||||
if (media is FeedMedia) {
|
||||
item = media.item
|
||||
if (item?.itemIdentifier != media.item?.itemIdentifier) {
|
||||
item = media.item
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
if (item != null) {
|
||||
val openFeed: Intent = MainActivity.getIntentToOpenFeed(requireContext(), item!!.feedId)
|
||||
binding.txtvPodcastTitle.setOnClickListener { startActivity(openFeed) }
|
||||
|
@ -223,8 +249,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
binding.txtvEpisodeTitle.scrollTo(0, 0)
|
||||
}
|
||||
})
|
||||
val fadeBackIn: ObjectAnimator = ObjectAnimator.ofFloat(
|
||||
binding.txtvEpisodeTitle, "alpha", 1f)
|
||||
val fadeBackIn: ObjectAnimator = ObjectAnimator.ofFloat(binding.txtvEpisodeTitle, "alpha", 1f)
|
||||
val set = AnimatorSet()
|
||||
set.playSequentially(verticalMarquee, fadeOut, fadeBackIn)
|
||||
set.start()
|
||||
|
@ -239,9 +264,7 @@ class PlayerDetailsFragment : Fragment() {
|
|||
private fun updateChapterControlVisibility() {
|
||||
var chapterControlVisible = false
|
||||
when {
|
||||
media?.getChapters() != null -> {
|
||||
chapterControlVisible = media!!.getChapters().isNotEmpty()
|
||||
}
|
||||
media?.getChapters() != null -> chapterControlVisible = media!!.getChapters().isNotEmpty()
|
||||
media is FeedMedia -> {
|
||||
val fm: FeedMedia? = (media as FeedMedia?)
|
||||
// If an item has chapters but they are not loaded yet, still display the button.
|
||||
|
@ -310,23 +333,18 @@ class PlayerDetailsFragment : Fragment() {
|
|||
if (controller == null || curr == null || displayedChapterIndex == -1) return
|
||||
|
||||
when {
|
||||
displayedChapterIndex < 1 -> {
|
||||
controller!!.seekTo(0)
|
||||
}
|
||||
displayedChapterIndex < 1 -> controller!!.seekTo(0)
|
||||
(controller!!.position - 10000 * controller!!.currentPlaybackSpeedMultiplier) < curr.start -> {
|
||||
refreshChapterData(displayedChapterIndex - 1)
|
||||
if (media != null) controller!!.seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
}
|
||||
else -> {
|
||||
controller!!.seekTo(curr.start.toInt())
|
||||
}
|
||||
else -> controller!!.seekTo(curr.start.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun seekToNextChapter() {
|
||||
if (controller == null || media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1 || displayedChapterIndex + 1 >= media!!.getChapters().size) {
|
||||
return
|
||||
}
|
||||
if (controller == null || media == null || media!!.getChapters().isEmpty() || displayedChapterIndex == -1
|
||||
|| displayedChapterIndex + 1 >= media!!.getChapters().size) return
|
||||
|
||||
refreshChapterData(displayedChapterIndex + 1)
|
||||
controller!!.seekTo(media!!.getChapters()[displayedChapterIndex].start.toInt())
|
||||
|
@ -393,7 +411,11 @@ class PlayerDetailsFragment : Fragment() {
|
|||
|
||||
fun setItem(item_: FeedItem) {
|
||||
Log.d(TAG, "setItem ${item_.title}")
|
||||
item = item_
|
||||
if (item?.itemIdentifier != item_.itemIdentifier) {
|
||||
item = item_
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
}
|
||||
|
||||
// override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
|
|
|
@ -64,9 +64,8 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
|
|||
}
|
||||
|
||||
// replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already
|
||||
if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) {
|
||||
if (!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>"))
|
||||
shownotes = shownotes.replace("\n", "<br />")
|
||||
}
|
||||
|
||||
val document = Jsoup.parse(shownotes)
|
||||
cleanCss(document)
|
||||
|
@ -79,10 +78,8 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
|
|||
val elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX)
|
||||
Log.d(TAG, "Recognized " + elementsWithTimeCodes.size + " timecodes")
|
||||
|
||||
if (elementsWithTimeCodes.size == 0) {
|
||||
// No elements with timecodes
|
||||
return
|
||||
}
|
||||
if (elementsWithTimeCodes.size == 0) return // No elements with timecodes
|
||||
|
||||
var useHourFormat = true
|
||||
|
||||
if (playableDuration != Int.MAX_VALUE) {
|
||||
|
@ -107,9 +104,7 @@ class ShownotesCleaner(context: Context, private val rawShownotes: String, priva
|
|||
}
|
||||
}
|
||||
|
||||
if (!useHourFormat) {
|
||||
break
|
||||
}
|
||||
if (!useHourFormat) break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -136,10 +136,10 @@ object WidgetUpdater {
|
|||
} else {
|
||||
views.setViewVisibility(R.id.layout_center, View.VISIBLE)
|
||||
}
|
||||
val showPlaybackSpeed = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + id, false)
|
||||
val showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false)
|
||||
val showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false)
|
||||
val showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false)
|
||||
val showPlaybackSpeed = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + id, true)
|
||||
val showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, true)
|
||||
val showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, true)
|
||||
val showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, true)
|
||||
|
||||
if (showPlaybackSpeed || showRewind || showSkip || showFastForward) {
|
||||
views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE)
|
||||
|
|
|
@ -8,9 +8,8 @@ import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.createInstanceF
|
|||
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
|
||||
class WidgetUpdaterWorker(context: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : Worker(context, workerParams) {
|
||||
class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
try {
|
||||
updateWidget()
|
||||
|
@ -27,13 +26,9 @@ class WidgetUpdaterWorker(context: Context,
|
|||
private fun updateWidget() {
|
||||
val media = createInstanceFromPreferences(applicationContext)
|
||||
if (media != null) {
|
||||
WidgetUpdater.updateWidget(applicationContext,
|
||||
WidgetState(media, PlayerStatus.STOPPED,
|
||||
media.getPosition(), media.getDuration(),
|
||||
getCurrentPlaybackSpeed(media)))
|
||||
WidgetUpdater.updateWidget(applicationContext, WidgetState(media, PlayerStatus.STOPPED, media.getPosition(), media.getDuration(), getCurrentPlaybackSpeed(media)))
|
||||
} else {
|
||||
WidgetUpdater.updateWidget(applicationContext,
|
||||
WidgetState(PlayerStatus.STOPPED))
|
||||
WidgetUpdater.updateWidget(applicationContext, WidgetState(PlayerStatus.STOPPED))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,4 +23,10 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
android:id="@+id/reader_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -2,6 +2,13 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/show_home_reader_view"
|
||||
android:icon="@drawable/baseline_home_24"
|
||||
android:title="@string/home_label"
|
||||
custom:showAsAction="always">
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/show_video"
|
||||
android:icon="@drawable/baseline_fullscreen_24"
|
||||
|
|
|
@ -282,4 +282,10 @@
|
|||
* 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
|
||||
* RSS feeds with no playable media can be subscribed and read/listened via the above two ways
|
||||
|
||||
## 4.9.1
|
||||
|
||||
* reader mode of episode home view observes the theme of the app
|
||||
* reader mode content of episode home view is cached so that subsequent loading is quicker
|
||||
* episode home reader content can be switched on in player detailed view from the action bar
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
Version 4.9.1 brings several changes:
|
||||
|
||||
* reader mode of episode home view observes the theme of the app
|
||||
* reader mode content of episode home view is cached so that subsequent loading is quicker
|
||||
* episode home reader content can be switched on in player detailed view from the action bar
|
Loading…
Reference in New Issue