5.4.1 commit
This commit is contained in:
parent
36e1823d58
commit
797e9b64ab
|
@ -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 {
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue