Skip to content

Commit 4cf22fa

Browse files
committed
Adds Remote Media Controls API
1 parent b17a509 commit 4cf22fa

File tree

5 files changed

+136
-1
lines changed

5 files changed

+136
-1
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ androidx-material-icons-core = { module = "androidx.compose.material:material-ic
179179
androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
180180
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
181181
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
182+
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
182183
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
183184
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" }
184185
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" }

wear/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ plugins {
88

99
android {
1010
namespace = "com.example.wear"
11-
compileSdk = 36
11+
compileSdkPreview = "CinnamonBun"
1212

1313
defaultConfig {
1414
applicationId = "com.example.wear"
@@ -35,6 +35,7 @@ android {
3535
sourceCompatibility = JavaVersion.VERSION_21
3636
targetCompatibility = JavaVersion.VERSION_21
3737
}
38+
useLibrary("wear-sdk")
3839
kotlin {
3940
jvmToolchain(21)
4041
compilerOptions {
@@ -64,6 +65,7 @@ dependencies {
6465
implementation((libs.androidx.credentials.play.services.auth))
6566
implementation(libs.androidx.media3.exoplayer)
6667
implementation(libs.androidx.media3.ui)
68+
implementation(libs.androidx.media3.session)
6769
implementation(libs.androidx.wear.input)
6870
implementation(libs.androidx.wear.phone.interactions)
6971
implementation(libs.android.identity.googleid)

wear/src/main/AndroidManifest.xml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,22 @@
424424
</service>
425425
<!-- [END android_wear_datalayer_mywearablelistenerservice_manifest] -->
426426

427+
<activity
428+
android:name=".snippets.audio.RemoteMediaSessionActivity"
429+
android:exported="true"
430+
android:taskAffinity="">
431+
<!-- [START android_wear_remote_media_session_manifest] -->
432+
<intent-filter>
433+
<action android:name="android.intent.action.MAIN" />
434+
<category android:name="android.intent.category.LAUNCHER" />
435+
</intent-filter>
436+
<!-- [END android_wear_remote_media_session_manifest] -->
437+
<intent-filter>
438+
<action android:name="com.google.wear.services.media.action.REMOTE_MEDIA_ACTIVITY" />
439+
<category android:name="android.intent.category.DEFAULT" />
440+
</intent-filter>
441+
</activity>
442+
427443
</application>
428444

429445
</manifest>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.example.wear.snippets.audio
2+
3+
import android.app.Application
4+
import android.media.session.MediaSession
5+
import android.media.session.MediaSessionManager
6+
import android.os.Build
7+
import androidx.annotation.RequiresApi
8+
import androidx.core.content.ContextCompat
9+
import androidx.lifecycle.AndroidViewModel
10+
import androidx.media3.common.Player
11+
import androidx.media3.session.MediaController
12+
import androidx.media3.session.SessionToken
13+
import kotlinx.coroutines.ExperimentalCoroutinesApi
14+
import kotlinx.coroutines.channels.awaitClose
15+
import kotlinx.coroutines.channels.trySendBlocking
16+
import kotlinx.coroutines.flow.Flow
17+
import kotlinx.coroutines.flow.callbackFlow
18+
import kotlinx.coroutines.flow.distinctUntilChanged
19+
import kotlinx.coroutines.flow.flatMapLatest
20+
import kotlinx.coroutines.flow.map
21+
import kotlinx.coroutines.flow.mapNotNull
22+
import kotlinx.coroutines.guava.await
23+
import java.util.concurrent.Executor
24+
25+
class RemoteMediaActivityViewModel(application: Application) : AndroidViewModel(application) {
26+
private val mediaSessionManager = application.getSystemService(MediaSessionManager::class.java)
27+
private val mainExecutor: Executor = ContextCompat.getMainExecutor(application)
28+
29+
@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
30+
private val sessionFlow: Flow<List<MediaSession.Token>> = callbackFlow {
31+
// [START android_wear_remote_media_session_listener]
32+
val callback =
33+
MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
34+
val tokens = controllers?.map { it.sessionToken } ?: emptyList()
35+
trySendBlocking(tokens)
36+
}
37+
// [END android_wear_remote_media_session_listener]
38+
// [START android_wear_remote_media_session_register_listener]
39+
mediaSessionManager.addOnActiveSessionsForPackageChangedListener(
40+
application.packageName,
41+
mainExecutor,
42+
callback,
43+
)
44+
// [END android_wear_remote_media_session_register_listener]
45+
// [START android_wear_remote_media_session_get_sessions]
46+
trySendBlocking(mediaSessionManager.getActiveSessionsForPackage(application.packageName))
47+
// [END android_wear_remote_media_session_get_sessions]
48+
// [START android_wear_remote_media_session_remove_listener]
49+
awaitClose { mediaSessionManager.removeOnActiveSessionsForPackageChangedListener(callback) }
50+
// [END android_wear_remote_media_session_remove_listener]
51+
}
52+
53+
@OptIn(ExperimentalCoroutinesApi::class)
54+
@RequiresApi(37)
55+
// [START android_wear_remote_media_session_media_controller]
56+
private val controllerFlow: Flow<MediaController?> =
57+
sessionFlow
58+
.distinctUntilChanged()
59+
.flatMapLatest { tokens ->
60+
val token = tokens.firstOrNull() ?: return@flatMapLatest kotlinx.coroutines.flow.flowOf(null)
61+
callbackFlow {
62+
val sessionToken = SessionToken.createSessionToken(application, token).await()
63+
val controller = MediaController.Builder(application, sessionToken).buildAsync().await()
64+
val listener = object : Player.Listener {
65+
override fun onEvents(player: Player, events: Player.Events) {
66+
trySendBlocking(controller)
67+
}
68+
}
69+
controller.addListener(listener)
70+
trySendBlocking(controller)
71+
awaitClose {
72+
controller.removeListener(listener)
73+
controller.release()
74+
}
75+
}
76+
}
77+
// [END android_wear_remote_media_session_media_controller]
78+
79+
// [START android_wear_remote_media_session_listen_events]
80+
private val MediaController.controllerEventFlow: Flow<Unit>
81+
get() =
82+
callbackFlow<Unit> {
83+
val listener =
84+
object : Player.Listener {
85+
override fun onEvents(player: Player, events: Player.Events) {
86+
trySendBlocking(Unit)
87+
}
88+
}
89+
this@controllerEventFlow.addListener(listener)
90+
awaitClose { this@controllerEventFlow.removeListener(listener) }
91+
}
92+
93+
// [END android_wear_remote_media_session_listen_events]
94+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.example.wear.snippets.audio
2+
3+
import android.os.Build
4+
import android.os.Bundle
5+
import androidx.activity.ComponentActivity
6+
import androidx.activity.compose.setContent
7+
import androidx.lifecycle.ViewModelProvider
8+
9+
class RemoteMediaSessionActivity : ComponentActivity() {
10+
11+
private lateinit var viewModel: RemoteMediaActivityViewModel
12+
13+
override fun onCreate(savedInstanceState: Bundle?) {
14+
super.onCreate(savedInstanceState)
15+
if (Build.VERSION.SDK_INT >= 37) {
16+
viewModel = ViewModelProvider(this)[RemoteMediaActivityViewModel::class.java]
17+
setContent {
18+
// set the UI content here
19+
}
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)