Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ androidx-material-icons-core = { module = "androidx.compose.material:material-ic
androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" }
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" }
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "androidx-navigation3" }
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "androidx-navigation3" }
Expand Down
4 changes: 3 additions & 1 deletion wear/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {

android {
namespace = "com.example.wear"
compileSdk = 36
compileSdkPreview = "CinnamonBun"

defaultConfig {
applicationId = "com.example.wear"
Expand All @@ -35,6 +35,7 @@ android {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
useLibrary("wear-sdk")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a code snippet to be documented in DAC?

kotlin {
jvmToolchain(21)
compilerOptions {
Expand Down Expand Up @@ -64,6 +65,7 @@ dependencies {
implementation((libs.androidx.credentials.play.services.auth))
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.media3.session)
implementation(libs.androidx.wear.input)
implementation(libs.androidx.wear.phone.interactions)
implementation(libs.android.identity.googleid)
Expand Down
16 changes: 16 additions & 0 deletions wear/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,22 @@
</service>
<!-- [END android_wear_datalayer_mywearablelistenerservice_manifest] -->

<activity
android:name=".snippets.audio.RemoteMediaSessionActivity"
android:exported="true"
android:taskAffinity="">
<!-- [START android_wear_remote_media_session_manifest] -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- [END android_wear_remote_media_session_manifest] -->
<intent-filter>
<action android:name="com.google.wear.services.media.action.REMOTE_MEDIA_ACTIVITY" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not including this in the code snippet?

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.wear.snippets.audio

import android.app.Application
import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import androidx.media3.common.Player
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import java.util.concurrent.Executor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.guava.await

class RemoteMediaActivityViewModel(application: Application) : AndroidViewModel(application) {
private val mediaSessionManager = application.getSystemService(MediaSessionManager::class.java)
private val mainExecutor: Executor = ContextCompat.getMainExecutor(application)

@RequiresApi(Build.VERSION_CODES.CUR_DEVELOPMENT)
private val sessionFlow: Flow<List<MediaSession.Token>> = callbackFlow {
// [START android_wear_remote_media_session_listener]
val callback =
MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
val tokens = controllers?.map { it.sessionToken } ?: emptyList()
trySendBlocking(tokens)
}
// [END android_wear_remote_media_session_listener]
// [START android_wear_remote_media_session_register_listener]
mediaSessionManager.addOnActiveSessionsForPackageChangedListener(
application.packageName,
mainExecutor,
callback,
)
// [END android_wear_remote_media_session_register_listener]
// [START android_wear_remote_media_session_get_sessions]
trySendBlocking(mediaSessionManager.getActiveSessionsForPackage(application.packageName))
// [END android_wear_remote_media_session_get_sessions]
// [START android_wear_remote_media_session_remove_listener]
awaitClose { mediaSessionManager.removeOnActiveSessionsForPackageChangedListener(callback) }
// [END android_wear_remote_media_session_remove_listener]
}

@OptIn(ExperimentalCoroutinesApi::class)
@RequiresApi(37)
// [START android_wear_remote_media_session_media_controller]
private val controllerFlow: Flow<MediaController?> =
sessionFlow
.distinctUntilChanged()
.flatMapLatest { tokens ->
val token = tokens.firstOrNull() ?: return@flatMapLatest kotlinx.coroutines.flow.flowOf(null)
callbackFlow {
val sessionToken = SessionToken.createSessionToken(application, token).await()
val controller = MediaController.Builder(application, sessionToken).buildAsync().await()
val listener = object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
trySendBlocking(controller)
}
}
controller.addListener(listener)
trySendBlocking(controller)
awaitClose {
controller.removeListener(listener)
controller.release()
}
}
}
// [END android_wear_remote_media_session_media_controller]

// [START android_wear_remote_media_session_listen_events]
private val MediaController.controllerEventFlow: Flow<Unit>
get() =
callbackFlow<Unit> {
val listener =
object : Player.Listener {
override fun onEvents(player: Player, events: Player.Events) {
trySendBlocking(Unit)
}
}
this@controllerEventFlow.addListener(listener)
awaitClose { this@controllerEventFlow.removeListener(listener) }
}

// [END android_wear_remote_media_session_listen_events]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.wear.snippets.audio

import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.lifecycle.ViewModelProvider

class RemoteMediaSessionActivity : ComponentActivity() {

private lateinit var viewModel: RemoteMediaActivityViewModel
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The viewModel property is initialized but never used in the activity. Consider using it to observe the media state or remove it if this is just a skeleton activity.


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= 37) {
viewModel = ViewModelProvider(this)[RemoteMediaActivityViewModel::class.java]
setContent {
// set the UI content here
}
}
}
}
Loading