From a5675d9f28b29ab3dbebc482de7126176bf87acd Mon Sep 17 00:00:00 2001 From: Katherine Kuan <843428+kkuan2011@users.noreply.github.com> Date: Thu, 27 Aug 2026 05:30:15 +0000 Subject: [PATCH 01/10] Add code snippets for Media I migration --- gradle/libs.versions.toml | 2 + media/build.gradle.kts | 8 + .../main/java/com/example/media/Assistant.kt | 138 +++++++++ .../main/java/com/example/media/AudioFocus.kt | 187 ++++++++++++ .../main/java/com/example/media/EditingApp.kt | 171 +++++++++++ .../java/com/example/media/PlaybackApp.kt | 60 ++++ .../main/java/com/example/media/Sharing.kt | 41 +++ .../com/example/media/SurfacesAndroidTv.kt | 28 ++ .../java/com/example/media/SurfacesMobile.kt | 278 ++++++++++++++++++ 9 files changed, 913 insertions(+) create mode 100644 media/src/main/java/com/example/media/Assistant.kt create mode 100644 media/src/main/java/com/example/media/AudioFocus.kt create mode 100644 media/src/main/java/com/example/media/EditingApp.kt create mode 100644 media/src/main/java/com/example/media/Sharing.kt create mode 100644 media/src/main/java/com/example/media/SurfacesAndroidTv.kt create mode 100644 media/src/main/java/com/example/media/SurfacesMobile.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39a34593d..ed17f7dcf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -224,10 +224,12 @@ androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmode androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-viewmodel-navigation3" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } +androidx-media = { module = "androidx.media:media", version = "1.7.0" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } androidx-media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } +androidx-media3-transformer = { module = "androidx.media3:media3-transformer", version.ref = "media3" } androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" } androidx-media3-ui-compose = { module = "androidx.media3:media3-ui-compose", version.ref = "media3Ui" } androidx-media3-ui-compose-material3 = { module = "androidx.media3:media3-ui-compose-material3", version.ref = "media3Ui" } diff --git a/media/build.gradle.kts b/media/build.gradle.kts index 7d3545b0d..64d6f28c7 100644 --- a/media/build.gradle.kts +++ b/media/build.gradle.kts @@ -26,5 +26,13 @@ android { } dependencies { + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.media) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.effect) implementation(libs.androidx.media3.exoplayer) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.transformer) + implementation(libs.androidx.media3.ui) + implementation(libs.guava.android) } diff --git a/media/src/main/java/com/example/media/Assistant.kt b/media/src/main/java/com/example/media/Assistant.kt new file mode 100644 index 000000000..9581660e5 --- /dev/null +++ b/media/src/main/java/com/example/media/Assistant.kt @@ -0,0 +1,138 @@ +/* + * 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.media + +import android.content.Context +import android.os.Bundle +import android.provider.MediaStore +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.session.MediaSessionCompat +import android.util.Log +import androidx.media.MediaBrowserServiceCompat + +private const val TAG = "AssistantSnippets" +private const val MEDIA_ID_EMPTY_ROOT = "@empty@" + +private fun setSessionFlags(session: MediaSessionCompat) { + // [START android_media_assistant_set_flags] + session.setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or + MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS + ) + // [END android_media_assistant_set_flags] +} + +private class SearchMediaSessionCallback : MediaSessionCompat.Callback() { + private var isArtistFocus = false + private var isAlbumFocus = false + private var artist: String? = null + private var album: String? = null + + // [START android_media_assistant_on_play_from_search] + override fun onPlayFromSearch(query: String?, extras: Bundle?) { + if (query.isNullOrEmpty()) { + // The user provided generic string e.g. 'Play music' + // Build appropriate playlist queue + } else { + // Build a queue based on songs that match "query" or "extras" param + val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS) + if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) { + isArtistFocus = true + artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) + } else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) { + isAlbumFocus = true + album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) + } + + // Implement additional "extras" param filtering + } + + // Implement your logic to retrieve the queue + var result: String? = when { + isArtistFocus -> artist?.also { + searchMusicByArtist(it) + } + isAlbumFocus -> album?.also { + searchMusicByAlbum(it) + } + else -> null + } + result = result ?: run { + // No focus found, search by query for song title + query?.also { + searchMusicBySongTitle(it) + } + } + + if (result?.isNotEmpty() == true) { + // Immediately start playing from the beginning of the search results + // Implement your logic to start playing music + playMusic(result) + } else { + // Handle no queue found. Stop playing if the app + // is currently playing a song + } + } + // [END android_media_assistant_on_play_from_search] + + private fun searchMusicByArtist(artist: String): String = artist + private fun searchMusicByAlbum(album: String): String = album + private fun searchMusicBySongTitle(title: String): String = title + private fun playMusic(track: String) {} +} + +private class MyMediaBrowserService : MediaBrowserServiceCompat() { + private val packageValidator = PackageValidator() + + // [START android_media_assistant_on_get_root] + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? { + + // To ensure you are not allowing any arbitrary app to browse your app's contents, you + // need to check the origin: + if (!packageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { + // If the request comes from an untrusted package, return an empty browser root. + // If you return null, then the media browser will not be able to connect and + // no further calls will be made to other media browsing methods. + Log.i( + TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. Returning empty " + + "browser root so all apps can use MediaController. $clientPackageName" + ) + return MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null) + } + + // Return browser roots for browsing... + // [START_EXCLUDE silent] + return null + // [END_EXCLUDE] + } + // [END android_media_assistant_on_get_root] + + override fun onLoadChildren( + parentId: String, + result: MediaBrowserServiceCompat.Result> + ) { + result.sendResult(null) + } +} + +private class PackageValidator { + fun isCallerAllowed(service: Context, packageName: String, uid: Int): Boolean = true +} diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt new file mode 100644 index 000000000..77ae1f5d6 --- /dev/null +++ b/media/src/main/java/com/example/media/AudioFocus.kt @@ -0,0 +1,187 @@ +/* + * 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.media + +import android.content.Context +import android.media.AudioAttributes +import android.media.AudioFocusRequest +import android.media.AudioManager +import android.os.Handler +import android.os.Looper +import android.support.v4.media.session.MediaControllerCompat +import java.util.concurrent.TimeUnit + +private class AudioFocusManager(private val context: Context, private val mediaController: MediaControllerCompat) { + private lateinit var audioManager: AudioManager + private lateinit var focusRequest: AudioFocusRequest + private val handler = Handler(Looper.getMainLooper()) + private var delayedStopRunnable = Runnable { + mediaController.transportControls.stop() + } + + private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + onAudioFocusChange(focusChange) + } + + private fun requestFocusPostOreo() { + // [START android_media_audio_focus_request] + // initializing variables for audio focus and playback management + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { + setAudioAttributes(AudioAttributes.Builder().run { + setUsage(AudioAttributes.USAGE_GAME) + setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + build() + }) + setAcceptsDelayedFocusGain(true) + setOnAudioFocusChangeListener(afChangeListener, handler) + build() + } + val focusLock = Any() + + var playbackDelayed = false + var playbackNowAuthorized = false + + // requesting audio focus and processing the response + val res = audioManager.requestAudioFocus(focusRequest) + synchronized(focusLock) { + playbackNowAuthorized = when (res) { + AudioManager.AUDIOFOCUS_REQUEST_FAILED -> false + AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> { + playbackNow() + true + } + AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> { + playbackDelayed = true + false + } + else -> false + } + } + // [END android_media_audio_focus_request] + } + + private var playbackDelayed = false + private var resumeOnFocusGain = false + private val focusLock = Any() + + private fun onAudioFocusChange(focusChange: Int) { + // [START android_media_audio_focus_request_listener_impl] + // implementing OnAudioFocusChangeListener to react to focus changes + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> + if (playbackDelayed || resumeOnFocusGain) { + synchronized(focusLock) { + playbackDelayed = false + resumeOnFocusGain = false + } + playbackNow() + } + AudioManager.AUDIOFOCUS_LOSS -> { + synchronized(focusLock) { + resumeOnFocusGain = false + playbackDelayed = false + } + pausePlayback() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + synchronized(focusLock) { + // only resume if playback is being interrupted + resumeOnFocusGain = isPlaying() + playbackDelayed = false + } + pausePlayback() + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // ... pausing or ducking depends on your app + } + } + // [END android_media_audio_focus_request_listener_impl] + } + + @Suppress("DEPRECATION") + private fun requestFocusPreOreo() { + // [START android_media_audio_focus_request_pre_o] + audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + lateinit var afChangeListener: AudioManager.OnAudioFocusChangeListener + + // [START_EXCLUDE silent] + /* + // [END_EXCLUDE] + ... + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + // Request audio focus for playback + val result: Int = audioManager.requestAudioFocus( + afChangeListener, + // Use the music stream. + AudioManager.STREAM_MUSIC, + // Request permanent focus. + AudioManager.AUDIOFOCUS_GAIN + ) + + if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + // Start playback + } + // [END android_media_audio_focus_request_pre_o] + } + + @Suppress("DEPRECATION") + private fun abandonFocusExample(afChangeListener: AudioManager.OnAudioFocusChangeListener) { + // [START android_media_audio_focus_abandon] + audioManager.abandonAudioFocus(afChangeListener) + // [END android_media_audio_focus_abandon] + } + + private fun setupHandlerChangeListener() { + // [START android_media_audio_focus_change_listener] + val handler = Handler() + val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + // Permanent loss of audio focus + // Pause playback immediately + mediaController.transportControls.pause() + // Wait 30 seconds before stopping playback + handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // Pause playback + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // Lower the volume, keep playing + } + AudioManager.AUDIOFOCUS_GAIN -> { + // Your app has been granted audio focus again + // Raise volume to normal, restart playback if necessary + } + } + } + // [END android_media_audio_focus_change_listener] + + // [START android_media_audio_focus_delayed_stop_runnable] + var delayedStopRunnable = Runnable { + mediaController.transportControls.stop() + } + // [END android_media_audio_focus_delayed_stop_runnable] + } + + private fun playbackNow() {} + private fun pausePlayback() {} + private fun isPlaying(): Boolean = true +} diff --git a/media/src/main/java/com/example/media/EditingApp.kt b/media/src/main/java/com/example/media/EditingApp.kt new file mode 100644 index 000000000..614a9d4c5 --- /dev/null +++ b/media/src/main/java/com/example/media/EditingApp.kt @@ -0,0 +1,171 @@ +/* + * 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.media + +import android.content.Context +import android.graphics.Matrix +import android.net.Uri +import androidx.annotation.OptIn +import androidx.media3.common.MediaItem +import androidx.media3.common.MimeTypes +import androidx.media3.common.audio.ChannelMixingAudioProcessor +import androidx.media3.common.util.UnstableApi +import androidx.media3.effect.Crop +import androidx.media3.effect.MatrixTransformation +import androidx.media3.effect.ScaleAndRotateTransformation +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.audio.AudioSink +import androidx.media3.exoplayer.audio.DefaultAudioSink +import androidx.media3.transformer.Composition +import androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR +import androidx.media3.transformer.EditedMediaItem +import androidx.media3.transformer.Effects +import androidx.media3.transformer.EditedMediaItemSequence +import androidx.media3.transformer.Transformer +import com.google.common.collect.ImmutableList +import kotlin.math.min + +@OptIn(UnstableApi::class) +private fun configureTranscode(context: Context) { + // [START android_media_editing_transcode] + val transformer = Transformer.Builder(context) + .setVideoMimeType(MimeTypes.VIDEO_H265) + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .build() + // [END android_media_editing_transcode] +} + +@OptIn(UnstableApi::class) +private fun setHdrMode(videoSequence: EditedMediaItemSequence) { + // [START android_media_editing_set_hdr_mode] + val composition = Composition.Builder( + ImmutableList.of(videoSequence)) + .setHdrMode(HDR_MODE_KEEP_HDR) + .build() + // [END android_media_editing_set_hdr_mode] +} + +@OptIn(UnstableApi::class) +private fun trimVideo(videoUri: Uri) { + // [START android_media_editing_trim_video] + val clippingConfiguration = MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(10_000) // start at 10 seconds + .setEndPositionMs(20_000) // end at 20 seconds + .build() + val mediaItem = MediaItem.Builder() + .setUri(videoUri) + .setClippingConfiguration(clippingConfiguration) + .build() + // [END android_media_editing_trim_video] +} + +@OptIn(UnstableApi::class) +private fun builtInEffects(mediaItem: MediaItem) { + // [START android_media_editing_built_in_effects] + val channelMixingProcessor = ChannelMixingAudioProcessor() + val rotateEffect = ScaleAndRotateTransformation.Builder().setRotationDegrees(60f).build() + val cropEffect = Crop(-0.5f, 0.5f, -0.5f, 0.5f) + val effects = Effects(listOf(channelMixingProcessor), listOf(rotateEffect, cropEffect)) + + val editedMediaItem = EditedMediaItem.Builder(mediaItem) + .setEffects(effects) + .build() + // [END android_media_editing_built_in_effects] +} + +@OptIn(UnstableApi::class) +private fun customEffects(inputMediaItem: MediaItem) { + // [START android_media_editing_custom_effects] + val zoomEffect = MatrixTransformation { presentationTimeUs -> + val transformationMatrix = Matrix() + // Set the scaling factor based on the playback position + val scale = min(1f, presentationTimeUs / 1_000f) + transformationMatrix.postScale(/* x */ scale, /* y */ scale) + transformationMatrix + } + + val editedMediaItem = EditedMediaItem.Builder(inputMediaItem) + .setEffects(Effects(listOf(), listOf(zoomEffect))) + .build() + // [END android_media_editing_custom_effects] +} + +@OptIn(UnstableApi::class) +private fun previewEffects(context: Context, inputMediaItem: MediaItem, zoomEffect: MatrixTransformation) { + // [START android_media_editing_preview_effects] + val player = ExoPlayer.Builder(context) + .build() + .also { exoPlayer -> + exoPlayer.setMediaItem(inputMediaItem) + exoPlayer.setVideoEffects(listOf(zoomEffect)) + exoPlayer.prepare() + } + // [END android_media_editing_preview_effects] +} + +@OptIn(UnstableApi::class) +private fun previewAudioEffects(context: Context, channelMixingProcessor: ChannelMixingAudioProcessor) { + // [START android_media_editing_preview_audio_effects] + val player = ExoPlayer.Builder(context, object : DefaultRenderersFactory(context) { + override fun buildAudioSink( + context: Context, + enableFloatOutput: Boolean, + enableAudioTrackPlaybackParams: Boolean, + // [START_EXCLUDE silent] + /* + // [END_EXCLUDE] + enableOffload: Boolean + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + ): AudioSink? { + return DefaultAudioSink.Builder(context) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + // [START_EXCLUDE silent] + /* + // [END_EXCLUDE] + .setOffloadMode(if (enableOffload) { + DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED + } else { + DefaultAudioSink.OFFLOAD_MODE_DISABLED + }) + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + .setAudioProcessors(arrayOf(channelMixingProcessor)) + .build() + } + }).build() + // [END android_media_editing_preview_audio_effects] +} + +@OptIn(UnstableApi::class) +private fun startTransformation( + context: Context, + editedMediaItem: EditedMediaItem, + outputPath: String, + listener: Transformer.Listener +) { + // [START android_media_editing_start_transformation] + val transformer = Transformer.Builder(context) + .addListener(listener) + .build() + transformer.start(editedMediaItem, outputPath) + // [END android_media_editing_start_transformation] +} diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt index 827eb6720..52fdd946a 100644 --- a/media/src/main/java/com/example/media/PlaybackApp.kt +++ b/media/src/main/java/com/example/media/PlaybackApp.kt @@ -16,11 +16,71 @@ package com.example.media +import android.app.Activity +import android.content.ComponentName import android.content.Context import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaController +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSessionService +import androidx.media3.session.SessionToken +import androidx.media3.ui.PlayerView +import com.google.common.util.concurrent.MoreExecutors private fun createExoPlayer(context: Context) { // [START android_media_playback_app_create_exoplayer] val player = ExoPlayer.Builder(context).build() // [END android_media_playback_app_create_exoplayer] } + +private fun createMediaSession(context: Context) { + // [START android_media_playback_app_create_media_session] + val player = ExoPlayer.Builder(context).build() + val mediaSession = MediaSession.Builder(context, player).build() + // [END android_media_playback_app_create_media_session] +} + +// [START android_media_playback_app_playback_service] +class PlaybackService : MediaSessionService() { + private var mediaSession: MediaSession? = null + + override fun onCreate() { + super.onCreate() + val player = ExoPlayer.Builder(this).build() + mediaSession = MediaSession.Builder(this, player).build() + } + + override fun onDestroy() { + mediaSession?.run { + player.release() + release() + mediaSession = null + } + super.onDestroy() + } + + // [START android_media_playback_app_on_get_session] + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession + } + // [END android_media_playback_app_on_get_session] +} +// [END android_media_playback_app_playback_service] + +private class PlayerActivity : Activity() { + private lateinit var playerView: PlayerView + + // [START android_media_playback_app_connect_ui] + override fun onStart() { + super.onStart() + val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) + val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() + controllerFuture.addListener({ + // Call controllerFuture.get() to retrieve the MediaController. + // MediaController implements the Player interface, so it can be + // attached to the PlayerView UI component. + playerView.player = controllerFuture.get() + }, MoreExecutors.directExecutor()) + } + // [END android_media_playback_app_connect_ui] +} diff --git a/media/src/main/java/com/example/media/Sharing.kt b/media/src/main/java/com/example/media/Sharing.kt new file mode 100644 index 000000000..167b2fcc9 --- /dev/null +++ b/media/src/main/java/com/example/media/Sharing.kt @@ -0,0 +1,41 @@ +/* + * 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.media + +import android.content.Context +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.transformer.Transformer + +@OptIn(UnstableApi::class) +private fun setupHdrToSdrTransformer(context: Context) { + // [START android_media_sharing_hdr_to_sdr] + val transformer = Transformer.Builder(context) + // [START_EXCLUDE silent] + /* + // [END_EXCLUDE] + .setTransformationRequest( + TransformationRequest.Builder() + .setHdrMode(TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR) + .build()) + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + .addListener(object : Transformer.Listener {}) + .build() + // [END android_media_sharing_hdr_to_sdr] +} diff --git a/media/src/main/java/com/example/media/SurfacesAndroidTv.kt b/media/src/main/java/com/example/media/SurfacesAndroidTv.kt new file mode 100644 index 000000000..95ebe3b3c --- /dev/null +++ b/media/src/main/java/com/example/media/SurfacesAndroidTv.kt @@ -0,0 +1,28 @@ +/* + * 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.media + +import android.content.Context +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.MediaSession + +private fun setupTvMediaSession(context: Context) { + // [START android_media_surfaces_android_tv_media_session] + val player = ExoPlayer.Builder(context).build() + val mediaSession = MediaSession.Builder(context, player).build() + // [END android_media_surfaces_android_tv_media_session] +} diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt new file mode 100644 index 000000000..afcb4331a --- /dev/null +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -0,0 +1,278 @@ +/* + * 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.media + +import android.app.PendingIntent +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.session.PlaybackStateCompat +import android.util.Log +import androidx.annotation.OptIn +import androidx.core.app.NotificationCompat +import androidx.media.MediaBrowserServiceCompat +import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT +import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV +import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.CommandButton +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaSession.ConnectionResult +import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder +import androidx.media3.session.MediaSessionService +import androidx.media3.session.MediaStyleNotificationHelper +import androidx.media3.session.SessionCommand +import androidx.media3.session.SessionResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture + +private const val ACTION_FAVORITES = "ACTION_FAVORITES" +private const val CUSTOM_ACTION_1 = "CUSTOM_ACTION_1" +private const val CUSTOM_ACTION_2 = "CUSTOM_ACTION_2" +private const val TAG = "SurfacesMobile" +private const val MY_RECENTS_ROOT_ID = "MY_RECENTS_ROOT_ID" +private const val MY_MEDIA_ROOT_ID = "MY_MEDIA_ROOT_ID" +private const val MY_EMPTY_ROOT_ID = "MY_EMPTY_ROOT_ID" +private const val CHANNEL_ID = "playback_channel" + +@OptIn(UnstableApi::class) +// [START android_media_surfaces_mobile_custom_command_buttons] +class CustomControlsPlaybackService : MediaSessionService() { + private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) + private var mediaSession: MediaSession? = null + + override fun onCreate() { + super.onCreate() + val favoriteButton = + CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) + .setDisplayName("Save to favorites") + .setSessionCommand(customCommandFavorites) + .build() + val player = ExoPlayer.Builder(this).build() + // Build the session with a custom layout. + mediaSession = + MediaSession.Builder(this, player) + .setCallback(MyCallback()) + .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) + .build() + } + + private inner class MyCallback : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult { + // Set available player and session commands. + return AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .add(customCommandFavorites) + .build() + ) + .build() + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == ACTION_FAVORITES) { + // Do custom logic here + saveToFavorites(session.player.currentMediaItem) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + return super.onCustomCommand(session, controller, customCommand, args) + } + } + + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession + } + + private fun saveToFavorites(item: MediaItem?) {} +} +// [END android_media_surfaces_mobile_custom_command_buttons] + +private fun addStandardActions(context: Context, notificationBuilder: NotificationCompat.Builder) { + // [START android_media_surfaces_mobile_add_standard_actions] + val session = MediaSessionCompat(context, TAG) + val playbackStateBuilder = PlaybackStateCompat.Builder() + val style = androidx.media.app.NotificationCompat.MediaStyle() + + // For this example, the media is currently paused: + val state = PlaybackStateCompat.STATE_PAUSED + val position = 0L + val playbackSpeed = 1f + playbackStateBuilder.setState(state, position, playbackSpeed) + + // And the user can play, skip to next or previous, and seek + val stateActions = (PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + or PlaybackStateCompat.ACTION_SKIP_TO_NEXT + or PlaybackStateCompat.ACTION_SEEK_TO) // adding the seek action enables seeking with the seekbar + playbackStateBuilder.setActions(stateActions) + + // ... do more setup here ... + + session.setPlaybackState(playbackStateBuilder.build()) + style.setMediaSession(session.sessionToken) + notificationBuilder.setStyle(style) + // [END android_media_surfaces_mobile_add_standard_actions] +} + +private fun slotReservationExtras(session: MediaSessionCompat) { + // [START android_media_surfaces_mobile_slot_reservation_extras] + session.setExtras(Bundle().apply { + putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + }) + // [END android_media_surfaces_mobile_slot_reservation_extras] +} + +private fun addCustomActions(playbackStateBuilder: PlaybackStateCompat.Builder) { + // [START android_media_surfaces_mobile_add_custom_actions] + val customAction = PlaybackStateCompat.CustomAction.Builder( + "com.example.MY_CUSTOM_ACTION", // action ID + "Custom Action", // title - used as content description for the button + android.R.drawable.ic_media_play + ).build() + + playbackStateBuilder.addCustomAction(customAction) + // [END android_media_surfaces_mobile_add_custom_actions] +} + +private fun playbackStateCallback(session: MediaSessionCompat) { + // [START android_media_surfaces_mobile_playback_state_callback] + val callback = object: MediaSessionCompat.Callback() { + override fun onPlay() { + // start playback + } + + override fun onPause() { + // pause playback + } + + override fun onSkipToPrevious() { + // skip to previous + } + + override fun onSkipToNext() { + // skip to next + } + + override fun onSeekTo(pos: Long) { + // jump to position in track + } + + override fun onCustomAction(action: String?, extras: Bundle?) { + when (action) { + CUSTOM_ACTION_1 -> doCustomAction1(extras) + CUSTOM_ACTION_2 -> doCustomAction2(extras) + else -> { + Log.w(TAG, "Unknown custom action $action") + } + } + } + + } + + session.setCallback(callback) + // [END android_media_surfaces_mobile_playback_state_callback] +} + +private fun doCustomAction1(extras: Bundle?) {} +private fun doCustomAction2(extras: Bundle?) {} + +private class MobileResumptionBrowserService : MediaBrowserServiceCompat() { + // [START android_media_surfaces_mobile_on_get_root] + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? { + // [START_EXCLUDE silent] + /* + // [END_EXCLUDE] + ... + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + // Verify that the specified package is SystemUI. You'll need to write your + // own logic to do this. + if (isSystem(clientPackageName, clientUid)) { + rootHints?.let { + if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) { + // Return a tree with a single playable media item for resumption. + val extras = Bundle().apply { + putBoolean(BrowserRoot.EXTRA_RECENT, true) + } + return BrowserRoot(MY_RECENTS_ROOT_ID, extras) + } + } + // You can return your normal tree if the EXTRA_RECENT flag is not present. + return BrowserRoot(MY_MEDIA_ROOT_ID, null) + } + // Return an empty tree to disallow browsing. + return BrowserRoot(MY_EMPTY_ROOT_ID, null) + } + // [END android_media_surfaces_mobile_on_get_root] + + private fun isSystem(clientPackageName: String, clientUid: Int): Boolean = true + + override fun onLoadChildren( + parentId: String, + result: MediaBrowserServiceCompat.Result> + ) { + result.sendResult(null) + } +} + +@OptIn(UnstableApi::class) +private fun preAndroid13Notification( + context: Context, + mediaSession: MediaSession, + prevPendingIntent: PendingIntent, + pausePendingIntent: PendingIntent, + nextPendingIntent: PendingIntent, + albumArtBitmap: Bitmap +) { + // [START android_media_surfaces_mobile_pre_android_13_notification] + var notification = NotificationCompat.Builder(context, CHANNEL_ID) + // Show controls on lock screen even when user hides sensitive content. + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setSmallIcon(android.R.drawable.ic_media_play) + // Add media control buttons that invoke intents in your media service + .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 + .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 + .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 + // Apply the media style template + .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) + .setShowActionsInCompactView(1 /* #1: pause button */)) + .setContentTitle("Wonderful music") + .setContentText("My Awesome Band") + .setLargeIcon(albumArtBitmap) + .build() + // [END android_media_surfaces_mobile_pre_android_13_notification] +} From 375a755acfd9a084e17180b0cd2cfd719f6f4c1d Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Mon, 14 Sep 2026 12:52:12 -0700 Subject: [PATCH 02/10] migrate hardcoded media snippets --- .../main/java/com/example/media/Assistant.kt | 5 +- .../main/java/com/example/media/AudioFocus.kt | 128 ++++++------ .../main/java/com/example/media/EditingApp.kt | 50 ++--- .../java/com/example/media/PlaybackApp.kt | 15 +- .../main/java/com/example/media/Sharing.kt | 14 +- .../java/com/example/media/SurfacesMobile.kt | 190 ++++++++++-------- 6 files changed, 209 insertions(+), 193 deletions(-) diff --git a/media/src/main/java/com/example/media/Assistant.kt b/media/src/main/java/com/example/media/Assistant.kt index 9581660e5..bfa4f8ef4 100644 --- a/media/src/main/java/com/example/media/Assistant.kt +++ b/media/src/main/java/com/example/media/Assistant.kt @@ -112,8 +112,9 @@ private class MyMediaBrowserService : MediaBrowserServiceCompat() { // If you return null, then the media browser will not be able to connect and // no further calls will be made to other media browsing methods. Log.i( - TAG, "OnGetRoot: Browsing NOT ALLOWED for unknown caller. Returning empty " - + "browser root so all apps can use MediaController. $clientPackageName" + TAG, + "OnGetRoot: Browsing NOT ALLOWED for unknown caller. Returning empty " + + "browser root so all apps can use MediaController. $clientPackageName" ) return MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null) } diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt index 77ae1f5d6..91158f56b 100644 --- a/media/src/main/java/com/example/media/AudioFocus.kt +++ b/media/src/main/java/com/example/media/AudioFocus.kt @@ -17,6 +17,7 @@ package com.example.media import android.content.Context +import android.content.ContextWrapper import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager @@ -25,28 +26,33 @@ import android.os.Looper import android.support.v4.media.session.MediaControllerCompat import java.util.concurrent.TimeUnit -private class AudioFocusManager(private val context: Context, private val mediaController: MediaControllerCompat) { +private class AudioFocusManager( + context: Context +) : ContextWrapper(context), AudioManager.OnAudioFocusChangeListener { private lateinit var audioManager: AudioManager private lateinit var focusRequest: AudioFocusRequest private val handler = Handler(Looper.getMainLooper()) - private var delayedStopRunnable = Runnable { - mediaController.transportControls.stop() - } private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> onAudioFocusChange(focusChange) } + private var playbackDelayed = false + private var resumeOnFocusGain = false + private val focusLock = Any() + private fun requestFocusPostOreo() { // [START android_media_audio_focus_request] // initializing variables for audio focus and playback management - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { - setAudioAttributes(AudioAttributes.Builder().run { - setUsage(AudioAttributes.USAGE_GAME) - setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - build() - }) + setAudioAttributes( + AudioAttributes.Builder().run { + setUsage(AudioAttributes.USAGE_GAME) + setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) + build() + } + ) setAcceptsDelayedFocusGain(true) setOnAudioFocusChangeListener(afChangeListener, handler) build() @@ -72,16 +78,12 @@ private class AudioFocusManager(private val context: Context, private val mediaC else -> false } } - // [END android_media_audio_focus_request] - } - private var playbackDelayed = false - private var resumeOnFocusGain = false - private val focusLock = Any() - - private fun onAudioFocusChange(focusChange: Int) { - // [START android_media_audio_focus_request_listener_impl] // implementing OnAudioFocusChangeListener to react to focus changes + // [START_EXCLUDE silent] + } + // [END_EXCLUDE] + override fun onAudioFocusChange(focusChange: Int) { when (focusChange) { AudioManager.AUDIOFOCUS_GAIN -> if (playbackDelayed || resumeOnFocusGain) { @@ -110,29 +112,22 @@ private class AudioFocusManager(private val context: Context, private val mediaC // ... pausing or ducking depends on your app } } - // [END android_media_audio_focus_request_listener_impl] } + // [END android_media_audio_focus_request] - @Suppress("DEPRECATION") private fun requestFocusPreOreo() { // [START android_media_audio_focus_request_pre_o] - audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager lateinit var afChangeListener: AudioManager.OnAudioFocusChangeListener - // [START_EXCLUDE silent] - /* - // [END_EXCLUDE] - ... - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] + // ... // Request audio focus for playback val result: Int = audioManager.requestAudioFocus( - afChangeListener, - // Use the music stream. - AudioManager.STREAM_MUSIC, - // Request permanent focus. - AudioManager.AUDIOFOCUS_GAIN + afChangeListener, + // Use the music stream. + AudioManager.STREAM_MUSIC, + // Request permanent focus. + AudioManager.AUDIOFOCUS_GAIN ) if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { @@ -141,47 +136,48 @@ private class AudioFocusManager(private val context: Context, private val mediaC // [END android_media_audio_focus_request_pre_o] } - @Suppress("DEPRECATION") private fun abandonFocusExample(afChangeListener: AudioManager.OnAudioFocusChangeListener) { // [START android_media_audio_focus_abandon] audioManager.abandonAudioFocus(afChangeListener) // [END android_media_audio_focus_abandon] } - private fun setupHandlerChangeListener() { - // [START android_media_audio_focus_change_listener] - val handler = Handler() - val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - when (focusChange) { - AudioManager.AUDIOFOCUS_LOSS -> { - // Permanent loss of audio focus - // Pause playback immediately - mediaController.transportControls.pause() - // Wait 30 seconds before stopping playback - handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - // Pause playback - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - // Lower the volume, keep playing - } - AudioManager.AUDIOFOCUS_GAIN -> { - // Your app has been granted audio focus again - // Raise volume to normal, restart playback if necessary - } - } - } - // [END android_media_audio_focus_change_listener] + private fun playbackNow() {} + private fun pausePlayback() {} + private fun isPlaying(): Boolean = true +} - // [START android_media_audio_focus_delayed_stop_runnable] - var delayedStopRunnable = Runnable { - mediaController.transportControls.stop() +private class AudioFocusLegacyChangeHandler( + private val mediaController: MediaControllerCompat +) { + // [START android_media_audio_focus_change_listener] + private val handler = Handler() + private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> + when (focusChange) { + AudioManager.AUDIOFOCUS_LOSS -> { + // Permanent loss of audio focus + // Pause playback immediately + mediaController.transportControls.pause() + // Wait 30 seconds before stopping playback + handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + // Pause playback + } + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // Lower the volume, keep playing + } + AudioManager.AUDIOFOCUS_GAIN -> { + // Your app has been granted audio focus again + // Raise volume to normal, restart playback if necessary + } } - // [END android_media_audio_focus_delayed_stop_runnable] } + // [END android_media_audio_focus_change_listener] - private fun playbackNow() {} - private fun pausePlayback() {} - private fun isPlaying(): Boolean = true + // [START android_media_audio_focus_delayed_stop_runnable] + private var delayedStopRunnable = Runnable { + mediaController.transportControls.stop() + } + // [END android_media_audio_focus_delayed_stop_runnable] } diff --git a/media/src/main/java/com/example/media/EditingApp.kt b/media/src/main/java/com/example/media/EditingApp.kt index 614a9d4c5..85e3e3080 100644 --- a/media/src/main/java/com/example/media/EditingApp.kt +++ b/media/src/main/java/com/example/media/EditingApp.kt @@ -34,8 +34,8 @@ import androidx.media3.exoplayer.audio.DefaultAudioSink import androidx.media3.transformer.Composition import androidx.media3.transformer.Composition.HDR_MODE_KEEP_HDR import androidx.media3.transformer.EditedMediaItem -import androidx.media3.transformer.Effects import androidx.media3.transformer.EditedMediaItemSequence +import androidx.media3.transformer.Effects import androidx.media3.transformer.Transformer import com.google.common.collect.ImmutableList import kotlin.math.min @@ -54,7 +54,8 @@ private fun configureTranscode(context: Context) { private fun setHdrMode(videoSequence: EditedMediaItemSequence) { // [START android_media_editing_set_hdr_mode] val composition = Composition.Builder( - ImmutableList.of(videoSequence)) + ImmutableList.of(videoSequence) + ) .setHdrMode(HDR_MODE_KEEP_HDR) .build() // [END android_media_editing_set_hdr_mode] @@ -121,37 +122,22 @@ private fun previewEffects(context: Context, inputMediaItem: MediaItem, zoomEffe @OptIn(UnstableApi::class) private fun previewAudioEffects(context: Context, channelMixingProcessor: ChannelMixingAudioProcessor) { // [START android_media_editing_preview_audio_effects] - val player = ExoPlayer.Builder(context, object : DefaultRenderersFactory(context) { - override fun buildAudioSink( - context: Context, - enableFloatOutput: Boolean, - enableAudioTrackPlaybackParams: Boolean, - // [START_EXCLUDE silent] - /* - // [END_EXCLUDE] - enableOffload: Boolean - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] - ): AudioSink? { - return DefaultAudioSink.Builder(context) - .setEnableFloatOutput(enableFloatOutput) - .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) - // [START_EXCLUDE silent] - /* - // [END_EXCLUDE] - .setOffloadMode(if (enableOffload) { - DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED - } else { - DefaultAudioSink.OFFLOAD_MODE_DISABLED - }) - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] - .setAudioProcessors(arrayOf(channelMixingProcessor)) - .build() + val player = ExoPlayer.Builder( + context, + object : DefaultRenderersFactory(context) { + override fun buildAudioSink( + context: Context, + enableFloatOutput: Boolean, + enableAudioTrackPlaybackParams: Boolean + ): AudioSink? { + return DefaultAudioSink.Builder(context) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setAudioProcessors(arrayOf(channelMixingProcessor)) + .build() } - }).build() + } + ).build() // [END android_media_editing_preview_audio_effects] } diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt index 52fdd946a..ec28bbc90 100644 --- a/media/src/main/java/com/example/media/PlaybackApp.kt +++ b/media/src/main/java/com/example/media/PlaybackApp.kt @@ -44,12 +44,14 @@ private fun createMediaSession(context: Context) { class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null + // Create your Player and MediaSession in the onCreate lifecycle event override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } + // Remember to release the player and media session in onDestroy override fun onDestroy() { mediaSession?.run { player.release() @@ -59,11 +61,14 @@ class PlaybackService : MediaSessionService() { super.onDestroy() } + // [START_EXCLUDE silent] // [START android_media_playback_app_on_get_session] - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return mediaSession - } + // This example always accepts the connection request + override fun onGetSession( + controllerInfo: MediaSession.ControllerInfo + ): MediaSession? = mediaSession // [END android_media_playback_app_on_get_session] + // [END_EXCLUDE] } // [END android_media_playback_app_playback_service] @@ -72,14 +77,16 @@ private class PlayerActivity : Activity() { // [START android_media_playback_app_connect_ui] override fun onStart() { + // [START_EXCLUDE silent] super.onStart() + // [END_EXCLUDE] val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() controllerFuture.addListener({ // Call controllerFuture.get() to retrieve the MediaController. // MediaController implements the Player interface, so it can be // attached to the PlayerView UI component. - playerView.player = controllerFuture.get() + playerView.setPlayer(controllerFuture.get()) }, MoreExecutors.directExecutor()) } // [END android_media_playback_app_connect_ui] diff --git a/media/src/main/java/com/example/media/Sharing.kt b/media/src/main/java/com/example/media/Sharing.kt index 167b2fcc9..2d79b6feb 100644 --- a/media/src/main/java/com/example/media/Sharing.kt +++ b/media/src/main/java/com/example/media/Sharing.kt @@ -22,7 +22,10 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.transformer.Transformer @OptIn(UnstableApi::class) -private fun setupHdrToSdrTransformer(context: Context) { +private fun setupHdrToSdrTransformer( + context: Context, + listener: Transformer.Listener +) { // [START android_media_sharing_hdr_to_sdr] val transformer = Transformer.Builder(context) // [START_EXCLUDE silent] @@ -33,9 +36,14 @@ private fun setupHdrToSdrTransformer(context: Context) { .setHdrMode(TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR) .build()) // [START_EXCLUDE silent] - */ + */ // [END_EXCLUDE] - .addListener(object : Transformer.Listener {}) + .addListener( + // [START_EXCLUDE silent] + listener + // [END_EXCLUDE] + /* ... */ + ) .build() // [END android_media_sharing_hdr_to_sdr] } diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt index afcb4331a..0b8462040 100644 --- a/media/src/main/java/com/example/media/SurfacesMobile.kt +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -20,14 +20,14 @@ import android.app.PendingIntent import android.content.Context import android.graphics.Bitmap import android.os.Bundle -import android.os.Handler import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.annotation.OptIn -import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationCompat as CoreNotificationCompat import androidx.media.MediaBrowserServiceCompat +import androidx.media.app.NotificationCompat import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV import androidx.media3.common.MediaItem @@ -57,68 +57,70 @@ private const val CHANNEL_ID = "playback_channel" @OptIn(UnstableApi::class) // [START android_media_surfaces_mobile_custom_command_buttons] class CustomControlsPlaybackService : MediaSessionService() { - private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) - private var mediaSession: MediaSession? = null + private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) + private var mediaSession: MediaSession? = null - override fun onCreate() { - super.onCreate() - val favoriteButton = - CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) - .setDisplayName("Save to favorites") - .setSessionCommand(customCommandFavorites) - .build() - val player = ExoPlayer.Builder(this).build() - // Build the session with a custom layout. - mediaSession = - MediaSession.Builder(this, player) - .setCallback(MyCallback()) - .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) - .build() - } - - private inner class MyCallback : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): ConnectionResult { - // Set available player and session commands. - return AcceptedResultBuilder(session) - .setAvailableSessionCommands( - ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - .add(customCommandFavorites) - .build() - ) - .build() + override fun onCreate() { + super.onCreate() + val favoriteButton = + CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) + .setDisplayName("Save to favorites") + .setSessionCommand(customCommandFavorites) + .build() + val player = ExoPlayer.Builder(this).build() + // Build the session with a custom layout. + mediaSession = + MediaSession.Builder(this, player) + .setCallback(MyCallback()) + .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) + .build() } - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - if (customCommand.customAction == ACTION_FAVORITES) { - // Do custom logic here - saveToFavorites(session.player.currentMediaItem) - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) - } - return super.onCustomCommand(session, controller, customCommand, args) + private inner class MyCallback : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult { + // Set available player and session commands. + return AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .add(customCommandFavorites) + .build() + ) + .build() + } + + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == ACTION_FAVORITES) { + // Do custom logic here + saveToFavorites(session.player.currentMediaItem) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + } + return super.onCustomCommand(session, controller, customCommand, args) + } } - } - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return mediaSession - } + // [START_EXCLUDE silent] + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession + } - private fun saveToFavorites(item: MediaItem?) {} + private fun saveToFavorites(item: MediaItem?) {} + // [END_EXCLUDE] } // [END android_media_surfaces_mobile_custom_command_buttons] -private fun addStandardActions(context: Context, notificationBuilder: NotificationCompat.Builder) { +private fun addStandardActions(context: Context, notificationBuilder: CoreNotificationCompat.Builder) { // [START android_media_surfaces_mobile_add_standard_actions] val session = MediaSessionCompat(context, TAG) val playbackStateBuilder = PlaybackStateCompat.Builder() - val style = androidx.media.app.NotificationCompat.MediaStyle() + val style = NotificationCompat.MediaStyle() // For this example, the media is currently paused: val state = PlaybackStateCompat.STATE_PAUSED @@ -127,11 +129,13 @@ private fun addStandardActions(context: Context, notificationBuilder: Notificati playbackStateBuilder.setState(state, position, playbackSpeed) // And the user can play, skip to next or previous, and seek - val stateActions = (PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PLAY_PAUSE - or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - or PlaybackStateCompat.ACTION_SKIP_TO_NEXT - or PlaybackStateCompat.ACTION_SEEK_TO) // adding the seek action enables seeking with the seekbar + val stateActions = ( + PlaybackStateCompat.ACTION_PLAY + or PlaybackStateCompat.ACTION_PLAY_PAUSE + or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + or PlaybackStateCompat.ACTION_SKIP_TO_NEXT + or PlaybackStateCompat.ACTION_SEEK_TO + ) // adding the seek action enables seeking with the seekbar playbackStateBuilder.setActions(stateActions) // ... do more setup here ... @@ -144,10 +148,12 @@ private fun addStandardActions(context: Context, notificationBuilder: Notificati private fun slotReservationExtras(session: MediaSessionCompat) { // [START android_media_surfaces_mobile_slot_reservation_extras] - session.setExtras(Bundle().apply { - putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) - putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) - }) + session.setExtras( + Bundle().apply { + putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + } + ) // [END android_media_surfaces_mobile_slot_reservation_extras] } @@ -156,7 +162,14 @@ private fun addCustomActions(playbackStateBuilder: PlaybackStateCompat.Builder) val customAction = PlaybackStateCompat.CustomAction.Builder( "com.example.MY_CUSTOM_ACTION", // action ID "Custom Action", // title - used as content description for the button + // [START_EXCLUDE silent] android.R.drawable.ic_media_play + /* + // [END_EXCLUDE] + R.drawable.ic_custom_action + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] ).build() playbackStateBuilder.addCustomAction(customAction) @@ -165,7 +178,7 @@ private fun addCustomActions(playbackStateBuilder: PlaybackStateCompat.Builder) private fun playbackStateCallback(session: MediaSessionCompat) { // [START android_media_surfaces_mobile_playback_state_callback] - val callback = object: MediaSessionCompat.Callback() { + val callback = object : MediaSessionCompat.Callback() { override fun onPlay() { // start playback } @@ -186,7 +199,7 @@ private fun playbackStateCallback(session: MediaSessionCompat) { // jump to position in track } - override fun onCustomAction(action: String?, extras: Bundle?) { + override fun onCustomAction(action: String, extras: Bundle?) { when (action) { CUSTOM_ACTION_1 -> doCustomAction1(extras) CUSTOM_ACTION_2 -> doCustomAction2(extras) @@ -195,7 +208,6 @@ private fun playbackStateCallback(session: MediaSessionCompat) { } } } - } session.setCallback(callback) @@ -212,13 +224,7 @@ private class MobileResumptionBrowserService : MediaBrowserServiceCompat() { clientUid: Int, rootHints: Bundle? ): BrowserRoot? { - // [START_EXCLUDE silent] - /* - // [END_EXCLUDE] - ... - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] + // ... // Verify that the specified package is SystemUI. You'll need to write your // own logic to do this. if (isSystem(clientPackageName, clientUid)) { @@ -259,20 +265,32 @@ private fun preAndroid13Notification( albumArtBitmap: Bitmap ) { // [START android_media_surfaces_mobile_pre_android_13_notification] - var notification = NotificationCompat.Builder(context, CHANNEL_ID) - // Show controls on lock screen even when user hides sensitive content. - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setSmallIcon(android.R.drawable.ic_media_play) - // Add media control buttons that invoke intents in your media service - .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 - .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 - .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 - // Apply the media style template - .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) - .setShowActionsInCompactView(1 /* #1: pause button */)) - .setContentTitle("Wonderful music") - .setContentText("My Awesome Band") - .setLargeIcon(albumArtBitmap) - .build() + var notification = CoreNotificationCompat.Builder(context, CHANNEL_ID) + // Show controls on lock screen even when user hides sensitive content. + .setVisibility(CoreNotificationCompat.VISIBILITY_PUBLIC) + // [START_EXCLUDE silent] + .setSmallIcon(android.R.drawable.ic_media_play) + .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 + .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 + .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 + /* + // [END_EXCLUDE] + .setSmallIcon(R.drawable.ic_stat_player) + // Add media control buttons that invoke intents in your media service + .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 + .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 + .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + // Apply the media style template + .setStyle( + MediaStyleNotificationHelper.MediaStyle(mediaSession) + .setShowActionsInCompactView(1 /* #1: pause button */) + ) + .setContentTitle("Wonderful music") + .setContentText("My Awesome Band") + .setLargeIcon(albumArtBitmap) + .build() // [END android_media_surfaces_mobile_pre_android_13_notification] } From 974ba73d0eb5846256a197d6041afbb42a7599fd Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Mon, 14 Sep 2026 13:07:39 -0700 Subject: [PATCH 03/10] add snippets: Media I --- .../java/com/example/media/SurfacesMobile.kt | 50 ++------------ .../com/example/media/SurfacesMobilePre13.kt | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 media/src/main/java/com/example/media/SurfacesMobilePre13.kt diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt index 0b8462040..d0ce5a5cc 100644 --- a/media/src/main/java/com/example/media/SurfacesMobile.kt +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -16,16 +16,13 @@ package com.example.media -import android.app.PendingIntent import android.content.Context -import android.graphics.Bitmap import android.os.Bundle import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import android.util.Log import androidx.annotation.OptIn -import androidx.core.app.NotificationCompat as CoreNotificationCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.app.NotificationCompat import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT @@ -38,7 +35,6 @@ import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.ConnectionResult import androidx.media3.session.MediaSession.ConnectionResult.AcceptedResultBuilder import androidx.media3.session.MediaSessionService -import androidx.media3.session.MediaStyleNotificationHelper import androidx.media3.session.SessionCommand import androidx.media3.session.SessionResult import com.google.common.collect.ImmutableList @@ -52,7 +48,6 @@ private const val TAG = "SurfacesMobile" private const val MY_RECENTS_ROOT_ID = "MY_RECENTS_ROOT_ID" private const val MY_MEDIA_ROOT_ID = "MY_MEDIA_ROOT_ID" private const val MY_EMPTY_ROOT_ID = "MY_EMPTY_ROOT_ID" -private const val CHANNEL_ID = "playback_channel" @OptIn(UnstableApi::class) // [START android_media_surfaces_mobile_custom_command_buttons] @@ -116,7 +111,10 @@ class CustomControlsPlaybackService : MediaSessionService() { } // [END android_media_surfaces_mobile_custom_command_buttons] -private fun addStandardActions(context: Context, notificationBuilder: CoreNotificationCompat.Builder) { +private fun addStandardActions( + context: Context, + notificationBuilder: androidx.core.app.NotificationCompat.Builder +) { // [START android_media_surfaces_mobile_add_standard_actions] val session = MediaSessionCompat(context, TAG) val playbackStateBuilder = PlaybackStateCompat.Builder() @@ -254,43 +252,3 @@ private class MobileResumptionBrowserService : MediaBrowserServiceCompat() { result.sendResult(null) } } - -@OptIn(UnstableApi::class) -private fun preAndroid13Notification( - context: Context, - mediaSession: MediaSession, - prevPendingIntent: PendingIntent, - pausePendingIntent: PendingIntent, - nextPendingIntent: PendingIntent, - albumArtBitmap: Bitmap -) { - // [START android_media_surfaces_mobile_pre_android_13_notification] - var notification = CoreNotificationCompat.Builder(context, CHANNEL_ID) - // Show controls on lock screen even when user hides sensitive content. - .setVisibility(CoreNotificationCompat.VISIBILITY_PUBLIC) - // [START_EXCLUDE silent] - .setSmallIcon(android.R.drawable.ic_media_play) - .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 - .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 - .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 - /* - // [END_EXCLUDE] - .setSmallIcon(R.drawable.ic_stat_player) - // Add media control buttons that invoke intents in your media service - .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 - .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 - .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] - // Apply the media style template - .setStyle( - MediaStyleNotificationHelper.MediaStyle(mediaSession) - .setShowActionsInCompactView(1 /* #1: pause button */) - ) - .setContentTitle("Wonderful music") - .setContentText("My Awesome Band") - .setLargeIcon(albumArtBitmap) - .build() - // [END android_media_surfaces_mobile_pre_android_13_notification] -} diff --git a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt new file mode 100644 index 000000000..3ecad2719 --- /dev/null +++ b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt @@ -0,0 +1,68 @@ +/* + * 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.media + +import android.app.PendingIntent +import android.content.Context +import android.graphics.Bitmap +import androidx.annotation.OptIn +import androidx.core.app.NotificationCompat +import androidx.media3.common.util.UnstableApi +import androidx.media3.session.MediaSession +import androidx.media3.session.MediaStyleNotificationHelper + +private const val CHANNEL_ID = "playback_channel" + +@OptIn(UnstableApi::class) +private fun preAndroid13Notification( + context: Context, + mediaSession: MediaSession, + prevPendingIntent: PendingIntent, + pausePendingIntent: PendingIntent, + nextPendingIntent: PendingIntent, + albumArtBitmap: Bitmap +) { + // [START android_media_surfaces_mobile_pre_android_13_notification] + var notification = NotificationCompat.Builder(context, CHANNEL_ID) + // Show controls on lock screen even when user hides sensitive content. + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + // [START_EXCLUDE silent] + .setSmallIcon(android.R.drawable.ic_media_play) + .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 + .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 + .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 + /* + // [END_EXCLUDE] + .setSmallIcon(R.drawable.ic_stat_player) + // Add media control buttons that invoke intents in your media service + .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 + .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 + .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 + // [START_EXCLUDE silent] + */ + // [END_EXCLUDE] + // Apply the media style template + .setStyle( + MediaStyleNotificationHelper.MediaStyle(mediaSession) + .setShowActionsInCompactView(1 /* #1: pause button */) + ) + .setContentTitle("Wonderful music") + .setContentText("My Awesome Band") + .setLargeIcon(albumArtBitmap) + .build() + // [END android_media_surfaces_mobile_pre_android_13_notification] +} From 7fad19c32bdf9ee251d80750226cf8151bc9c266 Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Thu, 17 Sep 2026 12:48:04 -0700 Subject: [PATCH 04/10] Fix Media I snippet tags and sharing HDR API --- .../java/com/example/media/PlaybackApp.kt | 19 +++++++++------- .../main/java/com/example/media/Sharing.kt | 22 ++++++++++--------- .../java/com/example/media/SurfacesMobile.kt | 2 +- .../com/example/media/SurfacesMobilePre13.kt | 10 +++++---- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt index ec28bbc90..5a6d297e5 100644 --- a/media/src/main/java/com/example/media/PlaybackApp.kt +++ b/media/src/main/java/com/example/media/PlaybackApp.kt @@ -61,14 +61,14 @@ class PlaybackService : MediaSessionService() { super.onDestroy() } - // [START_EXCLUDE silent] + // [END android_media_playback_app_playback_service] // [START android_media_playback_app_on_get_session] // This example always accepts the connection request override fun onGetSession( controllerInfo: MediaSession.ControllerInfo ): MediaSession? = mediaSession // [END android_media_playback_app_on_get_session] - // [END_EXCLUDE] + // [START android_media_playback_app_playback_service] } // [END android_media_playback_app_playback_service] @@ -82,12 +82,15 @@ private class PlayerActivity : Activity() { // [END_EXCLUDE] val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() - controllerFuture.addListener({ - // Call controllerFuture.get() to retrieve the MediaController. - // MediaController implements the Player interface, so it can be - // attached to the PlayerView UI component. - playerView.setPlayer(controllerFuture.get()) - }, MoreExecutors.directExecutor()) + controllerFuture.addListener( + { + // Call controllerFuture.get() to retrieve the MediaController. + // MediaController implements the Player interface, so it can be + // attached to the PlayerView UI component. + playerView.setPlayer(controllerFuture.get()) + }, + MoreExecutors.directExecutor() + ) } // [END android_media_playback_app_connect_ui] } diff --git a/media/src/main/java/com/example/media/Sharing.kt b/media/src/main/java/com/example/media/Sharing.kt index 2d79b6feb..003b1b5fd 100644 --- a/media/src/main/java/com/example/media/Sharing.kt +++ b/media/src/main/java/com/example/media/Sharing.kt @@ -19,11 +19,16 @@ package com.example.media import android.content.Context import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi +import androidx.media3.transformer.Composition +import androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL +import androidx.media3.transformer.EditedMediaItemSequence import androidx.media3.transformer.Transformer +import com.google.common.collect.ImmutableList @OptIn(UnstableApi::class) private fun setupHdrToSdrTransformer( context: Context, + videoSequence: EditedMediaItemSequence, listener: Transformer.Listener ) { // [START android_media_sharing_hdr_to_sdr] @@ -31,19 +36,16 @@ private fun setupHdrToSdrTransformer( // [START_EXCLUDE silent] /* // [END_EXCLUDE] - .setTransformationRequest( - TransformationRequest.Builder() - .setHdrMode(TransformationRequest.HDR_MODE_TONE_MAP_HDR_TO_SDR) - .build()) + .addListener(/* ... */) // [START_EXCLUDE silent] */ + .addListener(listener) // [END_EXCLUDE] - .addListener( - // [START_EXCLUDE silent] - listener - // [END_EXCLUDE] - /* ... */ - ) + .build() + val composition = Composition.Builder( + ImmutableList.of(videoSequence) + ) + .setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL) .build() // [END android_media_sharing_hdr_to_sdr] } diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt index d0ce5a5cc..66424f145 100644 --- a/media/src/main/java/com/example/media/SurfacesMobile.kt +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -100,8 +100,8 @@ class CustomControlsPlaybackService : MediaSessionService() { return super.onCustomCommand(session, controller, customCommand, args) } } - // [START_EXCLUDE silent] + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { return mediaSession } diff --git a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt index 3ecad2719..3dfa4f882 100644 --- a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt +++ b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt @@ -20,10 +20,14 @@ import android.app.PendingIntent import android.content.Context import android.graphics.Bitmap import androidx.annotation.OptIn +// [START android_media_surfaces_mobile_pre_android_13_notification] import androidx.core.app.NotificationCompat +// [END android_media_surfaces_mobile_pre_android_13_notification] import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaSession +// [START android_media_surfaces_mobile_pre_android_13_notification] import androidx.media3.session.MediaStyleNotificationHelper +// [END android_media_surfaces_mobile_pre_android_13_notification] private const val CHANNEL_ID = "playback_channel" @@ -56,10 +60,8 @@ private fun preAndroid13Notification( */ // [END_EXCLUDE] // Apply the media style template - .setStyle( - MediaStyleNotificationHelper.MediaStyle(mediaSession) - .setShowActionsInCompactView(1 /* #1: pause button */) - ) + .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) + .setShowActionsInCompactView(1 /* #1: pause button */)) .setContentTitle("Wonderful music") .setContentText("My Awesome Band") .setLargeIcon(albumArtBitmap) From b9a6ea261c9c77fc20a510dff3a9a992b522cb85 Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Fri, 18 Sep 2026 12:11:00 -0700 Subject: [PATCH 05/10] adjust indentation and spacing in visible regions --- .../main/java/com/example/media/AudioFocus.kt | 57 ++++++++++--------- .../com/example/media/SurfacesMobilePre13.kt | 1 + 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt index 91158f56b..1a8759d40 100644 --- a/media/src/main/java/com/example/media/AudioFocus.kt +++ b/media/src/main/java/com/example/media/AudioFocus.kt @@ -28,14 +28,12 @@ import java.util.concurrent.TimeUnit private class AudioFocusManager( context: Context -) : ContextWrapper(context), AudioManager.OnAudioFocusChangeListener { +) : ContextWrapper(context) { private lateinit var audioManager: AudioManager private lateinit var focusRequest: AudioFocusRequest private val handler = Handler(Looper.getMainLooper()) - private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> - onAudioFocusChange(focusChange) - } + private val afChangeListener = AudioFocusHandler() private var playbackDelayed = false private var resumeOnFocusGain = false @@ -80,40 +78,43 @@ private class AudioFocusManager( } // implementing OnAudioFocusChangeListener to react to focus changes - // [START_EXCLUDE silent] + // [END android_media_audio_focus_request] } - // [END_EXCLUDE] - override fun onAudioFocusChange(focusChange: Int) { - when (focusChange) { - AudioManager.AUDIOFOCUS_GAIN -> - if (playbackDelayed || resumeOnFocusGain) { + + private inner class AudioFocusHandler : AudioManager.OnAudioFocusChangeListener { + // [START android_media_audio_focus_request] + override fun onAudioFocusChange(focusChange: Int) { + when (focusChange) { + AudioManager.AUDIOFOCUS_GAIN -> + if (playbackDelayed || resumeOnFocusGain) { + synchronized(focusLock) { + playbackDelayed = false + resumeOnFocusGain = false + } + playbackNow() + } + AudioManager.AUDIOFOCUS_LOSS -> { synchronized(focusLock) { - playbackDelayed = false resumeOnFocusGain = false + playbackDelayed = false } - playbackNow() + pausePlayback() } - AudioManager.AUDIOFOCUS_LOSS -> { - synchronized(focusLock) { - resumeOnFocusGain = false - playbackDelayed = false + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { + synchronized(focusLock) { + // only resume if playback is being interrupted + resumeOnFocusGain = isPlaying() + playbackDelayed = false + } + pausePlayback() } - pausePlayback() - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - synchronized(focusLock) { - // only resume if playback is being interrupted - resumeOnFocusGain = isPlaying() - playbackDelayed = false + AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { + // ... pausing or ducking depends on your app } - pausePlayback() - } - AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - // ... pausing or ducking depends on your app } } + // [END android_media_audio_focus_request] } - // [END android_media_audio_focus_request] private fun requestFocusPreOreo() { // [START android_media_audio_focus_request_pre_o] diff --git a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt index 3dfa4f882..ebff8f8a9 100644 --- a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt +++ b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt @@ -27,6 +27,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaSession // [START android_media_surfaces_mobile_pre_android_13_notification] import androidx.media3.session.MediaStyleNotificationHelper + // [END android_media_surfaces_mobile_pre_android_13_notification] private const val CHANNEL_ID = "playback_channel" From caf1b1cc5f591f8b42d25dda7b3abbfc3af97a23 Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Fri, 18 Sep 2026 13:04:36 -0700 Subject: [PATCH 06/10] revert creation of Sharing.kt for further page rework --- .../main/java/com/example/media/Sharing.kt | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 media/src/main/java/com/example/media/Sharing.kt diff --git a/media/src/main/java/com/example/media/Sharing.kt b/media/src/main/java/com/example/media/Sharing.kt deleted file mode 100644 index 003b1b5fd..000000000 --- a/media/src/main/java/com/example/media/Sharing.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.media - -import android.content.Context -import androidx.annotation.OptIn -import androidx.media3.common.util.UnstableApi -import androidx.media3.transformer.Composition -import androidx.media3.transformer.Composition.HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL -import androidx.media3.transformer.EditedMediaItemSequence -import androidx.media3.transformer.Transformer -import com.google.common.collect.ImmutableList - -@OptIn(UnstableApi::class) -private fun setupHdrToSdrTransformer( - context: Context, - videoSequence: EditedMediaItemSequence, - listener: Transformer.Listener -) { - // [START android_media_sharing_hdr_to_sdr] - val transformer = Transformer.Builder(context) - // [START_EXCLUDE silent] - /* - // [END_EXCLUDE] - .addListener(/* ... */) - // [START_EXCLUDE silent] - */ - .addListener(listener) - // [END_EXCLUDE] - .build() - val composition = Composition.Builder( - ImmutableList.of(videoSequence) - ) - .setHdrMode(HDR_MODE_TONE_MAP_HDR_TO_SDR_USING_OPEN_GL) - .build() - // [END android_media_sharing_hdr_to_sdr] -} From 92f6feb6e31288c5a4b76e57aa0ad9b61713a61e Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Fri, 18 Sep 2026 13:17:43 -0700 Subject: [PATCH 07/10] change androidx-media to version ref vs hardcoded --- gradle/libs.versions.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed17f7dcf..25e7ee995 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ androidx-glance-appwidget = "1.3.0-alpha02" androidx-lifecycle-compose = "2.11.0" androidx-lifecycle-runtime-compose = "2.11.0" androidx-lifecycle-viewmodel-navigation3 = "2.11.0" +androidx-media = "1.7.0" androidx-navigation = "2.9.8" androidx-navigation3 = "1.2.0-beta01" androidx-paging = "3.5.0" @@ -224,7 +225,7 @@ androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmode androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-viewmodel-navigation3" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } -androidx-media = { module = "androidx.media:media", version = "1.7.0" } +androidx-media = { module = "androidx.media:media", version.ref = "androidx-media" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } From 705687364f2a4d30c9e676c13f6a091066e6339b Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Wed, 30 Sep 2026 21:01:09 +0000 Subject: [PATCH 08/10] Apply Media legacy assessment and address review feedback - Remove the Assistant, pre-Android 13 and legacy mobile surfaces snippets. Those pages and sections stay hardcoded on DAC as legacy. - Remove the androidx-media dependency. AudioFocus.kt now uses the framework MediaController in non-rendered code. - Keep the rendered class name PlaybackService by wrapping the custom command buttons snippet in a private object (review feedback). - Reuse playback-app's create_media_session region for the Android TV page instead of a duplicate region. - Open audio_focus_request and playback_service once each, using silent excludes. Rendered output is unchanged. - End natural-language comments inside regions with a full stop. --- gradle/libs.versions.toml | 2 - media/build.gradle.kts | 1 - .../main/java/com/example/media/Assistant.kt | 139 ---------- .../main/java/com/example/media/AudioFocus.kt | 36 +-- .../main/java/com/example/media/EditingApp.kt | 6 +- .../java/com/example/media/PlaybackApp.kt | 10 +- .../com/example/media/SurfacesAndroidTv.kt | 28 -- .../java/com/example/media/SurfacesMobile.kt | 258 ++++-------------- .../com/example/media/SurfacesMobilePre13.kt | 71 ----- 9 files changed, 78 insertions(+), 473 deletions(-) delete mode 100644 media/src/main/java/com/example/media/Assistant.kt delete mode 100644 media/src/main/java/com/example/media/SurfacesAndroidTv.kt delete mode 100644 media/src/main/java/com/example/media/SurfacesMobilePre13.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 25e7ee995..0366eb4c6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,7 +27,6 @@ androidx-glance-appwidget = "1.3.0-alpha02" androidx-lifecycle-compose = "2.11.0" androidx-lifecycle-runtime-compose = "2.11.0" androidx-lifecycle-viewmodel-navigation3 = "2.11.0" -androidx-media = "1.7.0" androidx-navigation = "2.9.8" androidx-navigation3 = "1.2.0-beta01" androidx-paging = "3.5.0" @@ -225,7 +224,6 @@ androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmode androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "androidx-lifecycle-viewmodel-navigation3" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } -androidx-media = { module = "androidx.media:media", version.ref = "androidx-media" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } androidx-media3-effect = { module = "androidx.media3:media3-effect", version.ref = "media3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } diff --git a/media/build.gradle.kts b/media/build.gradle.kts index 64d6f28c7..aa0c4a344 100644 --- a/media/build.gradle.kts +++ b/media/build.gradle.kts @@ -27,7 +27,6 @@ android { dependencies { implementation(libs.androidx.core.ktx) - implementation(libs.androidx.media) implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.effect) implementation(libs.androidx.media3.exoplayer) diff --git a/media/src/main/java/com/example/media/Assistant.kt b/media/src/main/java/com/example/media/Assistant.kt deleted file mode 100644 index bfa4f8ef4..000000000 --- a/media/src/main/java/com/example/media/Assistant.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.media - -import android.content.Context -import android.os.Bundle -import android.provider.MediaStore -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.session.MediaSessionCompat -import android.util.Log -import androidx.media.MediaBrowserServiceCompat - -private const val TAG = "AssistantSnippets" -private const val MEDIA_ID_EMPTY_ROOT = "@empty@" - -private fun setSessionFlags(session: MediaSessionCompat) { - // [START android_media_assistant_set_flags] - session.setFlags( - MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or - MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS - ) - // [END android_media_assistant_set_flags] -} - -private class SearchMediaSessionCallback : MediaSessionCompat.Callback() { - private var isArtistFocus = false - private var isAlbumFocus = false - private var artist: String? = null - private var album: String? = null - - // [START android_media_assistant_on_play_from_search] - override fun onPlayFromSearch(query: String?, extras: Bundle?) { - if (query.isNullOrEmpty()) { - // The user provided generic string e.g. 'Play music' - // Build appropriate playlist queue - } else { - // Build a queue based on songs that match "query" or "extras" param - val mediaFocus: String? = extras?.getString(MediaStore.EXTRA_MEDIA_FOCUS) - if (mediaFocus == MediaStore.Audio.Artists.ENTRY_CONTENT_TYPE) { - isArtistFocus = true - artist = extras.getString(MediaStore.EXTRA_MEDIA_ARTIST) - } else if (mediaFocus == MediaStore.Audio.Albums.ENTRY_CONTENT_TYPE) { - isAlbumFocus = true - album = extras.getString(MediaStore.EXTRA_MEDIA_ALBUM) - } - - // Implement additional "extras" param filtering - } - - // Implement your logic to retrieve the queue - var result: String? = when { - isArtistFocus -> artist?.also { - searchMusicByArtist(it) - } - isAlbumFocus -> album?.also { - searchMusicByAlbum(it) - } - else -> null - } - result = result ?: run { - // No focus found, search by query for song title - query?.also { - searchMusicBySongTitle(it) - } - } - - if (result?.isNotEmpty() == true) { - // Immediately start playing from the beginning of the search results - // Implement your logic to start playing music - playMusic(result) - } else { - // Handle no queue found. Stop playing if the app - // is currently playing a song - } - } - // [END android_media_assistant_on_play_from_search] - - private fun searchMusicByArtist(artist: String): String = artist - private fun searchMusicByAlbum(album: String): String = album - private fun searchMusicBySongTitle(title: String): String = title - private fun playMusic(track: String) {} -} - -private class MyMediaBrowserService : MediaBrowserServiceCompat() { - private val packageValidator = PackageValidator() - - // [START android_media_assistant_on_get_root] - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot? { - - // To ensure you are not allowing any arbitrary app to browse your app's contents, you - // need to check the origin: - if (!packageValidator.isCallerAllowed(this, clientPackageName, clientUid)) { - // If the request comes from an untrusted package, return an empty browser root. - // If you return null, then the media browser will not be able to connect and - // no further calls will be made to other media browsing methods. - Log.i( - TAG, - "OnGetRoot: Browsing NOT ALLOWED for unknown caller. Returning empty " + - "browser root so all apps can use MediaController. $clientPackageName" - ) - return MediaBrowserServiceCompat.BrowserRoot(MEDIA_ID_EMPTY_ROOT, null) - } - - // Return browser roots for browsing... - // [START_EXCLUDE silent] - return null - // [END_EXCLUDE] - } - // [END android_media_assistant_on_get_root] - - override fun onLoadChildren( - parentId: String, - result: MediaBrowserServiceCompat.Result> - ) { - result.sendResult(null) - } -} - -private class PackageValidator { - fun isCallerAllowed(service: Context, packageName: String, uid: Int): Boolean = true -} diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt index 1a8759d40..4b4ade5f4 100644 --- a/media/src/main/java/com/example/media/AudioFocus.kt +++ b/media/src/main/java/com/example/media/AudioFocus.kt @@ -21,9 +21,9 @@ import android.content.ContextWrapper import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager +import android.media.session.MediaController import android.os.Handler import android.os.Looper -import android.support.v4.media.session.MediaControllerCompat import java.util.concurrent.TimeUnit private class AudioFocusManager( @@ -41,7 +41,7 @@ private class AudioFocusManager( private fun requestFocusPostOreo() { // [START android_media_audio_focus_request] - // initializing variables for audio focus and playback management + // initializing variables for audio focus and playback management. audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager focusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run { setAudioAttributes( @@ -60,7 +60,7 @@ private class AudioFocusManager( var playbackDelayed = false var playbackNowAuthorized = false - // requesting audio focus and processing the response + // requesting audio focus and processing the response. val res = audioManager.requestAudioFocus(focusRequest) synchronized(focusLock) { playbackNowAuthorized = when (res) { @@ -77,12 +77,12 @@ private class AudioFocusManager( } } - // implementing OnAudioFocusChangeListener to react to focus changes - // [END android_media_audio_focus_request] + // implementing OnAudioFocusChangeListener to react to focus changes. + // [START_EXCLUDE silent] } private inner class AudioFocusHandler : AudioManager.OnAudioFocusChangeListener { - // [START android_media_audio_focus_request] + // [END_EXCLUDE] override fun onAudioFocusChange(focusChange: Int) { when (focusChange) { AudioManager.AUDIOFOCUS_GAIN -> @@ -102,14 +102,14 @@ private class AudioFocusManager( } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { synchronized(focusLock) { - // only resume if playback is being interrupted + // only resume if playback is being interrupted. resumeOnFocusGain = isPlaying() playbackDelayed = false } pausePlayback() } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - // ... pausing or ducking depends on your app + // ... pausing or ducking depends on your app. } } } @@ -122,7 +122,7 @@ private class AudioFocusManager( lateinit var afChangeListener: AudioManager.OnAudioFocusChangeListener // ... - // Request audio focus for playback + // Request audio focus for playback. val result: Int = audioManager.requestAudioFocus( afChangeListener, // Use the music stream. @@ -132,7 +132,7 @@ private class AudioFocusManager( ) if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - // Start playback + // Start playback. } // [END android_media_audio_focus_request_pre_o] } @@ -149,28 +149,28 @@ private class AudioFocusManager( } private class AudioFocusLegacyChangeHandler( - private val mediaController: MediaControllerCompat + private val mediaController: MediaController ) { // [START android_media_audio_focus_change_listener] private val handler = Handler() private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> { - // Permanent loss of audio focus - // Pause playback immediately + // Permanent loss of audio focus. + // Pause playback immediately. mediaController.transportControls.pause() - // Wait 30 seconds before stopping playback + // Wait 30 seconds before stopping playback. handler.postDelayed(delayedStopRunnable, TimeUnit.SECONDS.toMillis(30)) } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> { - // Pause playback + // Pause playback. } AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> { - // Lower the volume, keep playing + // Lower the volume, keep playing. } AudioManager.AUDIOFOCUS_GAIN -> { - // Your app has been granted audio focus again - // Raise volume to normal, restart playback if necessary + // Your app has been granted audio focus again. + // Raise volume to normal, restart playback if necessary. } } } diff --git a/media/src/main/java/com/example/media/EditingApp.kt b/media/src/main/java/com/example/media/EditingApp.kt index 85e3e3080..64124ac55 100644 --- a/media/src/main/java/com/example/media/EditingApp.kt +++ b/media/src/main/java/com/example/media/EditingApp.kt @@ -65,8 +65,8 @@ private fun setHdrMode(videoSequence: EditedMediaItemSequence) { private fun trimVideo(videoUri: Uri) { // [START android_media_editing_trim_video] val clippingConfiguration = MediaItem.ClippingConfiguration.Builder() - .setStartPositionMs(10_000) // start at 10 seconds - .setEndPositionMs(20_000) // end at 20 seconds + .setStartPositionMs(10_000) // start at 10 seconds. + .setEndPositionMs(20_000) // end at 20 seconds. .build() val mediaItem = MediaItem.Builder() .setUri(videoUri) @@ -94,7 +94,7 @@ private fun customEffects(inputMediaItem: MediaItem) { // [START android_media_editing_custom_effects] val zoomEffect = MatrixTransformation { presentationTimeUs -> val transformationMatrix = Matrix() - // Set the scaling factor based on the playback position + // Set the scaling factor based on the playback position. val scale = min(1f, presentationTimeUs / 1_000f) transformationMatrix.postScale(/* x */ scale, /* y */ scale) transformationMatrix diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt index 5a6d297e5..331e010d0 100644 --- a/media/src/main/java/com/example/media/PlaybackApp.kt +++ b/media/src/main/java/com/example/media/PlaybackApp.kt @@ -44,14 +44,14 @@ private fun createMediaSession(context: Context) { class PlaybackService : MediaSessionService() { private var mediaSession: MediaSession? = null - // Create your Player and MediaSession in the onCreate lifecycle event + // Create your Player and MediaSession in the onCreate lifecycle event. override fun onCreate() { super.onCreate() val player = ExoPlayer.Builder(this).build() mediaSession = MediaSession.Builder(this, player).build() } - // Remember to release the player and media session in onDestroy + // Remember to release the player and media session in onDestroy. override fun onDestroy() { mediaSession?.run { player.release() @@ -60,15 +60,15 @@ class PlaybackService : MediaSessionService() { } super.onDestroy() } + // [START_EXCLUDE silent] - // [END android_media_playback_app_playback_service] // [START android_media_playback_app_on_get_session] - // This example always accepts the connection request + // This example always accepts the connection request. override fun onGetSession( controllerInfo: MediaSession.ControllerInfo ): MediaSession? = mediaSession // [END android_media_playback_app_on_get_session] - // [START android_media_playback_app_playback_service] + // [END_EXCLUDE] } // [END android_media_playback_app_playback_service] diff --git a/media/src/main/java/com/example/media/SurfacesAndroidTv.kt b/media/src/main/java/com/example/media/SurfacesAndroidTv.kt deleted file mode 100644 index 95ebe3b3c..000000000 --- a/media/src/main/java/com/example/media/SurfacesAndroidTv.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.media - -import android.content.Context -import androidx.media3.exoplayer.ExoPlayer -import androidx.media3.session.MediaSession - -private fun setupTvMediaSession(context: Context) { - // [START android_media_surfaces_android_tv_media_session] - val player = ExoPlayer.Builder(context).build() - val mediaSession = MediaSession.Builder(context, player).build() - // [END android_media_surfaces_android_tv_media_session] -} diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt index 66424f145..46ceff989 100644 --- a/media/src/main/java/com/example/media/SurfacesMobile.kt +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -16,17 +16,8 @@ package com.example.media -import android.content.Context import android.os.Bundle -import android.support.v4.media.MediaBrowserCompat -import android.support.v4.media.session.MediaSessionCompat -import android.support.v4.media.session.PlaybackStateCompat -import android.util.Log import androidx.annotation.OptIn -import androidx.media.MediaBrowserServiceCompat -import androidx.media.app.NotificationCompat -import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT -import androidx.media.utils.MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer @@ -42,213 +33,68 @@ import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture private const val ACTION_FAVORITES = "ACTION_FAVORITES" -private const val CUSTOM_ACTION_1 = "CUSTOM_ACTION_1" -private const val CUSTOM_ACTION_2 = "CUSTOM_ACTION_2" -private const val TAG = "SurfacesMobile" -private const val MY_RECENTS_ROOT_ID = "MY_RECENTS_ROOT_ID" -private const val MY_MEDIA_ROOT_ID = "MY_MEDIA_ROOT_ID" -private const val MY_EMPTY_ROOT_ID = "MY_EMPTY_ROOT_ID" -@OptIn(UnstableApi::class) -// [START android_media_surfaces_mobile_custom_command_buttons] -class CustomControlsPlaybackService : MediaSessionService() { - private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) - private var mediaSession: MediaSession? = null - - override fun onCreate() { - super.onCreate() - val favoriteButton = - CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) - .setDisplayName("Save to favorites") - .setSessionCommand(customCommandFavorites) - .build() - val player = ExoPlayer.Builder(this).build() - // Build the session with a custom layout. - mediaSession = - MediaSession.Builder(this, player) - .setCallback(MyCallback()) - .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) - .build() - } - - private inner class MyCallback : MediaSession.Callback { - override fun onConnect( - session: MediaSession, - controller: MediaSession.ControllerInfo - ): ConnectionResult { - // Set available player and session commands. - return AcceptedResultBuilder(session) - .setAvailableSessionCommands( - ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() - .add(customCommandFavorites) - .build() - ) - .build() +// Wrapper avoids a collision with PlaybackService in PlaybackApp.kt. +private object CustomControlsSnippet { + @OptIn(UnstableApi::class) + // [START android_media_surfaces_mobile_custom_command_buttons] + class PlaybackService : MediaSessionService() { + private val customCommandFavorites = SessionCommand(ACTION_FAVORITES, Bundle.EMPTY) + private var mediaSession: MediaSession? = null + + override fun onCreate() { + super.onCreate() + val favoriteButton = + CommandButton.Builder(CommandButton.ICON_HEART_UNFILLED) + .setDisplayName("Save to favorites") + .setSessionCommand(customCommandFavorites) + .build() + val player = ExoPlayer.Builder(this).build() + // Build the session with a custom layout. + mediaSession = + MediaSession.Builder(this, player) + .setCallback(MyCallback()) + .setMediaButtonPreferences(ImmutableList.of(favoriteButton)) + .build() } - override fun onCustomCommand( - session: MediaSession, - controller: MediaSession.ControllerInfo, - customCommand: SessionCommand, - args: Bundle - ): ListenableFuture { - if (customCommand.customAction == ACTION_FAVORITES) { - // Do custom logic here - saveToFavorites(session.player.currentMediaItem) - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) + private inner class MyCallback : MediaSession.Callback { + override fun onConnect( + session: MediaSession, + controller: MediaSession.ControllerInfo + ): ConnectionResult { + // Set available player and session commands. + return AcceptedResultBuilder(session) + .setAvailableSessionCommands( + ConnectionResult.DEFAULT_SESSION_COMMANDS.buildUpon() + .add(customCommandFavorites) + .build() + ) + .build() } - return super.onCustomCommand(session, controller, customCommand, args) - } - } - // [START_EXCLUDE silent] - - override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { - return mediaSession - } - - private fun saveToFavorites(item: MediaItem?) {} - // [END_EXCLUDE] -} -// [END android_media_surfaces_mobile_custom_command_buttons] - -private fun addStandardActions( - context: Context, - notificationBuilder: androidx.core.app.NotificationCompat.Builder -) { - // [START android_media_surfaces_mobile_add_standard_actions] - val session = MediaSessionCompat(context, TAG) - val playbackStateBuilder = PlaybackStateCompat.Builder() - val style = NotificationCompat.MediaStyle() - - // For this example, the media is currently paused: - val state = PlaybackStateCompat.STATE_PAUSED - val position = 0L - val playbackSpeed = 1f - playbackStateBuilder.setState(state, position, playbackSpeed) - - // And the user can play, skip to next or previous, and seek - val stateActions = ( - PlaybackStateCompat.ACTION_PLAY - or PlaybackStateCompat.ACTION_PLAY_PAUSE - or PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - or PlaybackStateCompat.ACTION_SKIP_TO_NEXT - or PlaybackStateCompat.ACTION_SEEK_TO - ) // adding the seek action enables seeking with the seekbar - playbackStateBuilder.setActions(stateActions) - - // ... do more setup here ... - - session.setPlaybackState(playbackStateBuilder.build()) - style.setMediaSession(session.sessionToken) - notificationBuilder.setStyle(style) - // [END android_media_surfaces_mobile_add_standard_actions] -} - -private fun slotReservationExtras(session: MediaSessionCompat) { - // [START android_media_surfaces_mobile_slot_reservation_extras] - session.setExtras( - Bundle().apply { - putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) - putBoolean(SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) - } - ) - // [END android_media_surfaces_mobile_slot_reservation_extras] -} - -private fun addCustomActions(playbackStateBuilder: PlaybackStateCompat.Builder) { - // [START android_media_surfaces_mobile_add_custom_actions] - val customAction = PlaybackStateCompat.CustomAction.Builder( - "com.example.MY_CUSTOM_ACTION", // action ID - "Custom Action", // title - used as content description for the button - // [START_EXCLUDE silent] - android.R.drawable.ic_media_play - /* - // [END_EXCLUDE] - R.drawable.ic_custom_action - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] - ).build() - playbackStateBuilder.addCustomAction(customAction) - // [END android_media_surfaces_mobile_add_custom_actions] -} - -private fun playbackStateCallback(session: MediaSessionCompat) { - // [START android_media_surfaces_mobile_playback_state_callback] - val callback = object : MediaSessionCompat.Callback() { - override fun onPlay() { - // start playback - } - - override fun onPause() { - // pause playback - } - - override fun onSkipToPrevious() { - // skip to previous - } - - override fun onSkipToNext() { - // skip to next - } - - override fun onSeekTo(pos: Long) { - // jump to position in track - } - - override fun onCustomAction(action: String, extras: Bundle?) { - when (action) { - CUSTOM_ACTION_1 -> doCustomAction1(extras) - CUSTOM_ACTION_2 -> doCustomAction2(extras) - else -> { - Log.w(TAG, "Unknown custom action $action") + override fun onCustomCommand( + session: MediaSession, + controller: MediaSession.ControllerInfo, + customCommand: SessionCommand, + args: Bundle + ): ListenableFuture { + if (customCommand.customAction == ACTION_FAVORITES) { + // Do custom logic here. + saveToFavorites(session.player.currentMediaItem) + return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS)) } + return super.onCustomCommand(session, controller, customCommand, args) } } - } - - session.setCallback(callback) - // [END android_media_surfaces_mobile_playback_state_callback] -} - -private fun doCustomAction1(extras: Bundle?) {} -private fun doCustomAction2(extras: Bundle?) {} + // [START_EXCLUDE silent] -private class MobileResumptionBrowserService : MediaBrowserServiceCompat() { - // [START android_media_surfaces_mobile_on_get_root] - override fun onGetRoot( - clientPackageName: String, - clientUid: Int, - rootHints: Bundle? - ): BrowserRoot? { - // ... - // Verify that the specified package is SystemUI. You'll need to write your - // own logic to do this. - if (isSystem(clientPackageName, clientUid)) { - rootHints?.let { - if (it.getBoolean(BrowserRoot.EXTRA_RECENT)) { - // Return a tree with a single playable media item for resumption. - val extras = Bundle().apply { - putBoolean(BrowserRoot.EXTRA_RECENT, true) - } - return BrowserRoot(MY_RECENTS_ROOT_ID, extras) - } - } - // You can return your normal tree if the EXTRA_RECENT flag is not present. - return BrowserRoot(MY_MEDIA_ROOT_ID, null) + override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { + return mediaSession } - // Return an empty tree to disallow browsing. - return BrowserRoot(MY_EMPTY_ROOT_ID, null) - } - // [END android_media_surfaces_mobile_on_get_root] - private fun isSystem(clientPackageName: String, clientUid: Int): Boolean = true - - override fun onLoadChildren( - parentId: String, - result: MediaBrowserServiceCompat.Result> - ) { - result.sendResult(null) + private fun saveToFavorites(item: MediaItem?) {} + // [END_EXCLUDE] } + // [END android_media_surfaces_mobile_custom_command_buttons] } diff --git a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt b/media/src/main/java/com/example/media/SurfacesMobilePre13.kt deleted file mode 100644 index ebff8f8a9..000000000 --- a/media/src/main/java/com/example/media/SurfacesMobilePre13.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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.media - -import android.app.PendingIntent -import android.content.Context -import android.graphics.Bitmap -import androidx.annotation.OptIn -// [START android_media_surfaces_mobile_pre_android_13_notification] -import androidx.core.app.NotificationCompat -// [END android_media_surfaces_mobile_pre_android_13_notification] -import androidx.media3.common.util.UnstableApi -import androidx.media3.session.MediaSession -// [START android_media_surfaces_mobile_pre_android_13_notification] -import androidx.media3.session.MediaStyleNotificationHelper - -// [END android_media_surfaces_mobile_pre_android_13_notification] - -private const val CHANNEL_ID = "playback_channel" - -@OptIn(UnstableApi::class) -private fun preAndroid13Notification( - context: Context, - mediaSession: MediaSession, - prevPendingIntent: PendingIntent, - pausePendingIntent: PendingIntent, - nextPendingIntent: PendingIntent, - albumArtBitmap: Bitmap -) { - // [START android_media_surfaces_mobile_pre_android_13_notification] - var notification = NotificationCompat.Builder(context, CHANNEL_ID) - // Show controls on lock screen even when user hides sensitive content. - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - // [START_EXCLUDE silent] - .setSmallIcon(android.R.drawable.ic_media_play) - .addAction(android.R.drawable.ic_media_previous, "Previous", prevPendingIntent) // #0 - .addAction(android.R.drawable.ic_media_pause, "Pause", pausePendingIntent) // #1 - .addAction(android.R.drawable.ic_media_next, "Next", nextPendingIntent) // #2 - /* - // [END_EXCLUDE] - .setSmallIcon(R.drawable.ic_stat_player) - // Add media control buttons that invoke intents in your media service - .addAction(R.drawable.ic_prev, "Previous", prevPendingIntent) // #0 - .addAction(R.drawable.ic_pause, "Pause", pausePendingIntent) // #1 - .addAction(R.drawable.ic_next, "Next", nextPendingIntent) // #2 - // [START_EXCLUDE silent] - */ - // [END_EXCLUDE] - // Apply the media style template - .setStyle(MediaStyleNotificationHelper.MediaStyle(mediaSession) - .setShowActionsInCompactView(1 /* #1: pause button */)) - .setContentTitle("Wonderful music") - .setContentText("My Awesome Band") - .setLargeIcon(albumArtBitmap) - .build() - // [END android_media_surfaces_mobile_pre_android_13_notification] -} From ec003d3eef0b6962c4dfacc203955fe9ccda7d78 Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Fri, 2 Oct 2026 20:48:42 +0000 Subject: [PATCH 09/10] Address internal review feedback on Media I snippets Show super.onStart() in connect_ui, move the saveToFavorites stub out of the region, drop the unused core-ktx dependency, and register PlaybackService in the module manifest. --- media/build.gradle.kts | 1 - media/src/main/AndroidManifest.xml | 14 +++++++++++++- .../src/main/java/com/example/media/PlaybackApp.kt | 2 -- .../main/java/com/example/media/SurfacesMobile.kt | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/media/build.gradle.kts b/media/build.gradle.kts index aa0c4a344..47900dc9b 100644 --- a/media/build.gradle.kts +++ b/media/build.gradle.kts @@ -26,7 +26,6 @@ android { } dependencies { - implementation(libs.androidx.core.ktx) implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.effect) implementation(libs.androidx.media3.exoplayer) diff --git a/media/src/main/AndroidManifest.xml b/media/src/main/AndroidManifest.xml index 51685f593..46e31bc20 100644 --- a/media/src/main/AndroidManifest.xml +++ b/media/src/main/AndroidManifest.xml @@ -16,6 +16,18 @@ --> - + + + + + + + + + + diff --git a/media/src/main/java/com/example/media/PlaybackApp.kt b/media/src/main/java/com/example/media/PlaybackApp.kt index 331e010d0..0b63d2435 100644 --- a/media/src/main/java/com/example/media/PlaybackApp.kt +++ b/media/src/main/java/com/example/media/PlaybackApp.kt @@ -77,9 +77,7 @@ private class PlayerActivity : Activity() { // [START android_media_playback_app_connect_ui] override fun onStart() { - // [START_EXCLUDE silent] super.onStart() - // [END_EXCLUDE] val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java)) val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync() controllerFuture.addListener( diff --git a/media/src/main/java/com/example/media/SurfacesMobile.kt b/media/src/main/java/com/example/media/SurfacesMobile.kt index 46ceff989..a3c91b881 100644 --- a/media/src/main/java/com/example/media/SurfacesMobile.kt +++ b/media/src/main/java/com/example/media/SurfacesMobile.kt @@ -92,9 +92,9 @@ private object CustomControlsSnippet { override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaSession? { return mediaSession } - - private fun saveToFavorites(item: MediaItem?) {} // [END_EXCLUDE] } // [END android_media_surfaces_mobile_custom_command_buttons] } + +private fun saveToFavorites(item: MediaItem?) {} From efe932e121043289db323ed82a31c8955b7f231d Mon Sep 17 00:00:00 2001 From: Barbara Law Date: Fri, 9 Oct 2026 17:29:39 +0000 Subject: [PATCH 10/10] Use non-deprecated Handler constructor in audio focus snippet --- media/src/main/java/com/example/media/AudioFocus.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/media/src/main/java/com/example/media/AudioFocus.kt b/media/src/main/java/com/example/media/AudioFocus.kt index 4b4ade5f4..e21872d83 100644 --- a/media/src/main/java/com/example/media/AudioFocus.kt +++ b/media/src/main/java/com/example/media/AudioFocus.kt @@ -152,7 +152,7 @@ private class AudioFocusLegacyChangeHandler( private val mediaController: MediaController ) { // [START android_media_audio_focus_change_listener] - private val handler = Handler() + private val handler = Handler(Looper.getMainLooper()) private val afChangeListener = AudioManager.OnAudioFocusChangeListener { focusChange -> when (focusChange) { AudioManager.AUDIOFOCUS_LOSS -> {