tuning and bug fixes

This commit is contained in:
Xilin Jia 2024-02-15 15:10:16 +01:00
parent 9a96130fa7
commit 0e36e6f39e
14 changed files with 406 additions and 394 deletions

1
.gitignore vendored
View File

@ -15,6 +15,7 @@ target/
build/ build/
**/*.project **/*.project
**/*.classpath **/*.classpath
**/.directory
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties

View File

@ -22,8 +22,8 @@ android {
// Version code schema: // Version code schema:
// "1.2.3-beta4" -> 1020304 // "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395 // "1.2.3" -> 1020395
versionCode 3020095 versionCode 3020096
versionName "3.2.0" versionName "3.2.1"
def commit = "" def commit = ""
try { try {
@ -65,6 +65,12 @@ android {
signingConfig signingConfigs.releaseConfig signingConfig signingConfigs.releaseConfig
} }
} }
applicationVariants.all { variant ->
variant.outputs.all { output ->
def applicationName = "PodVinci"
outputFileName = "${applicationName}_${variant.buildType.name}_${defaultConfig.versionName}.apk"
}
}
androidResources { androidResources {

View File

@ -22,6 +22,7 @@ import java.lang.ref.WeakReference
*/ */
open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapter<EpisodeItemViewHolder?>(mainActivity), open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapter<EpisodeItemViewHolder?>(mainActivity),
View.OnCreateContextMenuListener { View.OnCreateContextMenuListener {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity) private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private var episodes: List<FeedItem> = ArrayList() private var episodes: List<FeedItem> = ArrayList()
var longPressedItem: FeedItem? = null var longPressedItem: FeedItem? = null
@ -178,18 +179,22 @@ open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapte
} }
fun onContextItemSelected(item: MenuItem): Boolean { fun onContextItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.multi_select) { when (item.itemId) {
startSelectMode(longPressedPosition) R.id.multi_select -> {
return true startSelectMode(longPressedPosition)
} else if (item.itemId == R.id.select_all_above) { return true
setSelected(0, longPressedPosition, true) }
return true R.id.select_all_above -> {
} else if (item.itemId == R.id.select_all_below) { setSelected(0, longPressedPosition, true)
shouldSelectLazyLoadedItems = true return true
setSelected(longPressedPosition + 1, itemCount, true) }
return true R.id.select_all_below -> {
shouldSelectLazyLoadedItems = true
setSelected(longPressedPosition + 1, itemCount, true)
return true
}
else -> return false
} }
return false
} }
val selectedItems: List<Any> val selectedItems: List<Any>

View File

@ -12,6 +12,7 @@ import ac.mdiq.podvinci.R
*/ */
abstract class SelectableAdapter<T : RecyclerView.ViewHolder?>(private val activity: Activity) : abstract class SelectableAdapter<T : RecyclerView.ViewHolder?>(private val activity: Activity) :
RecyclerView.Adapter<T>() { RecyclerView.Adapter<T>() {
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val selectedIds = HashSet<Long>() private val selectedIds = HashSet<Long>()
private var onSelectModeListener: OnSelectModeListener? = null private var onSelectModeListener: OnSelectModeListener? = null

View File

@ -523,7 +523,8 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
.subscribe( .subscribe(
{ result: Feed? -> { result: Feed? ->
feed = result feed = result
if (feed != null) swipeActions?.setFilter(feed!!.itemFilter) Log.d(TAG, "loadItems subscribe called ${feed?.title}")
swipeActions?.setFilter(feed?.itemFilter)
refreshHeaderView() refreshHeaderView()
viewBinding!!.progressBar.visibility = View.GONE viewBinding!!.progressBar.visibility = View.GONE
adapter?.setDummyViews(0) adapter?.setDummyViews(0)
@ -541,11 +542,12 @@ class FeedItemlistFragment : Fragment(), AdapterView.OnItemClickListener, Toolba
private fun loadData(): Feed? { private fun loadData(): Feed? {
val feed: Feed = DBReader.getFeed(feedID, true) ?: return null val feed: Feed = DBReader.getFeed(feedID, true) ?: return null
Log.d(TAG, "loadData got feed ${feed.title} with items: ${feed.items.size} ${feed.items[0].getPubDate()}")
if (feed.items.isNotEmpty()) { if (feed.items.isNotEmpty()) {
DBReader.loadAdditionalFeedItemListData(feed.items) DBReader.loadAdditionalFeedItemListData(feed.items)
if (feed.sortOrder != null) { if (feed.sortOrder != null) {
val feedItems: MutableList<FeedItem> = feed.items val feedItems: MutableList<FeedItem> = feed.items
FeedItemPermutors.getPermutor(feed.sortOrder!!).reorder(feedItems.toMutableList()) FeedItemPermutors.getPermutor(feed.sortOrder!!).reorder(feedItems)
feed.items = feedItems feed.items = feedItems
} }
} }

View File

@ -11,7 +11,6 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultCallback
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
@ -33,12 +32,10 @@ import ac.mdiq.podvinci.core.export.ExportWriter
import ac.mdiq.podvinci.core.export.favorites.FavoritesWriter import ac.mdiq.podvinci.core.export.favorites.FavoritesWriter
import ac.mdiq.podvinci.core.export.html.HtmlWriter import ac.mdiq.podvinci.core.export.html.HtmlWriter
import ac.mdiq.podvinci.core.export.opml.OpmlWriter import ac.mdiq.podvinci.core.export.opml.OpmlWriter
import ac.mdiq.podvinci.core.storage.DatabaseExporter import ac.mdiq.podvinci.core.storage.DatabaseTransporter
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import io.reactivex.functions.Action
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -237,7 +234,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
} }
val uri = result.data!!.data val uri = result.data!!.data
progressDialog!!.show() progressDialog!!.show()
disposable = Completable.fromAction { DatabaseExporter.importBackup(uri, requireContext()) } disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
@ -251,7 +248,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
return return
} }
progressDialog!!.show() progressDialog!!.show()
disposable = Completable.fromAction { DatabaseExporter.exportToDocument(uri, requireContext()) } disposable = Completable.fromAction { DatabaseTransporter.exportToDocument(uri, requireContext()) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({

View File

@ -1,5 +1,12 @@
package ac.mdiq.podvinci.core.service.playback package ac.mdiq.podvinci.core.service.playback
import ac.mdiq.podvinci.core.ClientConfig
import ac.mdiq.podvinci.core.R
import ac.mdiq.podvinci.core.service.download.HttpCredentialEncoder
import ac.mdiq.podvinci.core.service.download.PodVinciHttpClient
import ac.mdiq.podvinci.core.util.NetworkUtils.wasDownloadBlocked
import ac.mdiq.podvinci.model.playback.Playable
import ac.mdiq.podvinci.storage.preferences.UserPreferences
import android.content.Context import android.content.Context
import android.media.audiofx.LoudnessEnhancer import android.media.audiofx.LoudnessEnhancer
import android.net.Uri import android.net.Uri
@ -13,11 +20,9 @@ import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DefaultDataSourceFactory import androidx.media3.datasource.DefaultDataSourceFactory
import androidx.media3.datasource.HttpDataSource.HttpDataSourceException
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.*
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.SeekParameters
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
@ -27,39 +32,37 @@ import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.mp3.Mp3Extractor import androidx.media3.extractor.mp3.Mp3Extractor
import androidx.media3.ui.DefaultTrackNameProvider import androidx.media3.ui.DefaultTrackNameProvider
import androidx.media3.ui.TrackNameProvider import androidx.media3.ui.TrackNameProvider
import ac.mdiq.podvinci.core.ClientConfig
import ac.mdiq.podvinci.core.service.download.PodVinciHttpClient
import ac.mdiq.podvinci.core.service.download.HttpCredentialEncoder
import ac.mdiq.podvinci.model.playback.Playable
import ac.mdiq.podvinci.storage.preferences.UserPreferences
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@UnstableApi @UnstableApi
class ExoPlayerWrapper internal constructor(private val context: Context) { class ExoPlayerWrapper internal constructor(private val context: Context) {
// TODO: need to experiment this, 5 seconds for now
private val bufferUpdateInterval = 5L
private val bufferingUpdateDisposable: Disposable private val bufferingUpdateDisposable: Disposable
private var exoPlayer: ExoPlayer? = null private lateinit var exoPlayer: ExoPlayer
private lateinit var trackSelector: DefaultTrackSelector
private var loudnessEnhancer: LoudnessEnhancer? = null
private var mediaSource: MediaSource? = null private var mediaSource: MediaSource? = null
private var audioSeekCompleteListener: Runnable? = null private var audioSeekCompleteListener: Runnable? = null
private var audioCompletionListener: Runnable? = null private var audioCompletionListener: Runnable? = null
private var audioErrorListener: Consumer<String>? = null private var audioErrorListener: Consumer<String>? = null
private var bufferingUpdateListener: Consumer<Int>? = null private var bufferingUpdateListener: Consumer<Int>? = null
private var playbackParameters: PlaybackParameters
private var trackSelector: DefaultTrackSelector? = null
private var loudnessEnhancer: LoudnessEnhancer? = null private var playbackParameters: PlaybackParameters
init { init {
createPlayer() createPlayer()
playbackParameters = exoPlayer!!.playbackParameters playbackParameters = exoPlayer.playbackParameters
bufferingUpdateDisposable = Observable.interval(2, TimeUnit.SECONDS) bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { tickNumber: Long? -> .subscribe { tickNumber: Long? ->
if (bufferingUpdateListener != null) { bufferingUpdateListener?.accept(exoPlayer.bufferedPercentage)
bufferingUpdateListener!!.accept(exoPlayer!!.bufferedPercentage)
}
} }
} }
@ -70,56 +73,53 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS) DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS)
loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true) loadControl.setBackBuffer(UserPreferences.rewindSecs * 1000 + 500, true)
trackSelector = DefaultTrackSelector(context) trackSelector = DefaultTrackSelector(context)
val audioOffloadPreferences = val audioOffloadPreferences = AudioOffloadPreferences.Builder()
AudioOffloadPreferences.Builder() .setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed
.setAudioOffloadMode(AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED) // Add additional options as needed .setIsGaplessSupportRequired(true)
.setIsGaplessSupportRequired(true) .build()
.build()
exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context)) exoPlayer = ExoPlayer.Builder(context, DefaultRenderersFactory(context))
.setTrackSelector(trackSelector!!) .setTrackSelector(trackSelector)
.setLoadControl(loadControl.build()) .setLoadControl(loadControl.build())
.build() .build()
exoPlayer!!.setSeekParameters(SeekParameters.EXACT) exoPlayer.setSeekParameters(SeekParameters.EXACT)
exoPlayer!!.trackSelectionParameters = exoPlayer!!.trackSelectionParameters exoPlayer.trackSelectionParameters = exoPlayer.trackSelectionParameters
.buildUpon() .buildUpon()
.setAudioOffloadPreferences(audioOffloadPreferences) .setAudioOffloadPreferences(audioOffloadPreferences)
.build() .build()
exoPlayer!!.addListener(object : Player.Listener { exoPlayer.addListener(object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: @Player.State Int) { override fun onPlaybackStateChanged(playbackState: @Player.State Int) {
if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) { if (audioCompletionListener != null && playbackState == Player.STATE_ENDED) {
audioCompletionListener!!.run() audioCompletionListener?.run()
} else if (bufferingUpdateListener != null && playbackState == Player.STATE_BUFFERING) { } else if (playbackState == Player.STATE_BUFFERING) {
bufferingUpdateListener!!.accept(BUFFERING_STARTED) bufferingUpdateListener?.accept(BUFFERING_STARTED)
} else if (bufferingUpdateListener != null) { } else {
bufferingUpdateListener!!.accept(BUFFERING_ENDED) bufferingUpdateListener?.accept(BUFFERING_ENDED)
}
}
override fun onPlayerError(error: PlaybackException) {
if (wasDownloadBlocked(error)) {
audioErrorListener?.accept(context.getString(R.string.download_error_blocked))
} else {
var cause = error.cause
if (cause is HttpDataSourceException) {
if (cause.cause != null) {
cause = cause.cause
}
}
if (cause != null && "Source error" == cause.message) {
cause = cause.cause
}
audioErrorListener?.accept(if (cause != null) cause.message else error.message)
} }
} }
// @Override
// public void onPlayerError(@NonNull ExoPlaybackException error) {
// if (audioErrorListener != null) {
// if (NetworkUtils.wasDownloadBlocked(error)) {
// audioErrorListener.accept(context.getString(R.string.download_error_blocked));
// } else {
// Throwable cause = error.getCause();
// if (cause instanceof HttpDataSource.HttpDataSourceException) {
// if (cause.getCause() != null) {
// cause = cause.getCause();
// }
// }
// if (cause != null && "Source error".equals(cause.getMessage())) {
// cause = cause.getCause();
// }
// audioErrorListener.accept(cause != null ? cause.getMessage() : error.getMessage());
// }
// }
// }
override fun onPositionDiscontinuity(oldPosition: PositionInfo, override fun onPositionDiscontinuity(oldPosition: PositionInfo,
newPosition: PositionInfo, newPosition: PositionInfo,
reason: @DiscontinuityReason Int reason: @DiscontinuityReason Int
) { ) {
if (audioSeekCompleteListener != null && reason == Player.DISCONTINUITY_REASON_SEEK) { if (reason == Player.DISCONTINUITY_REASON_SEEK) {
audioSeekCompleteListener!!.run() audioSeekCompleteListener?.run()
} }
} }
@ -128,41 +128,40 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
}) })
initLoudnessEnhancer(exoPlayer!!.audioSessionId) initLoudnessEnhancer(exoPlayer.audioSessionId)
} }
val currentPosition: Int val currentPosition: Int
get() = exoPlayer!!.currentPosition.toInt() get() = exoPlayer.currentPosition.toInt()
val currentSpeedMultiplier: Float val currentSpeedMultiplier: Float
get() = playbackParameters.speed get() = playbackParameters.speed
val duration: Int val duration: Int
get() { get() {
if (exoPlayer!!.duration == C.TIME_UNSET) { if (exoPlayer.duration == C.TIME_UNSET) {
return Playable.INVALID_TIME return Playable.INVALID_TIME
} }
return exoPlayer!!.duration.toInt() return exoPlayer.duration.toInt()
} }
val isPlaying: Boolean val isPlaying: Boolean
get() = exoPlayer!!.playWhenReady get() = exoPlayer.playWhenReady
fun pause() { fun pause() {
exoPlayer!!.pause() exoPlayer.pause()
} }
@Throws(IllegalStateException::class) @Throws(IllegalStateException::class)
fun prepare() { fun prepare() {
exoPlayer!!.setMediaSource(mediaSource!!, false) exoPlayer.setMediaSource(mediaSource!!, false)
exoPlayer!!.prepare() exoPlayer.prepare()
} }
fun release() { fun release() {
bufferingUpdateDisposable.dispose() bufferingUpdateDisposable.dispose()
if (exoPlayer != null) { exoPlayer.release()
exoPlayer!!.release()
}
audioSeekCompleteListener = null audioSeekCompleteListener = null
audioCompletionListener = null audioCompletionListener = null
audioErrorListener = null audioErrorListener = null
@ -170,25 +169,23 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
fun reset() { fun reset() {
exoPlayer!!.release() exoPlayer.release()
createPlayer() createPlayer()
} }
@Throws(IllegalStateException::class) @Throws(IllegalStateException::class)
fun seekTo(i: Int) { fun seekTo(i: Int) {
exoPlayer!!.seekTo(i.toLong()) exoPlayer.seekTo(i.toLong())
if (audioSeekCompleteListener != null) { audioSeekCompleteListener?.run()
audioSeekCompleteListener!!.run()
}
} }
fun setAudioStreamType(i: Int) { fun setAudioStreamType(i: Int) {
val a = exoPlayer!!.audioAttributes val a = exoPlayer.audioAttributes
val b = AudioAttributes.Builder() val b = AudioAttributes.Builder()
b.setContentType(i) b.setContentType(i)
b.setFlags(a.flags) b.setFlags(a.flags)
b.setUsage(a.usage) b.setUsage(a.usage)
exoPlayer!!.setAudioAttributes(b.build(), false) exoPlayer.setAudioAttributes(b.build(), false)
} }
@Throws(IllegalArgumentException::class, IllegalStateException::class) @Throws(IllegalArgumentException::class, IllegalStateException::class)
@ -222,34 +219,34 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
fun setDisplay(sh: SurfaceHolder?) { fun setDisplay(sh: SurfaceHolder?) {
exoPlayer!!.setVideoSurfaceHolder(sh) exoPlayer.setVideoSurfaceHolder(sh)
} }
fun setPlaybackParams(speed: Float, skipSilence: Boolean) { fun setPlaybackParams(speed: Float, skipSilence: Boolean) {
playbackParameters = PlaybackParameters(speed, playbackParameters.pitch) playbackParameters = PlaybackParameters(speed, playbackParameters.pitch)
exoPlayer!!.skipSilenceEnabled = skipSilence exoPlayer.skipSilenceEnabled = skipSilence
exoPlayer!!.playbackParameters = playbackParameters exoPlayer.playbackParameters = playbackParameters
} }
fun setVolume(v: Float, v1: Float) { fun setVolume(v: Float, v1: Float) {
if (v > 1) { if (v > 1) {
exoPlayer!!.volume = 1f exoPlayer.volume = 1f
loudnessEnhancer!!.setEnabled(true) loudnessEnhancer?.setEnabled(true)
loudnessEnhancer!!.setTargetGain((1000 * (v - 1)).toInt()) loudnessEnhancer?.setTargetGain((1000 * (v - 1)).toInt())
} else { } else {
exoPlayer!!.volume = v exoPlayer.volume = v
loudnessEnhancer!!.setEnabled(false) loudnessEnhancer?.setEnabled(false)
} }
} }
fun start() { fun start() {
exoPlayer!!.play() exoPlayer.play()
// Can't set params when paused - so always set it on start in case they changed // Can't set params when paused - so always set it on start in case they changed
exoPlayer!!.playbackParameters = playbackParameters exoPlayer.playbackParameters = playbackParameters
} }
fun stop() { fun stop() {
exoPlayer!!.stop() exoPlayer.stop()
} }
val audioTracks: List<String> val audioTracks: List<String>
@ -264,8 +261,8 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
private val formats: List<Format> private val formats: List<Format>
get() { get() {
val formats: MutableList<Format> = ArrayList() val formats: MutableList<Format> = arrayListOf()
val trackInfo = trackSelector!!.currentMappedTrackInfo val trackInfo = trackSelector.currentMappedTrackInfo
?: return emptyList() ?: return emptyList()
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
for (i in 0 until trackGroups.length) { for (i in 0 until trackGroups.length) {
@ -275,19 +272,19 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
} }
fun setAudioTrack(track: Int) { fun setAudioTrack(track: Int) {
val trackInfo = trackSelector!!.currentMappedTrackInfo ?: return val trackInfo = trackSelector.currentMappedTrackInfo ?: return
val trackGroups = trackInfo.getTrackGroups(audioRendererIndex) val trackGroups = trackInfo.getTrackGroups(audioRendererIndex)
val override = SelectionOverride(track, 0) val override = SelectionOverride(track, 0)
val rendererIndex = audioRendererIndex val rendererIndex = audioRendererIndex
val params = trackSelector!!.buildUponParameters() val params = trackSelector.buildUponParameters()
.setSelectionOverride(rendererIndex, trackGroups, override) .setSelectionOverride(rendererIndex, trackGroups, override)
trackSelector!!.setParameters(params) trackSelector.setParameters(params)
} }
private val audioRendererIndex: Int private val audioRendererIndex: Int
get() { get() {
for (i in 0 until exoPlayer!!.rendererCount) { for (i in 0 until exoPlayer.rendererCount) {
if (exoPlayer!!.getRendererType(i) == C.TRACK_TYPE_AUDIO) { if (exoPlayer.getRendererType(i) == C.TRACK_TYPE_AUDIO) {
return i return i
} }
} }
@ -296,7 +293,7 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
val selectedAudioTrack: Int val selectedAudioTrack: Int
get() { get() {
val trackSelections = exoPlayer!!.currentTrackSelections val trackSelections = exoPlayer.currentTrackSelections
val availableFormats = formats val availableFormats = formats
for (i in 0 until trackSelections.length) { for (i in 0 until trackSelections.length) {
val track = trackSelections[i] as ExoTrackSelection? ?: continue val track = trackSelections[i] as ExoTrackSelection? ?: continue
@ -321,18 +318,18 @@ class ExoPlayerWrapper internal constructor(private val context: Context) {
val videoWidth: Int val videoWidth: Int
get() { get() {
if (exoPlayer!!.videoFormat == null) { if (exoPlayer.videoFormat == null) {
return 0 return 0
} }
return exoPlayer!!.videoFormat!!.width return exoPlayer.videoFormat!!.width
} }
val videoHeight: Int val videoHeight: Int
get() { get() {
if (exoPlayer!!.videoFormat == null) { if (exoPlayer.videoFormat == null) {
return 0 return 0
} }
return exoPlayer!!.videoFormat!!.height return exoPlayer.videoFormat!!.height
} }
fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer<Int>?) { fun setOnBufferingUpdateListener(bufferingUpdateListener: Consumer<Int>?) {

View File

@ -108,6 +108,8 @@ import ac.mdiq.podvinci.storage.preferences.UserPreferences.showSkipOnFullNotifi
import ac.mdiq.podvinci.storage.preferences.UserPreferences.videoPlaybackSpeed import ac.mdiq.podvinci.storage.preferences.UserPreferences.videoPlaybackSpeed
import ac.mdiq.podvinci.ui.appstartintent.MainActivityStarter import ac.mdiq.podvinci.ui.appstartintent.MainActivityStarter
import ac.mdiq.podvinci.ui.appstartintent.VideoPlayerActivityStarter import ac.mdiq.podvinci.ui.appstartintent.VideoPlayerActivityStarter
import android.os.Build.VERSION_CODES
import androidx.core.content.ContextCompat
import io.reactivex.* import io.reactivex.*
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
@ -127,11 +129,12 @@ import kotlin.math.max
@UnstableApi @UnstableApi
class PlaybackService : MediaBrowserServiceCompat() { class PlaybackService : MediaBrowserServiceCompat() {
private var mediaPlayer: PlaybackServiceMediaPlayer? = null private var mediaPlayer: PlaybackServiceMediaPlayer? = null
private var taskManager: PlaybackServiceTaskManager? = null
private var stateManager: PlaybackServiceStateManager? = null
private var positionEventTimer: Disposable? = null private var positionEventTimer: Disposable? = null
private var notificationBuilder: PlaybackServiceNotificationBuilder? = null
private var castStateListener: CastStateListener? = null private lateinit var taskManager: PlaybackServiceTaskManager
private lateinit var stateManager: PlaybackServiceStateManager
private lateinit var notificationBuilder: PlaybackServiceNotificationBuilder
private lateinit var castStateListener: CastStateListener
private var autoSkippedFeedMediaId: String? = null private var autoSkippedFeedMediaId: String? = null
private var clickCount = 0 private var clickCount = 0
@ -162,9 +165,19 @@ class PlaybackService : MediaBrowserServiceCompat() {
stateManager = PlaybackServiceStateManager(this) stateManager = PlaybackServiceStateManager(this)
notificationBuilder = PlaybackServiceNotificationBuilder(this) notificationBuilder = PlaybackServiceNotificationBuilder(this)
// TODO: this shit doesn't work
// if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
// registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_EXPORTED)
// registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE), RECEIVER_EXPORTED)
// } else {
// ContextCompat.registerReceiver(applicationContext, autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), ContextCompat.RECEIVER_EXPORTED)
// ContextCompat.registerReceiver(applicationContext, shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE), ContextCompat.RECEIVER_EXPORTED)
// }
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS")) registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"))
registerReceiver(headsetDisconnected, IntentFilter(Intent.ACTION_HEADSET_PLUG))
registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE))
registerReceiver(headsetDisconnected, IntentFilter(Intent.ACTION_HEADSET_PLUG))
registerReceiver(bluetoothStateUpdated, IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) registerReceiver(bluetoothStateUpdated, IntentFilter(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED))
registerReceiver(audioBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) registerReceiver(audioBecomingNoisy, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
EventBus.getDefault().register(this) EventBus.getDefault().register(this)
@ -235,8 +248,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
super.onDestroy() super.onDestroy()
Log.d(TAG, "Service is about to be destroyed") Log.d(TAG, "Service is about to be destroyed")
if (notificationBuilder!!.playerStatus == PlayerStatus.PLAYING) { if (notificationBuilder.playerStatus == PlayerStatus.PLAYING) {
notificationBuilder!!.playerStatus = PlayerStatus.STOPPED notificationBuilder.playerStatus = PlayerStatus.STOPPED
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
if (ActivityCompat.checkSelfPermission(this, if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
@ -249,25 +262,24 @@ class PlaybackService : MediaBrowserServiceCompat() {
// for ActivityCompat#requestPermissions for more details. // for ActivityCompat#requestPermissions for more details.
return return
} }
notificationManager.notify(R.id.notification_playing, notificationBuilder!!.build()) notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
} }
stateManager!!.stopForeground(!isPersistNotify) stateManager.stopForeground(!isPersistNotify)
isRunning = false isRunning = false
currentMediaType = MediaType.UNKNOWN currentMediaType = MediaType.UNKNOWN
castStateListener!!.destroy() castStateListener.destroy()
cancelPositionObserver() cancelPositionObserver()
if (mediaSession != null) { mediaSession?.release()
mediaSession!!.release() mediaSession = null
mediaSession = null
}
unregisterReceiver(autoStateUpdated) unregisterReceiver(autoStateUpdated)
unregisterReceiver(headsetDisconnected) unregisterReceiver(headsetDisconnected)
unregisterReceiver(shutdownReceiver) unregisterReceiver(shutdownReceiver)
unregisterReceiver(bluetoothStateUpdated) unregisterReceiver(bluetoothStateUpdated)
unregisterReceiver(audioBecomingNoisy) unregisterReceiver(audioBecomingNoisy)
mediaPlayer!!.shutdown() mediaPlayer?.shutdown()
taskManager!!.shutdown() taskManager.shutdown()
EventBus.getDefault().unregister(this) EventBus.getDefault().unregister(this)
} }
@ -428,7 +440,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
super.onStartCommand(intent, flags, startId) super.onStartCommand(intent, flags, startId)
Log.d(TAG, "OnStartCommand called") Log.d(TAG, "OnStartCommand called")
stateManager!!.startForeground(R.id.notification_playing, notificationBuilder!!.build()) stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
notificationManager.cancel(R.id.notification_streaming_confirmation) notificationManager.cancel(R.id.notification_streaming_confirmation)
@ -438,13 +450,13 @@ class PlaybackService : MediaBrowserServiceCompat() {
val playable = intent.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE) val playable = intent.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
if (keycode == -1 && playable == null && customAction == null) { if (keycode == -1 && playable == null && customAction == null) {
Log.e(TAG, "PlaybackService was started with no arguments") Log.e(TAG, "PlaybackService was started with no arguments")
stateManager!!.stopService() stateManager.stopService()
return START_NOT_STICKY return START_NOT_STICKY
} }
if ((flags and START_FLAG_REDELIVERY) != 0) { if ((flags and START_FLAG_REDELIVERY) != 0) {
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.") Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.")
stateManager!!.stopForeground(true) stateManager.stopForeground(true)
} else { } else {
if (keycode != -1) { if (keycode != -1) {
val notificationButton: Boolean val notificationButton: Boolean
@ -456,12 +468,12 @@ class PlaybackService : MediaBrowserServiceCompat() {
notificationButton = true notificationButton = true
} }
val handled = handleKeycode(keycode, notificationButton) val handled = handleKeycode(keycode, notificationButton)
if (!handled && !stateManager!!.hasReceivedValidStartCommand()) { if (!handled && !stateManager.hasReceivedValidStartCommand()) {
stateManager!!.stopService() stateManager.stopService()
return START_NOT_STICKY return START_NOT_STICKY
} }
} else if (playable != null) { } else if (playable != null) {
stateManager!!.validStartCommandWasReceived() stateManager.validStartCommandWasReceived()
val allowStreamThisTime = intent.getBooleanExtra( val allowStreamThisTime = intent.getBooleanExtra(
PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false) PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false)
val allowStreamAlways = intent.getBooleanExtra( val allowStreamAlways = intent.getBooleanExtra(
@ -484,11 +496,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
{ error: Throwable -> { error: Throwable ->
Log.d(TAG, "Playable was not found. Stopping service.") Log.d(TAG, "Playable was not found. Stopping service.")
error.printStackTrace() error.printStackTrace()
stateManager!!.stopService() stateManager.stopService()
}) })
return START_NOT_STICKY return START_NOT_STICKY
} else { } else {
mediaSession!!.controller.transportControls.sendCustomAction(customAction, null) mediaSession?.controller?.transportControls?.sendCustomAction(customAction, null)
} }
} }
@ -508,7 +520,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
val duration = duration val duration = duration
if (skipIntro * 1000 < duration || duration <= 0) { if (skipIntro * 1000 < duration || duration <= 0) {
Log.d(TAG, "skipIntro " + playable.getEpisodeTitle()) Log.d(TAG, "skipIntro " + playable.getEpisodeTitle())
mediaPlayer!!.seekTo(skipIntro * 1000) mediaPlayer?.seekTo(skipIntro * 1000)
val skipIntroMesg = context.getString(R.string.pref_feed_skip_intro_toast, val skipIntroMesg = context.getString(R.string.pref_feed_skip_intro_toast,
skipIntro) skipIntro)
val toast = Toast.makeText(context, skipIntroMesg, val toast = Toast.makeText(context, skipIntroMesg,
@ -529,7 +541,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
val intentAllowThisTime = Intent(originalIntent) val intentAllowThisTime = Intent(originalIntent)
intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME) intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME)
intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true) intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true)
val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
PendingIntent.getForegroundService(this, PendingIntent.getForegroundService(this,
R.id.pending_intent_allow_stream_this_time, intentAllowThisTime, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
@ -542,7 +554,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
val intentAlwaysAllow = Intent(intentAllowThisTime) val intentAlwaysAllow = Intent(intentAllowThisTime)
intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS) intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS)
intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true) intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true)
val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
PendingIntent.getForegroundService(this, PendingIntent.getForegroundService(this,
R.id.pending_intent_allow_stream_always, intentAlwaysAllow, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
@ -589,44 +601,56 @@ class PlaybackService : MediaBrowserServiceCompat() {
*/ */
private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean { private fun handleKeycode(keycode: Int, notificationButton: Boolean): Boolean {
Log.d(TAG, "Handling keycode: $keycode") Log.d(TAG, "Handling keycode: $keycode")
val info = mediaPlayer!!.pSMPInfo val info = mediaPlayer?.pSMPInfo
val status = info.playerStatus val status = info?.playerStatus
when (keycode) { when (keycode) {
KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> { KeyEvent.KEYCODE_HEADSETHOOK, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (status == PlayerStatus.PLAYING) { when {
mediaPlayer!!.pause(!isPersistNotify, false) status == PlayerStatus.PLAYING -> {
} else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { mediaPlayer?.pause(!isPersistNotify, false)
mediaPlayer!!.resume() }
} else if (status == PlayerStatus.PREPARING) { status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> {
mediaPlayer!!.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) mediaPlayer?.resume()
} else if (status == PlayerStatus.INITIALIZED) { }
mediaPlayer!!.setStartWhenPrepared(true) status == PlayerStatus.PREPARING -> {
mediaPlayer!!.prepare() mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared())
} else if (mediaPlayer!!.getPlayable() == null) { }
startPlayingFromPreferences() status == PlayerStatus.INITIALIZED -> {
} else { mediaPlayer?.setStartWhenPrepared(true)
return false mediaPlayer?.prepare()
}
mediaPlayer?.getPlayable() == null -> {
startPlayingFromPreferences()
}
else -> {
return false
}
} }
taskManager!!.restartSleepTimer() taskManager.restartSleepTimer()
return true return true
} }
KeyEvent.KEYCODE_MEDIA_PLAY -> { KeyEvent.KEYCODE_MEDIA_PLAY -> {
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { when {
mediaPlayer!!.resume() status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED -> {
} else if (status == PlayerStatus.INITIALIZED) { mediaPlayer?.resume()
mediaPlayer!!.setStartWhenPrepared(true) }
mediaPlayer!!.prepare() status == PlayerStatus.INITIALIZED -> {
} else if (mediaPlayer!!.getPlayable() == null) { mediaPlayer?.setStartWhenPrepared(true)
startPlayingFromPreferences() mediaPlayer?.prepare()
} else { }
return false mediaPlayer?.getPlayable() == null -> {
startPlayingFromPreferences()
}
else -> {
return false
}
} }
taskManager!!.restartSleepTimer() taskManager.restartSleepTimer()
return true return true
} }
KeyEvent.KEYCODE_MEDIA_PAUSE -> { KeyEvent.KEYCODE_MEDIA_PAUSE -> {
if (status == PlayerStatus.PLAYING) { if (status == PlayerStatus.PLAYING) {
mediaPlayer!!.pause(!isPersistNotify, false) mediaPlayer?.pause(!isPersistNotify, false)
return true return true
} }
return false return false
@ -636,14 +660,14 @@ class PlaybackService : MediaBrowserServiceCompat() {
// Handle remapped button as notification button which is not remapped again. // Handle remapped button as notification button which is not remapped again.
return handleKeycode(hardwareForwardButton, true) return handleKeycode(hardwareForwardButton, true)
} else if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { } else if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer!!.skip() mediaPlayer?.skip()
return true return true
} }
return false return false
} }
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> { KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer!!.seekDelta(fastForwardSecs * 1000) mediaPlayer?.seekDelta(fastForwardSecs * 1000)
return true return true
} }
return false return false
@ -653,29 +677,29 @@ class PlaybackService : MediaBrowserServiceCompat() {
// Handle remapped button as notification button which is not remapped again. // Handle remapped button as notification button which is not remapped again.
return handleKeycode(hardwarePreviousButton, true) return handleKeycode(hardwarePreviousButton, true)
} else if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { } else if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer!!.seekTo(0) mediaPlayer?.seekTo(0)
return true return true
} }
return false return false
} }
KeyEvent.KEYCODE_MEDIA_REWIND -> { KeyEvent.KEYCODE_MEDIA_REWIND -> {
if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) { if (this.status == PlayerStatus.PLAYING || this.status == PlayerStatus.PAUSED) {
mediaPlayer!!.seekDelta(-rewindSecs * 1000) mediaPlayer?.seekDelta(-rewindSecs * 1000)
return true return true
} }
return false return false
} }
KeyEvent.KEYCODE_MEDIA_STOP -> { KeyEvent.KEYCODE_MEDIA_STOP -> {
if (status == PlayerStatus.PLAYING) { if (status == PlayerStatus.PLAYING) {
mediaPlayer!!.pause(true, true) mediaPlayer?.pause(true, true)
} }
stateManager!!.stopForeground(true) // gets rid of persistent notification stateManager.stopForeground(true) // gets rid of persistent notification
return true return true
} }
else -> { else -> {
Log.d(TAG, "Unhandled key code: $keycode") Log.d(TAG, "Unhandled key code: $keycode")
if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something if (info?.playable != null && info?.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something
val message = String.format(resources.getString(R.string.unknown_media_key), keycode) val message = String.format(resources.getString(R.string.unknown_media_key), keycode)
Toast.makeText(this, message, Toast.LENGTH_SHORT).show() Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
} }
@ -696,7 +720,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
{ error: Throwable -> { error: Throwable ->
Log.d(TAG, "Playable was not loaded from preferences. Stopping service.") Log.d(TAG, "Playable was not loaded from preferences. Stopping service.")
error.printStackTrace() error.printStackTrace()
stateManager!!.stopService() stateManager.stopService()
}) })
} }
@ -708,7 +732,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
PlaybackServiceStarter(this, playable) PlaybackServiceStarter(this, playable)
.intent) .intent)
writeNoMediaPlaying() writeNoMediaPlaying()
stateManager!!.stopService() stateManager.stopService()
return return
} }
@ -716,9 +740,9 @@ class PlaybackService : MediaBrowserServiceCompat() {
clearCurrentlyPlayingTemporaryPlaybackSpeed() clearCurrentlyPlayingTemporaryPlaybackSpeed()
} }
mediaPlayer!!.playMediaObject(playable, stream, true, true) mediaPlayer?.playMediaObject(playable, stream, true, true)
stateManager!!.validStartCommandWasReceived() stateManager.validStartCommandWasReceived()
stateManager!!.startForeground(R.id.notification_playing, notificationBuilder!!.build()) stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
recreateMediaSessionIfNeeded() recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(playable) updateNotificationAndMediaSession(playable)
addPlayableToQueue(playable) addPlayableToQueue(playable)
@ -730,14 +754,14 @@ class PlaybackService : MediaBrowserServiceCompat() {
*/ */
fun setVideoSurface(sh: SurfaceHolder?) { fun setVideoSurface(sh: SurfaceHolder?) {
Log.d(TAG, "Setting display") Log.d(TAG, "Setting display")
mediaPlayer!!.setVideoSurface(sh) mediaPlayer?.setVideoSurface(sh)
} }
fun notifyVideoSurfaceAbandoned() { fun notifyVideoSurfaceAbandoned() {
mediaPlayer!!.pause(true, false) mediaPlayer?.pause(true, false)
mediaPlayer!!.resetVideoSurface() mediaPlayer?.resetVideoSurface()
updateNotificationAndMediaSession(playable) updateNotificationAndMediaSession(playable)
stateManager!!.stopForeground(!isPersistNotify) stateManager.stopForeground(!isPersistNotify)
} }
private val taskManagerCallback: PSTMCallback = object : PSTMCallback { private val taskManagerCallback: PSTMCallback = object : PSTMCallback {
@ -752,7 +776,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun onChapterLoaded(media: Playable?) { override fun onChapterLoaded(media: Playable?) {
sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0) sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0)
updateMediaSession(mediaPlayer!!.playerStatus) updateMediaSession(mediaPlayer?.playerStatus)
} }
} }
@ -767,36 +791,34 @@ class PlaybackService : MediaBrowserServiceCompat() {
updateMediaSession(newInfo!!.playerStatus) updateMediaSession(newInfo!!.playerStatus)
when (newInfo.playerStatus) { when (newInfo.playerStatus) {
PlayerStatus.INITIALIZED -> { PlayerStatus.INITIALIZED -> {
if (mediaPlayer!!.pSMPInfo.playable != null) { if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
mediaPlayer!!.pSMPInfo.playerStatus)
} }
updateNotificationAndMediaSession(newInfo.playable) updateNotificationAndMediaSession(newInfo.playable)
} }
PlayerStatus.PREPARED -> { PlayerStatus.PREPARED -> {
if (mediaPlayer!!.pSMPInfo.playable != null) { if (mediaPlayer != null) {
writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, writeMediaPlaying(mediaPlayer!!.pSMPInfo.playable, mediaPlayer!!.pSMPInfo.playerStatus)
mediaPlayer!!.pSMPInfo.playerStatus)
} }
taskManager!!.startChapterLoader(newInfo.playable!!) taskManager.startChapterLoader(newInfo.playable!!)
} }
PlayerStatus.PAUSED -> { PlayerStatus.PAUSED -> {
updateNotificationAndMediaSession(newInfo.playable) updateNotificationAndMediaSession(newInfo.playable)
if (!isCasting) { if (!isCasting) {
stateManager!!.stopForeground(!isPersistNotify) stateManager.stopForeground(!isPersistNotify)
} }
cancelPositionObserver() cancelPositionObserver()
writePlayerStatus(mediaPlayer!!.playerStatus) if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
} }
PlayerStatus.STOPPED -> {} PlayerStatus.STOPPED -> {}
PlayerStatus.PLAYING -> { PlayerStatus.PLAYING -> {
writePlayerStatus(mediaPlayer!!.playerStatus) if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
saveCurrentPosition(true, null, Playable.INVALID_TIME) saveCurrentPosition(true, null, Playable.INVALID_TIME)
recreateMediaSessionIfNeeded() recreateMediaSessionIfNeeded()
updateNotificationAndMediaSession(newInfo.playable) updateNotificationAndMediaSession(newInfo.playable)
setupPositionObserver() setupPositionObserver()
stateManager!!.validStartCommandWasReceived() stateManager.validStartCommandWasReceived()
stateManager!!.startForeground(R.id.notification_playing, notificationBuilder!!.build()) stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
// set sleep timer if auto-enabled // set sleep timer if auto-enabled
var autoEnableByTime = true var autoEnableByTime = true
val fromSetting = autoEnableFrom() val fromSetting = autoEnableFrom()
@ -817,11 +839,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
PlayerStatus.ERROR -> { PlayerStatus.ERROR -> {
writeNoMediaPlaying() writeNoMediaPlaying()
stateManager!!.stopService() stateManager.stopService()
} }
else -> {} else -> {}
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
TileService.requestListeningState(applicationContext, TileService.requestListeningState(applicationContext,
ComponentName(applicationContext, QuickSettingsTileService::class.java)) ComponentName(applicationContext, QuickSettingsTileService::class.java))
} }
@ -829,11 +851,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED) sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED)
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED) bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED)
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED) bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED)
taskManager!!.requestWidgetUpdate() taskManager.requestWidgetUpdate()
} }
override fun shouldStop() { override fun shouldStop() {
stateManager!!.stopForeground(!isPersistNotify) stateManager.stopForeground(!isPersistNotify)
} }
override fun onMediaChanged(reloadUI: Boolean) { override fun onMediaChanged(reloadUI: Boolean) {
@ -851,21 +873,21 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
override fun onPlaybackStart(playable: Playable, position: Int) { override fun onPlaybackStart(playable: Playable, position: Int) {
taskManager!!.startWidgetUpdater() taskManager.startWidgetUpdater()
if (position != Playable.INVALID_TIME) { if (position != Playable.INVALID_TIME) {
playable.setPosition(position) playable.setPosition(position)
} else { } else {
skipIntro(playable) skipIntro(playable)
} }
playable.onPlaybackStart() playable.onPlaybackStart()
taskManager!!.startPositionSaver() taskManager.startPositionSaver()
} }
override fun onPlaybackPause(playable: Playable?, position: Int) { override fun onPlaybackPause(playable: Playable?, position: Int) {
taskManager!!.cancelPositionSaver() taskManager.cancelPositionSaver()
cancelPositionObserver() cancelPositionObserver()
saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position) saveCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position)
taskManager!!.cancelWidgetUpdater() taskManager.cancelWidgetUpdater()
if (playable != null) { if (playable != null) {
if (playable is FeedMedia) { if (playable is FeedMedia) {
SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false) SynchronizationQueueSink.enqueueEpisodePlayedIfSynchronizationIsActive(applicationContext, playable, false)
@ -897,10 +919,10 @@ class PlaybackService : MediaBrowserServiceCompat() {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@Suppress("unused") @Suppress("unused")
fun playerError(event: PlayerErrorEvent?) { fun playerError(event: PlayerErrorEvent?) {
if (mediaPlayer!!.playerStatus == PlayerStatus.PLAYING) { if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING) {
mediaPlayer!!.pause(true, false) mediaPlayer!!.pause(true, false)
} }
stateManager!!.stopService() stateManager.stopService()
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@ -908,7 +930,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
fun bufferUpdate(event: BufferUpdateEvent) { fun bufferUpdate(event: BufferUpdateEvent) {
if (event.hasEnded()) { if (event.hasEnded()) {
val playable = playable val playable = playable
if (this.playable is FeedMedia && playable!!.getDuration() <= 0 && mediaPlayer!!.getDuration() > 0) { if (this.playable is FeedMedia && playable!!.getDuration() <= 0 && (mediaPlayer?.getDuration()?:0) > 0) {
// Playable is being streamed and does not have a duration specified in the feed // Playable is being streamed and does not have a duration specified in the feed
playable.setDuration(mediaPlayer!!.getDuration()) playable.setDuration(mediaPlayer!!.getDuration())
DBWriter.setFeedMedia(playable as FeedMedia?) DBWriter.setFeedMedia(playable as FeedMedia?)
@ -921,16 +943,16 @@ class PlaybackService : MediaBrowserServiceCompat() {
@Suppress("unused") @Suppress("unused")
fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) { fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) {
if (event.isOver) { if (event.isOver) {
mediaPlayer!!.pause(true, true) mediaPlayer?.pause(true, true)
mediaPlayer!!.setVolume(1.0f, 1.0f) mediaPlayer?.setVolume(1.0f, 1.0f)
} else if (event.getTimeLeft() < PlaybackServiceTaskManager.NOTIFICATION_THRESHOLD) { } else if (event.getTimeLeft() < PlaybackServiceTaskManager.NOTIFICATION_THRESHOLD) {
val multiplicators = floatArrayOf(0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f) val multiplicators = floatArrayOf(0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f)
val multiplicator = multiplicators[max(0.0, (event.getTimeLeft().toInt() / 1000).toDouble()) val multiplicator = multiplicators[max(0.0, (event.getTimeLeft().toInt() / 1000).toDouble())
.toInt()] .toInt()]
Log.d(TAG, "onSleepTimerAlmostExpired: $multiplicator") Log.d(TAG, "onSleepTimerAlmostExpired: $multiplicator")
mediaPlayer!!.setVolume(multiplicator, multiplicator) mediaPlayer?.setVolume(multiplicator, multiplicator)
} else if (event.isCancelled) { } else if (event.isCancelled) {
mediaPlayer!!.setVolume(1.0f, 1.0f) mediaPlayer?.setVolume(1.0f, 1.0f)
} }
} }
@ -967,7 +989,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) { if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) {
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent) displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent)
writeNoMediaPlaying() writeNoMediaPlaying()
stateManager!!.stopService() stateManager.stopService()
return null return null
} }
return nextItem.media return nextItem.media
@ -980,11 +1002,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
Log.d(TAG, "Playback ended") Log.d(TAG, "Playback ended")
clearCurrentlyPlayingTemporaryPlaybackSpeed() clearCurrentlyPlayingTemporaryPlaybackSpeed()
if (stopPlaying) { if (stopPlaying) {
taskManager!!.cancelPositionSaver() taskManager.cancelPositionSaver()
cancelPositionObserver() cancelPositionObserver()
if (!isCasting) { if (!isCasting) {
stateManager!!.stopForeground(true) stateManager.stopForeground(true)
stateManager!!.stopService() stateManager.stopService()
} }
} }
if (mediaType == null) { if (mediaType == null) {
@ -1084,11 +1106,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
fun setSleepTimer(waitingTime: Long) { fun setSleepTimer(waitingTime: Long) {
Log.d(TAG, "Setting sleep timer to $waitingTime milliseconds") Log.d(TAG, "Setting sleep timer to $waitingTime milliseconds")
taskManager!!.setSleepTimer(waitingTime) taskManager.setSleepTimer(waitingTime)
} }
fun disableSleepTimer() { fun disableSleepTimer() {
taskManager!!.disableSleepTimer() taskManager.disableSleepTimer()
} }
private fun sendNotificationBroadcast(type: Int, code: Int) { private fun sendNotificationBroadcast(type: Int, code: Int) {
@ -1100,7 +1122,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
private fun skipEndingIfNecessary() { private fun skipEndingIfNecessary() {
val playable = mediaPlayer!!.getPlayable() as? FeedMedia ?: return val playable = mediaPlayer?.getPlayable() as? FeedMedia ?: return
val duration = duration val duration = duration
val remainingTime = duration - currentPosition val remainingTime = duration - currentPosition
@ -1227,8 +1249,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle()) builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle())
if (notificationBuilder!!.isIconCached) { if (notificationBuilder.isIconCached) {
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder!!.cachedIcon) builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.cachedIcon)
} else { } else {
var iconUri = p.getImageLocation() var iconUri = p.getImageLocation()
if (p is FeedMedia) { // Don't use embedded cover etc, which Android can't load if (p is FeedMedia) { // Don't use embedded cover etc, which Android can't load
@ -1247,7 +1269,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
} }
if (stateManager!!.hasReceivedValidStartCommand()) { if (stateManager.hasReceivedValidStartCommand()) {
mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity, mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0))) or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0)))
@ -1278,17 +1300,17 @@ class PlaybackService : MediaBrowserServiceCompat() {
if (playable == null || mediaPlayer == null) { if (playable == null || mediaPlayer == null) {
Log.d(TAG, "setupNotification: playable=$playable") Log.d(TAG, "setupNotification: playable=$playable")
Log.d(TAG, "setupNotification: mediaPlayer=$mediaPlayer") Log.d(TAG, "setupNotification: mediaPlayer=$mediaPlayer")
if (!stateManager!!.hasReceivedValidStartCommand()) { if (!stateManager.hasReceivedValidStartCommand()) {
stateManager!!.stopService() stateManager.stopService()
} }
return return
} }
val playerStatus = mediaPlayer!!.playerStatus val playerStatus = mediaPlayer!!.playerStatus
notificationBuilder!!.setPlayable(playable) notificationBuilder.setPlayable(playable)
notificationBuilder!!.setMediaSessionToken(mediaSession!!.sessionToken) notificationBuilder.setMediaSessionToken(mediaSession!!.sessionToken)
notificationBuilder!!.playerStatus = playerStatus notificationBuilder.playerStatus = playerStatus
notificationBuilder!!.updatePosition(currentPosition, currentPlaybackSpeed) notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
val notificationManager = NotificationManagerCompat.from(this) val notificationManager = NotificationManagerCompat.from(this)
if (ActivityCompat.checkSelfPermission(this, if (ActivityCompat.checkSelfPermission(this,
@ -1302,14 +1324,14 @@ class PlaybackService : MediaBrowserServiceCompat() {
// for ActivityCompat#requestPermissions for more details. // for ActivityCompat#requestPermissions for more details.
return return
} }
notificationManager.notify(R.id.notification_playing, notificationBuilder!!.build()) notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
if (!notificationBuilder!!.isIconCached) { if (!notificationBuilder.isIconCached) {
playableIconLoaderThread = Thread { playableIconLoaderThread = Thread {
Log.d(TAG, "Loading notification icon") Log.d(TAG, "Loading notification icon")
notificationBuilder!!.loadIcon() notificationBuilder.loadIcon()
if (!Thread.currentThread().isInterrupted) { if (!Thread.currentThread().isInterrupted) {
notificationManager.notify(R.id.notification_playing, notificationBuilder!!.build()) notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
updateMediaSessionMetadata(playable) updateMediaSessionMetadata(playable)
} }
} }
@ -1345,11 +1367,11 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
fun sleepTimerActive(): Boolean { fun sleepTimerActive(): Boolean {
return taskManager!!.isSleepTimerActive return taskManager.isSleepTimerActive
} }
val sleepTimerTimeLeft: Long val sleepTimerTimeLeft: Long
get() = taskManager!!.sleepTimerTimeLeft get() = taskManager.sleepTimerTimeLeft
private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) { private fun bluetoothNotifyChange(info: PSMPInfo?, whatChanged: String) {
var isPlaying = false var isPlaying = false
@ -1379,17 +1401,17 @@ class PlaybackService : MediaBrowserServiceCompat() {
if (!isConnectedToCar) { if (!isConnectedToCar) {
Log.d(TAG, "Car was unplugged during playback.") Log.d(TAG, "Car was unplugged during playback.")
} else { } else {
val playerStatus = mediaPlayer!!.playerStatus val playerStatus = mediaPlayer?.playerStatus
when (playerStatus) { when (playerStatus) {
PlayerStatus.PAUSED, PlayerStatus.PREPARED -> { PlayerStatus.PAUSED, PlayerStatus.PREPARED -> {
mediaPlayer!!.resume() mediaPlayer?.resume()
} }
PlayerStatus.PREPARING -> { PlayerStatus.PREPARING -> {
mediaPlayer!!.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared()) mediaPlayer?.setStartWhenPrepared(!mediaPlayer!!.isStartWhenPrepared())
} }
PlayerStatus.INITIALIZED -> { PlayerStatus.INITIALIZED -> {
mediaPlayer!!.setStartWhenPrepared(true) mediaPlayer?.setStartWhenPrepared(true)
mediaPlayer!!.prepare() mediaPlayer?.prepare()
} }
else -> {} else -> {}
} }
@ -1455,7 +1477,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
*/ */
private fun pauseIfPauseOnDisconnect() { private fun pauseIfPauseOnDisconnect() {
Log.d(TAG, "pauseIfPauseOnDisconnect()") Log.d(TAG, "pauseIfPauseOnDisconnect()")
transientPause = (mediaPlayer!!.playerStatus == PlayerStatus.PLAYING) transientPause = (mediaPlayer?.playerStatus == PlayerStatus.PLAYING)
if (isPauseOnHeadsetDisconnect && !isCasting) { if (isPauseOnHeadsetDisconnect && !isCasting) {
mediaPlayer!!.pause(!isPersistNotify, false) mediaPlayer!!.pause(!isPersistNotify, false)
} }
@ -1465,23 +1487,23 @@ class PlaybackService : MediaBrowserServiceCompat() {
* @param bluetooth true if the event for unpausing came from bluetooth * @param bluetooth true if the event for unpausing came from bluetooth
*/ */
private fun unpauseIfPauseOnDisconnect(bluetooth: Boolean) { private fun unpauseIfPauseOnDisconnect(bluetooth: Boolean) {
if (mediaPlayer!!.isAudioChannelInUse) { if (mediaPlayer != null && mediaPlayer!!.isAudioChannelInUse) {
Log.d(TAG, "unpauseIfPauseOnDisconnect() audio is in use") Log.d(TAG, "unpauseIfPauseOnDisconnect() audio is in use")
return return
} }
if (transientPause) { if (transientPause) {
transientPause = false transientPause = false
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
stateManager!!.stopService() stateManager.stopService()
return return
} }
if (!bluetooth && isUnpauseOnHeadsetReconnect) { if (!bluetooth && isUnpauseOnHeadsetReconnect) {
mediaPlayer!!.resume() mediaPlayer?.resume()
} else if (bluetooth && isUnpauseOnBluetoothReconnect) { } else if (bluetooth && isUnpauseOnBluetoothReconnect) {
// let the user know we've started playback again... // let the user know we've started playback again...
val v = applicationContext.getSystemService(VIBRATOR_SERVICE) as? Vibrator val v = applicationContext.getSystemService(VIBRATOR_SERVICE) as? Vibrator
v?.vibrate(500) v?.vibrate(500)
mediaPlayer!!.resume() mediaPlayer?.resume()
} }
} }
} }
@ -1490,7 +1512,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN)) EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN))
stateManager!!.stopService() stateManager.stopService()
} }
} }
} }
@ -1499,7 +1521,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
@Suppress("unused") @Suppress("unused")
fun volumeAdaptionChanged(event: VolumeAdaptionChangedEvent) { fun volumeAdaptionChanged(event: VolumeAdaptionChangedEvent) {
val playbackVolumeUpdater = PlaybackVolumeUpdater() val playbackVolumeUpdater = PlaybackVolumeUpdater()
playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer!!, event.feedId, event.volumeAdaptionSetting) if (mediaPlayer != null) playbackVolumeUpdater.updateVolumeIfNecessary(mediaPlayer!!, event.feedId, event.volumeAdaptionSetting)
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
@ -1508,7 +1530,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
if (playable is FeedMedia) { if (playable is FeedMedia) {
if ((playable as FeedMedia).getItem()?.feed?.id == event.feedId) { if ((playable as FeedMedia).getItem()?.feed?.id == event.feedId) {
if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) { if (event.speed == FeedPreferences.SPEED_USE_GLOBAL) {
playable?.let {setSpeed(getPlaybackSpeed(playable!!.getMediaType()))} setSpeed(getPlaybackSpeed(playable!!.getMediaType()))
} else { } else {
setSpeed(event.speed) setSpeed(event.speed)
} }
@ -1533,17 +1555,17 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
fun resume() { fun resume() {
mediaPlayer!!.resume() mediaPlayer?.resume()
taskManager!!.restartSleepTimer() taskManager.restartSleepTimer()
} }
fun prepare() { fun prepare() {
mediaPlayer!!.prepare() mediaPlayer?.prepare()
taskManager!!.restartSleepTimer() taskManager.restartSleepTimer()
} }
fun pause(abandonAudioFocus: Boolean, reinit: Boolean) { fun pause(abandonAudioFocus: Boolean, reinit: Boolean) {
mediaPlayer!!.pause(abandonAudioFocus, reinit) mediaPlayer?.pause(abandonAudioFocus, reinit)
} }
val pSMPInfo: PSMPInfo val pSMPInfo: PSMPInfo
@ -1553,7 +1575,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
get() = mediaPlayer!!.playerStatus get() = mediaPlayer!!.playerStatus
val playable: Playable? val playable: Playable?
get() = mediaPlayer!!.getPlayable() get() = mediaPlayer?.getPlayable()
fun setSpeed(speed: Float) { fun setSpeed(speed: Float) {
currentlyPlayingTemporaryPlaybackSpeed = speed currentlyPlayingTemporaryPlaybackSpeed = speed
@ -1563,34 +1585,31 @@ class PlaybackService : MediaBrowserServiceCompat() {
setPlaybackSpeed(speed) setPlaybackSpeed(speed)
} }
mediaPlayer!!.setPlaybackParams(speed, isSkipSilence) mediaPlayer?.setPlaybackParams(speed, isSkipSilence)
} }
fun skipSilence(skipSilence: Boolean) { fun skipSilence(skipSilence: Boolean) {
mediaPlayer!!.setPlaybackParams(currentPlaybackSpeed, skipSilence) mediaPlayer?.setPlaybackParams(currentPlaybackSpeed, skipSilence)
} }
val currentPlaybackSpeed: Float val currentPlaybackSpeed: Float
get() { get() {
if (mediaPlayer == null) { return mediaPlayer?.getPlaybackSpeed() ?: 1.0f
return 1.0f
}
return mediaPlayer!!.getPlaybackSpeed()
} }
var isStartWhenPrepared: Boolean var isStartWhenPrepared: Boolean
get() = mediaPlayer!!.isStartWhenPrepared() get() = mediaPlayer?.isStartWhenPrepared() ?: false
set(s) { set(s) {
mediaPlayer!!.setStartWhenPrepared(s) if (mediaPlayer != null) mediaPlayer!!.setStartWhenPrepared(s)
} }
fun seekTo(t: Int) { fun seekTo(t: Int) {
mediaPlayer!!.seekTo(t) mediaPlayer?.seekTo(t)
EventBus.getDefault().post(PlaybackPositionEvent(t, duration)) EventBus.getDefault().post(PlaybackPositionEvent(t, duration))
} }
private fun seekDelta(d: Int) { private fun seekDelta(d: Int) {
mediaPlayer!!.seekDelta(d) mediaPlayer?.seekDelta(d)
} }
val duration: Int val duration: Int
@ -1599,10 +1618,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
* an invalid state. * an invalid state.
*/ */
get() { get() {
if (mediaPlayer == null) { return mediaPlayer?.getDuration() ?: Playable.INVALID_TIME
return Playable.INVALID_TIME
}
return mediaPlayer!!.getDuration()
} }
val currentPosition: Int val currentPosition: Int
@ -1611,63 +1627,48 @@ class PlaybackService : MediaBrowserServiceCompat() {
* is in an invalid state. * is in an invalid state.
*/ */
get() { get() {
if (mediaPlayer == null) { return mediaPlayer?.getPosition() ?: Playable.INVALID_TIME
return Playable.INVALID_TIME
}
return mediaPlayer!!.getPosition()
} }
val audioTracks: List<String?>? val audioTracks: List<String?>
get() { get() {
if (mediaPlayer == null) { return mediaPlayer?.getAudioTracks() ?: listOf()
return emptyList<String>()
}
return mediaPlayer!!.getAudioTracks()
} }
val selectedAudioTrack: Int val selectedAudioTrack: Int
get() { get() {
if (mediaPlayer == null) { return mediaPlayer?.getSelectedAudioTrack() ?: -1
return -1
}
return mediaPlayer!!.getSelectedAudioTrack()
} }
fun setAudioTrack(track: Int) { fun setAudioTrack(track: Int) {
if (mediaPlayer != null) { mediaPlayer?.setAudioTrack(track)
mediaPlayer!!.setAudioTrack(track)
}
} }
val isStreaming: Boolean val isStreaming: Boolean
get() = mediaPlayer!!.isStreaming() get() = mediaPlayer?.isStreaming() ?: false
val videoSize: Pair<Int, Int>? val videoSize: Pair<Int, Int>?
get() = mediaPlayer!!.getVideoSize() get() = mediaPlayer!!.getVideoSize()
private fun setupPositionObserver() { private fun setupPositionObserver() {
if (positionEventTimer != null) { positionEventTimer?.dispose()
positionEventTimer!!.dispose()
}
Log.d(TAG, "Setting up position observer") Log.d(TAG, "Setting up position observer")
positionEventTimer = Observable.interval(1, TimeUnit.SECONDS) positionEventTimer = Observable.interval(POSITION_EVENT_INTERVAL, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { number: Long? -> .subscribe { number: Long? ->
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration)) EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
if (Build.VERSION.SDK_INT < 29) { if (Build.VERSION.SDK_INT < 29) {
notificationBuilder!!.updatePosition(currentPosition, currentPlaybackSpeed) notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
notificationManager.notify(R.id.notification_playing, notificationBuilder!!.build()) notificationManager?.notify(R.id.notification_playing, notificationBuilder.build())
} }
skipEndingIfNecessary() skipEndingIfNecessary()
} }
} }
private fun cancelPositionObserver() { private fun cancelPositionObserver() {
if (positionEventTimer != null) { positionEventTimer?.dispose()
positionEventTimer!!.dispose()
}
} }
private fun addPlayableToQueue(playable: Playable?) { private fun addPlayableToQueue(playable: Playable?) {
@ -1727,7 +1728,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun onStop() { override fun onStop() {
Log.d(TAG, "onStop()") Log.d(TAG, "onStop()")
mediaPlayer!!.stopPlayback(true) mediaPlayer?.stopPlayback(true)
} }
override fun onSkipToPrevious() { override fun onSkipToPrevious() {
@ -1741,23 +1742,22 @@ class PlaybackService : MediaBrowserServiceCompat() {
} }
fun onNextChapter() { fun onNextChapter() {
val chapters = mediaPlayer!!.getPlayable()!!.getChapters() val chapters = mediaPlayer?.getPlayable()?.getChapters() ?: listOf()
if (chapters.isEmpty()) { if (chapters.isEmpty()) {
// No chapters, just fallback to next episode // No chapters, just fallback to next episode
mediaPlayer!!.skip() mediaPlayer?.skip()
return return
} }
val nextChapter = getCurrentChapterIndex( val nextChapter = getCurrentChapterIndex(mediaPlayer?.getPlayable(), (mediaPlayer?.getPosition()?:0)) + 1
mediaPlayer!!.getPlayable(), mediaPlayer!!.getPosition()) + 1
if (chapters.size < nextChapter + 1) { if (chapters.size < nextChapter + 1) {
// We are on the last chapter, just fallback to the next episode // We are on the last chapter, just fallback to the next episode
mediaPlayer!!.skip() mediaPlayer?.skip()
return return
} }
mediaPlayer!!.seekTo(chapters[nextChapter].start.toInt()) mediaPlayer?.seekTo(chapters[nextChapter].start.toInt())
} }
override fun onFastForward() { override fun onFastForward() {
@ -1771,7 +1771,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
.getSystemService(UI_MODE_SERVICE) as UiModeManager .getSystemService(UI_MODE_SERVICE) as UiModeManager
if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT if (hardwareForwardButton == KeyEvent.KEYCODE_MEDIA_NEXT
|| uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) { || uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) {
mediaPlayer!!.skip() mediaPlayer?.skip()
} else { } else {
seekDelta(fastForwardSecs * 1000) seekDelta(fastForwardSecs * 1000)
} }
@ -1820,30 +1820,36 @@ class PlaybackService : MediaBrowserServiceCompat() {
override fun onCustomAction(action: String, extra: Bundle) { override fun onCustomAction(action: String, extra: Bundle) {
Log.d(TAG, "onCustomAction($action)") Log.d(TAG, "onCustomAction($action)")
if (CUSTOM_ACTION_FAST_FORWARD == action) { when (action) {
onFastForward() CUSTOM_ACTION_FAST_FORWARD -> {
} else if (CUSTOM_ACTION_REWIND == action) { onFastForward()
onRewind() }
} else if (CUSTOM_ACTION_SKIP_TO_NEXT == action) { CUSTOM_ACTION_REWIND -> {
mediaPlayer!!.skip() onRewind()
} else if (CUSTOM_ACTION_NEXT_CHAPTER == action) { }
onNextChapter() CUSTOM_ACTION_SKIP_TO_NEXT -> {
} else if (CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED == action) { mediaPlayer?.skip()
val selectedSpeeds = playbackSpeedArray }
CUSTOM_ACTION_NEXT_CHAPTER -> {
onNextChapter()
}
CUSTOM_ACTION_CHANGE_PLAYBACK_SPEED -> {
val selectedSpeeds = playbackSpeedArray
// If the list has zero or one element, there's nothing we can do to change the playback speed. // If the list has zero or one element, there's nothing we can do to change the playback speed.
if (selectedSpeeds.size > 1) { if (selectedSpeeds.size > 1) {
val speedPosition = selectedSpeeds.indexOf(mediaPlayer!!.getPlaybackSpeed()) val speedPosition = selectedSpeeds.indexOf(mediaPlayer?.getPlaybackSpeed()?:0f)
val newSpeed = if (speedPosition == selectedSpeeds.size - 1) { val newSpeed = if (speedPosition == selectedSpeeds.size - 1) {
// This is the last element. Wrap instead of going over the size of the list. // This is the last element. Wrap instead of going over the size of the list.
selectedSpeeds[0] selectedSpeeds[0]
} else { } else {
// If speedPosition is still -1 (the user isn't using a preset), use the first preset in the // If speedPosition is still -1 (the user isn't using a preset), use the first preset in the
// list. // list.
selectedSpeeds[speedPosition + 1] selectedSpeeds[speedPosition + 1]
}
onSetPlaybackSpeed(newSpeed)
} }
onSetPlaybackSpeed(newSpeed)
} }
} }
} }
@ -1855,6 +1861,8 @@ class PlaybackService : MediaBrowserServiceCompat() {
*/ */
private const val TAG = "PlaybackService" private const val TAG = "PlaybackService"
private const val POSITION_EVENT_INTERVAL = 10L
const val ACTION_PLAYER_STATUS_CHANGED: String = "action.ac.mdiq.podvinci.core.service.playerStatusChanged" const val ACTION_PLAYER_STATUS_CHANGED: String = "action.ac.mdiq.podvinci.core.service.playerStatusChanged"
private const val AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged" private const val AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"
private const val AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged" private const val AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"

View File

@ -37,12 +37,10 @@ open class AutomaticDownloadAlgorithm {
@UnstableApi open fun autoDownloadUndownloadedItems(context: Context?): Runnable? { @UnstableApi open fun autoDownloadUndownloadedItems(context: Context?): Runnable? {
return Runnable { return Runnable {
// true if we should auto download based on network status // true if we should auto download based on network status
val networkShouldAutoDl = (isAutoDownloadAllowed val networkShouldAutoDl = (isAutoDownloadAllowed && isEnableAutodownload)
&& isEnableAutodownload)
// true if we should auto download based on power status // true if we should auto download based on power status
val powerShouldAutoDl = (deviceCharging(context!!) val powerShouldAutoDl = (deviceCharging(context!!) || isEnableAutodownloadOnBattery)
|| isEnableAutodownloadOnBattery)
// we should only auto download if both network AND power are happy // we should only auto download if both network AND power are happy
if (networkShouldAutoDl && powerShouldAutoDl) { if (networkShouldAutoDl && powerShouldAutoDl) {
@ -56,9 +54,7 @@ open class AutomaticDownloadAlgorithm {
candidates.addAll(queue) candidates.addAll(queue)
for (newItem in newItems) { for (newItem in newItems) {
val feedPrefs = newItem.feed!!.preferences val feedPrefs = newItem.feed!!.preferences
if (feedPrefs!!.autoDownload if (feedPrefs!!.autoDownload && !candidates.contains(newItem) && feedPrefs.filter.shouldAutoDownload(newItem)) {
&& !candidates.contains(newItem)
&& feedPrefs.filter.shouldAutoDownload(newItem)) {
candidates.add(newItem) candidates.add(newItem)
} }
} }
@ -78,10 +74,8 @@ open class AutomaticDownloadAlgorithm {
val autoDownloadableEpisodes = candidates.size val autoDownloadableEpisodes = candidates.size
val downloadedEpisodes = getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED)) val downloadedEpisodes = getTotalEpisodeCount(FeedItemFilter(FeedItemFilter.DOWNLOADED))
val deletedEpisodes = build() val deletedEpisodes = build().makeRoomForEpisodes(context, autoDownloadableEpisodes)
.makeRoomForEpisodes(context, autoDownloadableEpisodes) val cacheIsUnlimited = episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
val cacheIsUnlimited =
episodeCacheSize == UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED
val episodeCacheSize = episodeCacheSize val episodeCacheSize = episodeCacheSize
val episodeSpaceLeft = val episodeSpaceLeft =
if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) { if (cacheIsUnlimited || episodeCacheSize >= downloadedEpisodes + autoDownloadableEpisodes) {
@ -99,6 +93,9 @@ open class AutomaticDownloadAlgorithm {
} }
} }
} }
else {
Log.d(TAG, "not auto downloaded networkShouldAutoDl: $networkShouldAutoDl powerShouldAutoDl $powerShouldAutoDl")
}
} }
} }

View File

@ -434,17 +434,17 @@ object DBReader {
*/ */
fun getFeed(feedId: Long, filtered: Boolean): Feed? { fun getFeed(feedId: Long, filtered: Boolean): Feed? {
Log.d(TAG, "getFeed() called with: feedId = [$feedId]") Log.d(TAG, "getFeed() called with: feedId = [$feedId]")
val adapter = getInstance() val adapter = getInstance() ?: return null
adapter!!.open() adapter.open()
var feed: Feed? = null
try { try {
adapter.getFeedCursor(feedId).use { cursor -> adapter.getFeedCursor(feedId).use { cursor ->
var feed: Feed? = null
if (cursor.moveToNext()) { if (cursor.moveToNext()) {
feed = extractFeedFromCursorRow(cursor) feed = extractFeedFromCursorRow(cursor)
if (filtered) { if (filtered) {
feed!!.items = getFeedItemList(feed, feed!!.itemFilter).toMutableList() feed.items = getFeedItemList(feed, feed.itemFilter).toMutableList()
} else { } else {
feed!!.items = getFeedItemList(feed).toMutableList() feed.items = getFeedItemList(feed).toMutableList()
} }
} else { } else {
Log.e(TAG, "getFeed could not find feed with id $feedId") Log.e(TAG, "getFeed could not find feed with id $feedId")

View File

@ -17,7 +17,7 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.channels.FileChannel import java.nio.channels.FileChannel
object DatabaseExporter { object DatabaseTransporter {
private const val TAG = "DatabaseExporter" private const val TAG = "DatabaseExporter"
private const val TEMP_DB_NAME = PodDBAdapter.DATABASE_NAME + "_tmp" private const val TEMP_DB_NAME = PodDBAdapter.DATABASE_NAME + "_tmp"

View File

@ -15,25 +15,21 @@ object FeedItemPermutors {
*/ */
@JvmStatic @JvmStatic
fun getPermutor(sortOrder: SortOrder): Permutor<FeedItem> { fun getPermutor(sortOrder: SortOrder): Permutor<FeedItem> {
var comparator: Comparator<FeedItem?>? = null var comparator: Comparator<FeedItem>? = null
var permutor: Permutor<FeedItem>? = null var permutor: Permutor<FeedItem>? = null
when (sortOrder) { when (sortOrder) {
SortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.EPISODE_TITLE_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
itemTitle(f1).compareTo( itemTitle(f1).compareTo(itemTitle(f2))
itemTitle(f2))
} }
SortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.EPISODE_TITLE_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
itemTitle(f2).compareTo( itemTitle(f2).compareTo(itemTitle(f1))
itemTitle(f1))
} }
SortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.DATE_OLD_NEW -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
pubDate(f1).compareTo( pubDate(f1).compareTo(pubDate(f2))
pubDate(f2))
} }
SortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.DATE_NEW_OLD -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
pubDate(f2).compareTo( pubDate(f2).compareTo(pubDate(f1))
pubDate(f1))
} }
SortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.DURATION_SHORT_LONG -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
duration(f1).compareTo(duration(f2)) duration(f1).compareTo(duration(f2))
@ -42,20 +38,16 @@ object FeedItemPermutors {
duration(f2).compareTo(duration(f1)) duration(f2).compareTo(duration(f1))
} }
SortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.EPISODE_FILENAME_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
itemLink(f1).compareTo( itemLink(f1).compareTo(itemLink(f2))
itemLink(f2))
} }
SortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.EPISODE_FILENAME_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
itemLink(f2).compareTo( itemLink(f2).compareTo(itemLink(f1))
itemLink(f1))
} }
SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
feedTitle(f1).compareTo( feedTitle(f1).compareTo(feedTitle(f2))
feedTitle(f2))
} }
SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? -> SortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: FeedItem?, f2: FeedItem? ->
feedTitle(f2).compareTo( feedTitle(f2).compareTo(feedTitle(f1))
feedTitle(f1))
} }
SortOrder.RANDOM -> permutor = object : Permutor<FeedItem> { SortOrder.RANDOM -> permutor = object : Permutor<FeedItem> {
override fun reorder(queue: MutableList<FeedItem>?) {if (!queue.isNullOrEmpty()) queue.shuffle()} override fun reorder(queue: MutableList<FeedItem>?) {if (!queue.isNullOrEmpty()) queue.shuffle()}
@ -74,9 +66,9 @@ object FeedItemPermutors {
} }
} }
if (comparator != null) { if (comparator != null) {
val comparator2: Comparator<FeedItem?> = comparator val comparator2: Comparator<FeedItem> = comparator
permutor = object : Permutor<FeedItem> { permutor = object : Permutor<FeedItem> {
override fun reorder(queue: MutableList<FeedItem>?) {if (!queue.isNullOrEmpty()) queue.sortedWith(comparator2)} override fun reorder(queue: MutableList<FeedItem>?) {if (!queue.isNullOrEmpty()) queue.sortWith(comparator2)}
} }
} }
return permutor!! return permutor!!
@ -84,32 +76,27 @@ object FeedItemPermutors {
// Null-safe accessors // Null-safe accessors
private fun pubDate(item: FeedItem?): Date { private fun pubDate(item: FeedItem?): Date {
return if ((item != null && item.pubDate != null)) item.pubDate!! return if (item?.pubDate != null) item.pubDate!! else Date(0)
else Date(0)
} }
private fun itemTitle(item: FeedItem?): String { private fun itemTitle(item: FeedItem?): String {
return if ((item != null && item.title != null)) item.title!!.lowercase(Locale.getDefault()) return if (item?.title != null) item.title!!.lowercase(Locale.getDefault()) else ""
else ""
} }
private fun duration(item: FeedItem?): Int { private fun duration(item: FeedItem?): Int {
return if ((item != null && item.media != null)) item.media!!.getDuration() return if (item?.media != null) item.media!!.getDuration() else 0
else 0
} }
private fun size(item: FeedItem?): Long { private fun size(item: FeedItem?): Long {
return if ((item != null && item.media != null)) item.media!!.size else 0 return if (item?.media != null) item.media!!.size else 0
} }
private fun itemLink(item: FeedItem?): String { private fun itemLink(item: FeedItem?): String {
return if ((item != null && item.link != null) return if (item?.link != null) item.link!!.lowercase(Locale.getDefault()) else ""
) item.link!!.lowercase(Locale.getDefault()) else ""
} }
private fun feedTitle(item: FeedItem?): String { private fun feedTitle(item: FeedItem?): String {
return if ((item != null && item.feed != null && item.feed!!.title != null) return if (item?.feed != null && item.feed!!.title != null) item.feed!!.title!!.lowercase(Locale.getDefault()) else ""
) item.feed!!.title!!.lowercase(Locale.getDefault()) else ""
} }
/** /**
@ -147,8 +134,8 @@ object FeedItemPermutors {
// Sort each individual list by PubDate (ascending/descending) // Sort each individual list by PubDate (ascending/descending)
val itemComparator: Comparator<FeedItem> = if (ascending) val itemComparator: Comparator<FeedItem> = if (ascending)
Comparator { f1: FeedItem, f2: FeedItem -> f1.pubDate?.compareTo(f2!!.pubDate)?:-1 } Comparator { f1: FeedItem, f2: FeedItem -> f1.pubDate?.compareTo(f2.pubDate)?:-1 }
else Comparator { f1: FeedItem, f2: FeedItem -> f2.pubDate?.compareTo(f1!!.pubDate)?:-1 } else Comparator { f1: FeedItem, f2: FeedItem -> f2.pubDate?.compareTo(f1.pubDate)?:-1 }
val feeds: MutableList<List<FeedItem>> = ArrayList() val feeds: MutableList<List<FeedItem>> = ArrayList()
for ((_, value) in map) { for ((_, value) in map) {

View File

@ -1,12 +1,5 @@
package ac.mdiq.podvinci.core.util.playback package ac.mdiq.podvinci.core.util.playback
import android.content.*
import android.os.IBinder
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import androidx.fragment.app.FragmentActivity
import androidx.media3.common.util.UnstableApi
import ac.mdiq.podvinci.core.feed.util.PlaybackSpeedUtils.getCurrentPlaybackSpeed import ac.mdiq.podvinci.core.feed.util.PlaybackSpeedUtils.getCurrentPlaybackSpeed
import ac.mdiq.podvinci.core.preferences.PlaybackPreferences import ac.mdiq.podvinci.core.preferences.PlaybackPreferences
import ac.mdiq.podvinci.core.service.playback.PlaybackService import ac.mdiq.podvinci.core.service.playback.PlaybackService
@ -20,6 +13,15 @@ import ac.mdiq.podvinci.model.feed.FeedMedia
import ac.mdiq.podvinci.model.playback.MediaType import ac.mdiq.podvinci.model.playback.MediaType
import ac.mdiq.podvinci.model.playback.Playable import ac.mdiq.podvinci.model.playback.Playable
import ac.mdiq.podvinci.playback.base.PlayerStatus import ac.mdiq.podvinci.playback.base.PlayerStatus
import android.content.*
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.media3.common.util.UnstableApi
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -71,10 +73,20 @@ abstract class PlaybackController(private val activity: FragmentActivity?) {
} }
initialized = true initialized = true
activity?.registerReceiver(statusUpdate, IntentFilter( // TODO: this shit doesn't work
PlaybackService.ACTION_PLAYER_STATUS_CHANGED)) // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity?.registerReceiver(notificationReceiver, IntentFilter( // activity?.registerReceiver(statusUpdate, IntentFilter(
PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION)) // PlaybackService.ACTION_PLAYER_STATUS_CHANGED), Context.RECEIVER_NOT_EXPORTED)
// activity?.registerReceiver(notificationReceiver, IntentFilter(
// PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), Context.RECEIVER_NOT_EXPORTED)
// } else {
// ContextCompat.registerReceiver(activity!!, statusUpdate, IntentFilter(
// PlaybackService.ACTION_PLAYER_STATUS_CHANGED), ContextCompat.RECEIVER_EXPORTED)
// ContextCompat.registerReceiver(activity, notificationReceiver, IntentFilter(
// PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION), ContextCompat.RECEIVER_EXPORTED)
// }
activity?.registerReceiver(statusUpdate, IntentFilter(PlaybackService.ACTION_PLAYER_STATUS_CHANGED))
activity?.registerReceiver(notificationReceiver, IntentFilter(PlaybackServiceInterface.ACTION_PLAYER_NOTIFICATION))
if (!released) { if (!released) {
bindToService() bindToService()

View File

@ -1,7 +1,5 @@
package ac.mdiq.podvinci.parser.feed.namespace package ac.mdiq.podvinci.parser.feed.namespace
import android.text.TextUtils
import android.util.Log
import ac.mdiq.podvinci.model.feed.FeedMedia import ac.mdiq.podvinci.model.feed.FeedMedia
import ac.mdiq.podvinci.parser.feed.HandlerState import ac.mdiq.podvinci.parser.feed.HandlerState
import ac.mdiq.podvinci.parser.feed.element.AtomText import ac.mdiq.podvinci.parser.feed.element.AtomText
@ -9,13 +7,13 @@ import ac.mdiq.podvinci.parser.feed.element.SyndElement
import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.getMimeType import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.getMimeType
import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.isImageFile import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.isImageFile
import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.isMediaFile import ac.mdiq.podvinci.parser.feed.util.MimeTypeUtils.isMediaFile
import android.util.Log
import org.xml.sax.Attributes import org.xml.sax.Attributes
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ /** Processes tags from the http://search.yahoo.com/mrss/ namespace. */
class Media : Namespace() { class Media : Namespace() {
override fun handleElementStart(localName: String, state: HandlerState, override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
attributes: Attributes): SyndElement {
if (CONTENT == localName) { if (CONTENT == localName) {
val url = attributes.getValue(DOWNLOAD_URL) val url = attributes.getValue(DOWNLOAD_URL)
val defaultStr = attributes.getValue(DEFAULT) val defaultStr = attributes.getValue(DEFAULT)
@ -47,15 +45,16 @@ class Media : Namespace() {
if (state.currentItem != null && (state.currentItem!!.media == null || isDefault) && url != null && validTypeMedia) { if (state.currentItem != null && (state.currentItem!!.media == null || isDefault) && url != null && validTypeMedia) {
var size: Long = 0 var size: Long = 0
val sizeStr = attributes.getValue(SIZE) val sizeStr = attributes.getValue(SIZE)
try { if (!sizeStr.isNullOrEmpty()) {
size = sizeStr.toLong() try {
} catch (e: NumberFormatException) { size = sizeStr.toLong()
Log.e(TAG, "Size \"$sizeStr\" could not be parsed.") } catch (e: NumberFormatException) {
Log.e(TAG, "Size \"$sizeStr\" could not be parsed.")
}
} }
var durationMs = 0 var durationMs = 0
val durationStr = attributes.getValue(DURATION) val durationStr = attributes.getValue(DURATION)
if (!TextUtils.isEmpty(durationStr)) { if (!durationStr.isNullOrEmpty()) {
try { try {
val duration = durationStr.toLong() val duration = durationStr.toLong()
durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt() durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()