5.4.1 commit

This commit is contained in:
Xilin Jia 2024-05-25 09:42:10 +01:00
parent 36e1823d58
commit 797e9b64ab
9 changed files with 122 additions and 79 deletions

View File

@ -1,7 +1,7 @@
plugins {
id('com.android.application')
id 'com.android.application'
id 'kotlin-android'
// id 'kotlin-kapt'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
id('com.github.triplet.play') version '3.8.3' apply false
}
@ -159,8 +159,8 @@ android {
// Version code schema (not used):
// "1.2.3-beta4" -> 1020304
// "1.2.3" -> 1020395
versionCode 3020147
versionName "5.4.0"
versionCode 3020148
versionName "5.4.1"
def commit = ""
try {
@ -221,6 +221,7 @@ android {
dependencies {
implementation "androidx.core:core-ktx:1.12.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
implementation 'com.android.volley:volley:1.2.1'
constraints {

View File

@ -56,6 +56,7 @@
tools:ignore="ExportedService">
<intent-filter>
<!-- <action android:name="androidx.media3.session.MediaLibraryService"/>-->
<action android:name="androidx.media3.session.MediaSessionService"/>
<action android:name="android.media.browse.MediaBrowserService"/>
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />

View File

@ -22,6 +22,7 @@ import android.os.IBinder
import android.util.Log
import android.util.Pair
import android.view.SurfaceHolder
import android.widget.MediaController
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
@ -114,31 +115,23 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
try {
activity.unregisterReceiver(statusUpdate)
} catch (e: IllegalArgumentException) {
// ignore
}
} catch (e: IllegalArgumentException) { }
try {
activity.unregisterReceiver(notificationReceiver)
} catch (e: IllegalArgumentException) {
// ignore
}
} catch (e: IllegalArgumentException) { }
unbind()
// media = null
released = true
if (eventsRegistered) {
eventsRegistered = false
}
if (eventsRegistered) eventsRegistered = false
}
private fun unbind() {
try {
activity.unbindService(mConnection)
} catch (e: IllegalArgumentException) {
// ignore
}
} catch (e: IllegalArgumentException) { }
initialized = false
}

View File

@ -574,7 +574,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
try {
clearMediaPlayerListeners()
// TODO: should use: exoPlayer!!.playWhenReady ?
if (exoPlayer!!.isPlaying) exoPlayer?.stop()
if (exoPlayer?.isPlaying == true) exoPlayer?.stop()
} catch (e: Exception) {
e.printStackTrace()
}
@ -686,7 +686,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
while (true) {
delay(bufferUpdateInterval)
withContext(Dispatchers.Main) {
if (bufferedPercentagePrev != exoPlayer!!.bufferedPercentage) {
if (exoPlayer != null && bufferedPercentagePrev != exoPlayer?.bufferedPercentage) {
bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage)
bufferedPercentagePrev = exoPlayer!!.bufferedPercentage
}

View File

@ -11,10 +11,10 @@ import ac.mdiq.podcini.playback.cast.CastPsmp
import ac.mdiq.podcini.playback.cast.CastStateListener
import ac.mdiq.podcini.playback.service.PlaybackServiceTaskManager.PSTMCallback
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.clearCurrentlyPlayingTemporaryPlaybackSpeed
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.loadPlayableFromPreferences
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentEpisodeIsVideo
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingTemporaryPlaybackSpeed
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.loadPlayableFromPreferences
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeMediaPlaying
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.writePlayerStatus
@ -60,11 +60,14 @@ import ac.mdiq.podcini.util.NetworkUtils.isStreamingAllowed
import ac.mdiq.podcini.util.event.EventFlow
import ac.mdiq.podcini.util.event.FlowEvent
import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.bluetooth.BluetoothA2dp
import android.content.*
import android.content.Intent.EXTRA_KEY_EVENT
import android.media.AudioManager
import android.os.*
import android.os.Build.VERSION_CODES
@ -74,6 +77,7 @@ import android.util.Log
import android.util.Pair
import android.view.KeyEvent
import android.view.SurfaceHolder
import android.view.ViewConfiguration
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.app.NotificationCompat
@ -81,6 +85,7 @@ import androidx.media3.common.Player.STATE_ENDED
import androidx.media3.common.Player.STATE_IDLE
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.MediaItemsWithStartPosition
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
@ -123,6 +128,9 @@ class PlaybackService : MediaSessionService() {
private val mBinder: IBinder = LocalBinder()
private var clickCount = 0
private val clickHandler = Handler(Looper.getMainLooper())
val mPlayerInfo: MediaPlayerInfo
get() = mediaPlayer!!.playerInfo
@ -164,7 +172,6 @@ class PlaybackService : MediaSessionService() {
val videoSize: Pair<Int, Int>?
get() = mediaPlayer?.getVideoSize()
inner class LocalBinder : Binder() {
val service: PlaybackService
get() = this@PlaybackService
@ -214,6 +221,7 @@ class PlaybackService : MediaSessionService() {
recreateMediaPlayer()
if (LocalMediaPlayer.exoPlayer == null) LocalMediaPlayer.createStaticPlayer(applicationContext)
mediaSession = MediaSession.Builder(applicationContext, LocalMediaPlayer.exoPlayer!!)
.setCallback(MyCallback())
.setCustomLayout(notificationCustomButtons)
@ -271,62 +279,65 @@ class PlaybackService : MediaSessionService() {
unregisterReceiver(bluetoothStateUpdated)
unregisterReceiver(audioBecomingNoisy)
taskManager.shutdown()
}
fun isServiceReady(): Boolean {
return mediaSession?.player?.playbackState != STATE_IDLE && mediaSession?.player?.playbackState != STATE_ENDED
}
private inner class MyCallback : MediaSession.Callback {
inner class MyCallback : MediaSession.Callback {
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
Logd(TAG, "in onConnect")
Logd(TAG, "in MyCallback onConnect")
val sessionCommands = MediaSession.ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
// .add(NotificationCustomButton.REWIND)
// .add(NotificationCustomButton.FORWARD)
if (session.isMediaNotificationController(controller)) {
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
// .remove(COMMAND_SEEK_TO_PREVIOUS)
// .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
// .remove(COMMAND_SEEK_TO_NEXT)
// .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
// .removeAll()
when {
session.isMediaNotificationController(controller) -> {
val playerCommands = MediaSession.ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
// .remove(COMMAND_SEEK_TO_PREVIOUS)
// .remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
// .remove(COMMAND_SEEK_TO_NEXT)
// .remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
// .removeAll()
//
// // Custom layout and available commands to configure the legacy/framework session.
// return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
//// .setCustomLayout(
//// ImmutableList.of(
//// createSeekBackwardButton(NotificationCustomButton.REWIND),
//// createSeekForwardButton(customCommandSeekForward))
//// )
// .setAvailablePlayerCommands(playerCommands.build())
// .setAvailableSessionCommands(sessionCommands.build())
// .build()
//
// // Custom layout and available commands to configure the legacy/framework session.
// return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
//// .setCustomLayout(
//// ImmutableList.of(
//// createSeekBackwardButton(NotificationCustomButton.REWIND),
//// createSeekForwardButton(customCommandSeekForward))
//// )
// .setAvailablePlayerCommands(playerCommands.build())
// .setAvailableSessionCommands(sessionCommands.build())
// .build()
// val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
// val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
/* Registering custom player command buttons for player notification. */
notificationCustomButtons.forEach { commandButton ->
Logd(TAG, "onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
/* Registering custom player command buttons for player notification. */
notificationCustomButtons.forEach { commandButton ->
Logd(TAG, "MyCallback onConnect commandButton ${commandButton.displayName}")
commandButton.sessionCommand?.let(sessionCommands::add)
}
return MediaSession.ConnectionResult.accept(
sessionCommands.build(),
playerCommands.build()
)
}
return MediaSession.ConnectionResult.accept(
sessionCommands.build(),
playerCommands.build()
)
} else if (session.isAutoCompanionController(controller)) {
// Available session commands to accept incoming custom commands from Auto.
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands.build())
.build()
session.isAutoCompanionController(controller) -> {
// Available session commands to accept incoming custom commands from Auto.
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
.setAvailableSessionCommands(sessionCommands.build())
.build()
}
// Default commands with default custom layout for all other controllers.
else -> return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
// Default commands with default custom layout for all other controllers.
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
}
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
Logd(TAG, "MyCallback onPostConnect")
super.onPostConnect(session, controller)
if (notificationCustomButtons.isNotEmpty()) {
/* Setting custom player command buttons to mediaLibrarySession for player notification. */
@ -337,6 +348,7 @@ class PlaybackService : MediaSessionService() {
override fun onCustomCommand(session: MediaSession, controller: MediaSession.ControllerInfo, customCommand: SessionCommand, args: Bundle): ListenableFuture<SessionResult> {
/* Handling custom command buttons from player notification. */
Logd(TAG, "onCustomCommand called ${customCommand.customAction}")
when (customCommand.customAction) {
NotificationCustomButton.REWIND.customAction -> mediaPlayer?.seekDelta(-rewindSecs * 1000)
NotificationCustomButton.FORWARD.customAction -> mediaPlayer?.seekDelta(fastForwardSecs * 1000)
@ -345,8 +357,9 @@ class PlaybackService : MediaSessionService() {
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
}
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaSession.MediaItemsWithStartPosition> {
val settable = SettableFuture.create<MediaSession.MediaItemsWithStartPosition>()
override fun onPlaybackResumption(mediaSession: MediaSession, controller: MediaSession.ControllerInfo): ListenableFuture<MediaItemsWithStartPosition> {
Logd(TAG, "onPlaybackResumption called ")
val settable = SettableFuture.create<MediaItemsWithStartPosition>()
// scope.launch {
// // Your app is responsible for storing the playlist and the start position
// // to use here
@ -355,6 +368,31 @@ class PlaybackService : MediaSessionService() {
// }
return settable
}
override fun onMediaButtonEvent(mediaSession: MediaSession, controller: MediaSession.ControllerInfo, intent: Intent): Boolean {
val keyEvent =if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU)
intent.extras!!.getParcelable(EXTRA_KEY_EVENT, KeyEvent::class.java)
else intent.extras!!.getParcelable(EXTRA_KEY_EVENT) as? KeyEvent
Logd(TAG, "onMediaButtonEvent ${keyEvent?.keyCode}")
if (keyEvent != null && keyEvent.action == KeyEvent.ACTION_DOWN && keyEvent.repeatCount == 0) {
val keyCode = keyEvent.keyCode
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
clickCount++
clickHandler.removeCallbacksAndMessages(null)
clickHandler.postDelayed({
when (clickCount) {
1 -> handleKeycode(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE, false)
2 -> mediaPlayer?.seekDelta(fastForwardSecs * 1000)
3 -> mediaPlayer?.seekDelta(-rewindSecs * 1000)
}
clickCount = 0
}, ViewConfiguration.getDoubleTapTimeout().toLong())
return true
} else return handleKeycode(keyCode, false)
}
return false
}
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? {
@ -373,6 +411,9 @@ class PlaybackService : MediaSessionService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
// val notification = createNotification()
// startForeground(NOTIFICATION_ID, notification)
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
@ -405,20 +446,6 @@ class PlaybackService : MediaSessionService() {
val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceConstants.EXTRA_ALLOW_STREAM_ALWAYS, false)
sendNotificationBroadcast(PlaybackServiceConstants.NOTIFICATION_TYPE_RELOAD, 0)
if (allowStreamAlways) isAllowMobileStreaming = true
// Observable.fromCallable {
// if (playable is FeedMedia) return@fromCallable DBReader.getFeedMedia(playable.id)
// else return@fromCallable playable
// }
// .subscribeOn(Schedulers.io())
// .observeOn(AndroidSchedulers.mainThread())
// .subscribe(
// { loadedPlayable: Playable? -> startPlaying(loadedPlayable, allowStreamThisTime) },
// { error: Throwable ->
// Logd(TAG, "Playable was not found. Stopping service.")
// error.printStackTrace()
// })
scope.launch {
try {
val loadedPlayable = withContext(Dispatchers.IO) {
@ -1324,6 +1351,9 @@ class PlaybackService : MediaSessionService() {
companion object {
private const val TAG = "PlaybackService"
private const val NOTIFICATION_ID = 5326
private const val CHANNEL_ID = "podcini_session_notification_channel_id"
private const val POSITION_EVENT_INTERVAL = 5L
const val ACTION_PLAYER_STATUS_CHANGED: String = "action.ac.mdiq.podcini.service.playerStatusChanged"

View File

@ -66,7 +66,13 @@ class OpmlReader {
}
}
}
eventType = xpp.next()
try {
// TODO: on first install app: java.io.IOException: Underlying input stream returned zero bytes
eventType = xpp.next()
} catch(e: Exception) {
Log.e(TAG, "xpp.next() invalid: $e")
break
}
}
Logd(TAG, "Parsing finished.")

View File

@ -74,6 +74,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -93,6 +94,7 @@ class MainActivity : CastEnabledActivity() {
private lateinit var mainView: View
private lateinit var audioPlayerFragment: AudioPlayerFragment
private lateinit var audioPlayerFragmentView: View
private lateinit var controllerFuture: ListenableFuture<MediaController>
private lateinit var navDrawer: View
private lateinit var dummyView : View
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
@ -503,7 +505,7 @@ class MainActivity : CastEnabledActivity() {
RatingDialog.init(this)
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
controllerFuture.addListener({
// Call controllerFuture.get() to retrieve the MediaController.
// MediaController implements the Player interface, so it can be
@ -533,7 +535,7 @@ class MainActivity : CastEnabledActivity() {
override fun onStop() {
super.onStop()
MediaController.releaseFuture(controllerFuture)
}
override fun onTrimMemory(level: Int) {

View File

@ -1,4 +1,9 @@
## 5.4.1
* fixed occasional crash of detecting existing OPML file for new install
* should have fixed the mal-functioning earphone buttons
## 5.4.0
* replaced thread with coroutines in DBWrite

View File

@ -0,0 +1,5 @@
Version 5.4.1 brings several changes:
* fixed occasional crash of detecting existing OPML file for new install
* should have fixed the mal-functioning earphone buttons