4.9.1 commit

This commit is contained in:
Xilin Jia 2024-04-23 22:54:22 +01:00
parent 61c61f4df1
commit c2fb0f4a31
22 changed files with 2257 additions and 398 deletions

View File

@ -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 {

View File

@ -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>

View File

@ -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
)
}
}

View File

@ -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 {

View File

@ -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(),
);
}

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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()
}
}
}

View File

@ -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())

View File

@ -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) {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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>

View File

@ -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"

View File

@ -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

View File

@ -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