Skip to content

Commit 1f1790b

Browse files
alperozturk96backportbot[bot]
authored andcommitted
fix(exo-player): service not start in time
Signed-off-by: alperozturk96 <alper_ozturk@proton.me>
1 parent 96186a6 commit 1f1790b

3 files changed

Lines changed: 181 additions & 114 deletions

File tree

Lines changed: 145 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/*
22
* Nextcloud - Android Client
33
*
4+
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
45
* SPDX-FileCopyrightText: 2024 Parneet Singh <gurayaparneet@gmail.com>
56
* SPDX-License-Identifier: AGPL-3.0-or-later
67
*/
@@ -41,9 +42,14 @@ import com.nextcloud.client.network.ClientFactory
4142
import com.nextcloud.common.NextcloudClient
4243
import com.nextcloud.utils.extensions.registerBroadcastReceiver
4344
import com.owncloud.android.MainApp
45+
import com.owncloud.android.R
4446
import com.owncloud.android.datamodel.ReceiverFlag
47+
import com.owncloud.android.lib.common.utils.Log_OC
48+
import kotlinx.coroutines.CoroutineScope
4549
import kotlinx.coroutines.Dispatchers
46-
import kotlinx.coroutines.runBlocking
50+
import kotlinx.coroutines.SupervisorJob
51+
import kotlinx.coroutines.cancel
52+
import kotlinx.coroutines.launch
4753
import kotlinx.coroutines.withContext
4854
import javax.inject.Inject
4955

@@ -52,21 +58,21 @@ class BackgroundPlayerService :
5258
MediaSessionService(),
5359
Injectable {
5460

61+
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
62+
5563
private val seekBackSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_BACK, Bundle.EMPTY)
5664
private val seekForwardSessionCommand = SessionCommand(SESSION_COMMAND_ACTION_SEEK_FORWARD, Bundle.EMPTY)
5765

58-
val seekForward =
59-
CommandButton.Builder()
60-
.setDisplayName("Seek Forward")
61-
.setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15))
66+
private val seekForward =
67+
CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_FORWARD_15))
68+
.setDisplayName(getString(R.string.media_player_seek_forward))
6269
.setSessionCommand(seekForwardSessionCommand)
6370
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 2) })
6471
.build()
6572

66-
val seekBackward =
67-
CommandButton.Builder()
68-
.setDisplayName("Seek Backward")
69-
.setIconResId(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_5))
73+
private val seekBackward =
74+
CommandButton.Builder(CommandButton.getIconResIdForIconConstant(CommandButton.ICON_SKIP_BACK_15))
75+
.setDisplayName(getString(R.string.media_player_seek_backward))
7076
.setSessionCommand(seekBackSessionCommand)
7177
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 0) })
7278
.build()
@@ -76,21 +82,37 @@ class BackgroundPlayerService :
7682

7783
@Inject
7884
lateinit var userAccountManager: UserAccountManager
79-
lateinit var exoPlayer: ExoPlayer
85+
86+
private lateinit var exoPlayer: ExoPlayer
8087
private var mediaSession: MediaSession? = null
8188

89+
private var isPlayerReady = false
90+
8291
private val stopReceiver = object : BroadcastReceiver() {
8392
override fun onReceive(context: Context?, intent: Intent?) {
8493
when (intent?.action) {
8594
RELEASE_MEDIA_SESSION_BROADCAST_ACTION -> release()
86-
STOP_MEDIA_SESSION_BROADCAST_ACTION -> exoPlayer.stop()
95+
STOP_MEDIA_SESSION_BROADCAST_ACTION -> {
96+
if (isPlayerReady) {
97+
exoPlayer.stop()
98+
} else {
99+
stopSelf()
100+
}
101+
}
87102
}
88103
}
89104
}
90105

91106
override fun onCreate() {
92107
super.onCreate()
93108

109+
MainApp.getAppComponent().inject(this)
110+
111+
exoPlayer = ExoPlayer.Builder(this).build()
112+
mediaSession = buildMediaSession(exoPlayer)
113+
114+
setMediaNotificationProvider(buildNotificationProvider())
115+
94116
registerBroadcastReceiver(
95117
stopReceiver,
96118
IntentFilter().apply {
@@ -100,109 +122,121 @@ class BackgroundPlayerService :
100122
ReceiverFlag.NotExported
101123
)
102124

103-
MainApp.getAppComponent().inject(this)
104-
initNextcloudExoPlayer()
105-
106-
setMediaNotificationProvider(object : DefaultMediaNotificationProvider(this) {
107-
override fun getMediaButtons(
108-
session: MediaSession,
109-
playerCommands: Player.Commands,
110-
customLayout: ImmutableList<CommandButton>,
111-
showPauseButton: Boolean
112-
): ImmutableList<CommandButton> {
113-
val playPauseButton =
114-
CommandButton.Builder()
115-
.setDisplayName("PlayPause")
116-
.setIconResId(
117-
CommandButton.getIconResIdForIconConstant(
118-
if (mediaSession?.player?.isPlaying == true) {
119-
CommandButton.ICON_PAUSE
120-
} else {
121-
CommandButton.ICON_PLAY
122-
}
123-
)
124-
)
125-
.setPlayerCommand(COMMAND_PLAY_PAUSE)
126-
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) })
127-
.build()
128-
129-
val myCustomButtonsLayout =
130-
ImmutableList.of(seekBackward, playPauseButton, seekForward)
131-
return myCustomButtonsLayout
132-
}
133-
})
125+
initExoPlayer()
134126
}
135127

136-
private fun initNextcloudExoPlayer() {
137-
runBlocking {
138-
var nextcloudClient: NextcloudClient
139-
withContext(Dispatchers.IO) {
140-
nextcloudClient = clientFactory.createNextcloudClient(userAccountManager.user)
141-
}
142-
nextcloudClient.let {
143-
exoPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
144-
mediaSession =
145-
MediaSession.Builder(applicationContext, exoPlayer)
146-
// set id to distinct this session to avoid crash
147-
// in case session release delayed a bit and
148-
// we start another session for eg. video
149-
.setId(BACKGROUND_MEDIA_SESSION_ID)
150-
.setCustomLayout(listOf(seekBackward, seekForward))
151-
.setCallback(object : MediaSession.Callback {
152-
override fun onConnect(
153-
session: MediaSession,
154-
controller: MediaSession.ControllerInfo
155-
): ConnectionResult = AcceptedResultBuilder(mediaSession!!)
156-
.setAvailablePlayerCommands(
157-
ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
158-
.remove(COMMAND_SEEK_TO_NEXT)
159-
.remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
160-
.remove(COMMAND_SEEK_TO_PREVIOUS)
161-
.remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
162-
.build()
163-
)
164-
.setAvailableSessionCommands(
165-
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
166-
.addSessionCommands(
167-
listOf(seekBackSessionCommand, seekForwardSessionCommand)
168-
).build()
169-
)
170-
.build()
171-
172-
override fun onPostConnect(session: MediaSession, controller: MediaSession.ControllerInfo) {
173-
session.setCustomLayout(listOf(seekBackward, seekForward))
174-
}
175-
176-
override fun onCustomCommand(
177-
session: MediaSession,
178-
controller: MediaSession.ControllerInfo,
179-
customCommand: SessionCommand,
180-
args: Bundle
181-
): ListenableFuture<SessionResult> = when (customCommand.customAction) {
182-
SESSION_COMMAND_ACTION_SEEK_FORWARD -> {
183-
session.player.seekForward()
184-
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
185-
}
186-
187-
SESSION_COMMAND_ACTION_SEEK_BACK -> {
188-
session.player.seekBack()
189-
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
190-
}
191-
192-
else -> super.onCustomCommand(session, controller, customCommand, args)
193-
}
194-
})
195-
.build()
128+
private fun initExoPlayer() {
129+
serviceScope.launch {
130+
try {
131+
val nextcloudClient: NextcloudClient = withContext(Dispatchers.IO) {
132+
clientFactory.createNextcloudClient(userAccountManager.user)
133+
}
134+
135+
val realPlayer = createNextcloudExoplayer(this@BackgroundPlayerService, nextcloudClient)
136+
137+
exoPlayer.release()
138+
exoPlayer = realPlayer
139+
isPlayerReady = true
140+
141+
// Update the session to use the real player
142+
mediaSession?.player = realPlayer
143+
} catch (e: Exception) {
144+
Log_OC.e(TAG, "Failed to initialise Nextcloud ExoPlayer: ${e.message}")
145+
stopSelf()
196146
}
197147
}
198148
}
199149

150+
private fun buildMediaSession(player: ExoPlayer): MediaSession =
151+
MediaSession.Builder(applicationContext, player)
152+
.setId(BACKGROUND_MEDIA_SESSION_ID)
153+
.setCustomLayout(listOf(seekBackward, seekForward))
154+
.setCallback(object : MediaSession.Callback {
155+
override fun onConnect(
156+
session: MediaSession,
157+
controller: MediaSession.ControllerInfo
158+
): ConnectionResult = AcceptedResultBuilder(mediaSession ?: session)
159+
.setAvailablePlayerCommands(
160+
ConnectionResult.DEFAULT_PLAYER_COMMANDS.buildUpon()
161+
.remove(COMMAND_SEEK_TO_NEXT)
162+
.remove(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)
163+
.remove(COMMAND_SEEK_TO_PREVIOUS)
164+
.remove(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)
165+
.build()
166+
)
167+
.setAvailableSessionCommands(
168+
ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon()
169+
.addSessionCommands(
170+
listOf(seekBackSessionCommand, seekForwardSessionCommand)
171+
).build()
172+
)
173+
.build()
174+
175+
override fun onPostConnect(
176+
session: MediaSession,
177+
controller: MediaSession.ControllerInfo
178+
) {
179+
session.setCustomLayout(listOf(seekBackward, seekForward))
180+
}
181+
182+
override fun onCustomCommand(
183+
session: MediaSession,
184+
controller: MediaSession.ControllerInfo,
185+
customCommand: SessionCommand,
186+
args: Bundle
187+
): ListenableFuture<SessionResult> = when (customCommand.customAction) {
188+
SESSION_COMMAND_ACTION_SEEK_FORWARD -> {
189+
session.player.seekForward()
190+
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
191+
}
192+
193+
SESSION_COMMAND_ACTION_SEEK_BACK -> {
194+
session.player.seekBack()
195+
Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
196+
}
197+
198+
else -> super.onCustomCommand(session, controller, customCommand, args)
199+
}
200+
})
201+
.build()
202+
203+
private fun buildNotificationProvider() = object : DefaultMediaNotificationProvider(this) {
204+
val icon = if (mediaSession?.player?.isPlaying == true) {
205+
CommandButton.ICON_PAUSE
206+
} else {
207+
CommandButton.ICON_PLAY
208+
}
209+
210+
val displayName = if (mediaSession?.player?.isPlaying == true) {
211+
getString(R.string.media_player_pause)
212+
} else {
213+
getString(R.string.media_player_play)
214+
}
215+
216+
override fun getMediaButtons(
217+
session: MediaSession,
218+
playerCommands: Player.Commands,
219+
customLayout: ImmutableList<CommandButton>,
220+
showPauseButton: Boolean
221+
): ImmutableList<CommandButton> {
222+
val playPauseButton =
223+
CommandButton.Builder(CommandButton.getIconResIdForIconConstant(icon))
224+
.setDisplayName(displayName)
225+
.setPlayerCommand(COMMAND_PLAY_PAUSE)
226+
.setExtras(Bundle().apply { putInt(COMMAND_KEY_COMPACT_VIEW_INDEX, 1) })
227+
.build()
228+
229+
return ImmutableList.of(seekBackward, playPauseButton, seekForward)
230+
}
231+
}
232+
200233
override fun onTaskRemoved(rootIntent: Intent?) {
201234
release()
202235
}
203236

204237
override fun onDestroy() {
205238
unregisterReceiver(stopReceiver)
239+
serviceScope.cancel()
206240
mediaSession?.run {
207241
player.release()
208242
release()
@@ -214,28 +248,26 @@ class BackgroundPlayerService :
214248
private fun release() {
215249
val player = mediaSession?.player
216250
if (player?.playWhenReady == true) {
217-
// Make sure the service is not in foreground.
218251
player.pause()
219252
}
220-
// Bug in Android 14, https://github.com/androidx/media/issues/805
221-
// that sometimes onTaskRemove() doesn't get called immediately
222-
// eventually gets called so the service stops but the notification doesn't clear out.
223-
// [WORKAROUND] So, explicitly removing the notification here.
224-
// TODO revisit after bug solved!
225253
val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
226254
nm.cancel(DefaultMediaNotificationProvider.DEFAULT_NOTIFICATION_ID)
227255
stopSelf()
228256
}
229257

230-
override fun onGetSession(p0: MediaSession.ControllerInfo): MediaSession? = mediaSession
258+
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? = mediaSession
231259

232260
companion object {
261+
private val TAG = BackgroundPlayerService::class.java.simpleName
262+
233263
private const val SESSION_COMMAND_ACTION_SEEK_BACK = "SESSION_COMMAND_ACTION_SEEK_BACK"
234264
private const val SESSION_COMMAND_ACTION_SEEK_FORWARD = "SESSION_COMMAND_ACTION_SEEK_FORWARD"
265+
private const val BACKGROUND_MEDIA_SESSION_ID =
266+
"com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID"
235267

236-
private const val BACKGROUND_MEDIA_SESSION_ID = "com.nextcloud.client.media.BACKGROUND_MEDIA_SESSION_ID"
237-
238-
const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.RELEASE_MEDIA_SESSION"
239-
const val STOP_MEDIA_SESSION_BROADCAST_ACTION = "com.nextcloud.client.media.STOP_MEDIA_SESSION"
268+
const val RELEASE_MEDIA_SESSION_BROADCAST_ACTION =
269+
"com.nextcloud.client.media.RELEASE_MEDIA_SESSION"
270+
const val STOP_MEDIA_SESSION_BROADCAST_ACTION =
271+
"com.nextcloud.client.media.STOP_MEDIA_SESSION"
240272
}
241273
}

app/src/main/java/com/nextcloud/client/media/PlayerService.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ class PlayerService : Service() {
6464
putExtra(IS_MEDIA_CONTROL_LAYOUT_READY, false)
6565
}
6666
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
67-
startForeground(file)
6867
}
6968

7069
override fun onStart() {
@@ -133,6 +132,19 @@ class PlayerService : Service() {
133132
override fun onBind(intent: Intent?): IBinder? = Binder(this)
134133

135134
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
135+
Log_OC.d(TAG, "player service started")
136+
if (!isRunning) {
137+
val file = intent.getParcelableArgument(EXTRA_FILE, OCFile::class.java)
138+
if (file != null) {
139+
startForeground(file)
140+
} else {
141+
startForegroundWithPlaceholder()
142+
stopForeground(STOP_FOREGROUND_REMOVE)
143+
stopSelf()
144+
return START_NOT_STICKY
145+
}
146+
}
147+
136148
when (intent.action) {
137149
ACTION_PLAY -> onActionPlay(intent)
138150
ACTION_STOP -> onActionStop()
@@ -142,6 +154,23 @@ class PlayerService : Service() {
142154
return START_NOT_STICKY
143155
}
144156

157+
private fun startForegroundWithPlaceholder() {
158+
val ticker = String.format(getString(R.string.media_notif_ticker), getString(R.string.app_name))
159+
notificationBuilder.run {
160+
setSmallIcon(R.drawable.ic_play_arrow)
161+
setWhen(System.currentTimeMillis())
162+
setOngoing(false)
163+
setContentTitle(ticker)
164+
setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_MEDIA)
165+
}
166+
ForegroundServiceHelper.startService(
167+
this,
168+
R.string.media_notif_ticker,
169+
notificationBuilder.build(),
170+
ForegroundServiceType.MediaPlayback
171+
)
172+
}
173+
145174
private fun onActionToggle() {
146175
player.run {
147176
if (isPlaying) {

0 commit comments

Comments
 (0)