-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMediaControllerState.kt
More file actions
158 lines (142 loc) · 5.45 KB
/
Copy pathMediaControllerState.kt
File metadata and controls
158 lines (142 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package io.oliverapps.radio.player.state
import android.content.ComponentName
import android.content.Context
import android.util.Log
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.media3.common.util.UnstableApi
import androidx.media3.session.MediaController
import androidx.media3.session.MediaSessionService
import androidx.media3.session.SessionToken
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import kotlin.reflect.KClass
@OptIn(UnstableApi::class)
@Composable
inline fun <reified S : MediaSessionService> rememberMediaController(
lifecycle: Lifecycle = LocalLifecycleOwner.current.lifecycle
): State<MediaController?> {
val appContext = LocalContext.current.applicationContext
val serviceKClass = S::class
val controllerManager = remember(serviceKClass) {
MediaControllerManager.getInstance(appContext, serviceKClass)
}
DisposableEffect(lifecycle, controllerManager) {
var isInitialized = false
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_START) {
if (!isInitialized) {
controllerManager.initialize()
isInitialized = true
}
} else if (event == Lifecycle.Event.ON_STOP) {
if (isInitialized) {
controllerManager.release()
isInitialized = false
}
}
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
// Only release if this specific effect sequence currently holds an active lease
if (isInitialized) {
controllerManager.release()
isInitialized = false
}
}
}
return controllerManager.controller
}
/**
* A Thread-safe, reference-counted manager that coordinates a single [MediaController]
* instance across multiple Activities or Compose destinations.
*/
@OptIn(UnstableApi::class)
class MediaControllerManager<S : MediaSessionService> private constructor(
context: Context,
private val serviceKClass: KClass<S>
) {
private val appContext = context.applicationContext
private var factory: ListenableFuture<MediaController>? = null
var controller = mutableStateOf<MediaController?>(null)
private set
private var refCount = 0
/**
* Increments the active reference connection claim.
* Builds a new connection only if no underlying controller channel exists.
*/
@Synchronized
fun initialize() {
refCount++
if (factory == null) {
val token = SessionToken(appContext, ComponentName(appContext, serviceKClass.java))
factory = MediaController.Builder(appContext, token).buildAsync().apply {
addListener(
{
try {
if (isDone) {
controller.value = get()
}
} catch (e: Exception) {
Log.e("MediaControllerManager", "Failed to resolve MediaController future", e)
controller.value = null
}
},
MoreExecutors.directExecutor()
)
}
} else if (factory?.isDone == true) {
// If another instance already connected it perfectly, immediately supply the active value
try {
controller.value = factory?.get()
} catch (e: Exception) {
controller.value = null
}
}
}
/**
* Decrements the connection claim. Completely tears down the underlying
* Media3 controller ONLY when the global reference count hitting zero indicates
* no visible UI components are left using it.
*/
@Synchronized
fun release() {
refCount--
if (refCount <= 0) {
refCount = 0 // Safeguard against underflow shifts
factory?.let {
try {
MediaController.releaseFuture(it)
} catch (e: Exception) {
Log.e("MediaControllerManager", "Error freeing MediaController lease", e)
}
controller.value = null
}
factory = null
}
}
companion object {
@Volatile
private var instances: MutableMap<KClass<out MediaSessionService>, MediaControllerManager<*>> = mutableMapOf()
@Suppress("UNCHECKED_CAST")
fun <S : MediaSessionService> getInstance(
context: Context,
serviceKClass: KClass<S>
): MediaControllerManager<S> {
return instances[serviceKClass] as? MediaControllerManager<S> ?: synchronized(this) {
(instances[serviceKClass] as? MediaControllerManager<S>) ?: MediaControllerManager(context, serviceKClass).also {
instances[serviceKClass] = it
}
}
}
}
}