From 079de73f0bc8d7eb86d5f55e782db498b7fc57f1 Mon Sep 17 00:00:00 2001 From: Kat Kuan <843428+kkuan2011@users.noreply.github.com> Date: Tue, 21 Apr 2026 23:53:03 +0000 Subject: [PATCH] Add code snippets for Android TV documentation --- gradle/libs.versions.toml | 1 + tv/build.gradle.kts | 3 + .../java/com/example/tv/ui/AmbientMode.kt | 43 ++++ .../com/example/tv/ui/AudioCapabilities.kt | 116 ++++++++++ .../java/com/example/tv/ui/DisplaySettings.kt | 72 ++++++ .../main/java/com/example/tv/ui/Hardware.kt | 99 +++++++++ .../java/com/example/tv/ui/MediaSession.kt | 125 +++++++++++ tv/src/main/java/com/example/tv/ui/Memory.kt | 33 +++ .../java/com/example/tv/ui/Multitasking.kt | 49 +++++ .../main/java/com/example/tv/ui/NowPlaying.kt | 43 ++++ .../main/java/com/example/tv/ui/TvCompose.kt | 207 ++++++++++++++++++ tv/src/main/java/com/example/tv/ui/TvGames.kt | 58 +++++ tv/src/main/res/drawable/button.xml | 28 +++ tv/src/main/res/drawable/button_focused.xml | 19 ++ tv/src/main/res/drawable/button_normal.xml | 19 ++ tv/src/main/res/drawable/button_pressed.xml | 19 ++ tv/src/main/res/drawable/placeholder.xml | 21 ++ tv/src/main/res/values/colors.xml | 19 ++ tv/src/main/res/values/create.xml | 25 +++ tv/src/main/res/values/strings.xml | 1 + 20 files changed, 1000 insertions(+) create mode 100644 tv/src/main/java/com/example/tv/ui/AmbientMode.kt create mode 100644 tv/src/main/java/com/example/tv/ui/AudioCapabilities.kt create mode 100644 tv/src/main/java/com/example/tv/ui/DisplaySettings.kt create mode 100644 tv/src/main/java/com/example/tv/ui/Hardware.kt create mode 100644 tv/src/main/java/com/example/tv/ui/MediaSession.kt create mode 100644 tv/src/main/java/com/example/tv/ui/Memory.kt create mode 100644 tv/src/main/java/com/example/tv/ui/Multitasking.kt create mode 100644 tv/src/main/java/com/example/tv/ui/NowPlaying.kt create mode 100644 tv/src/main/java/com/example/tv/ui/TvCompose.kt create mode 100644 tv/src/main/java/com/example/tv/ui/TvGames.kt create mode 100644 tv/src/main/res/drawable/button.xml create mode 100644 tv/src/main/res/drawable/button_focused.xml create mode 100644 tv/src/main/res/drawable/button_normal.xml create mode 100644 tv/src/main/res/drawable/button_pressed.xml create mode 100644 tv/src/main/res/drawable/placeholder.xml create mode 100644 tv/src/main/res/values/colors.xml create mode 100644 tv/src/main/res/values/create.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f3d5e413..032459af5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -282,6 +282,7 @@ robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectr roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } +tv-compose-foundation = { module = "androidx.tv:tv-foundation", version = "1.0.0-rc01" } tv-compose-material = { module = "androidx.tv:tv-material", version.ref = "tvComposeMaterial3" } truth = { module = "com.google.truth:truth", version.ref = "truth" } validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" } diff --git a/tv/build.gradle.kts b/tv/build.gradle.kts index 269758e42..df0e6f574 100644 --- a/tv/build.gradle.kts +++ b/tv/build.gradle.kts @@ -61,6 +61,9 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) implementation(libs.tv.compose.material) + implementation(libs.tv.compose.foundation) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.work.runtime.ktx) implementation(libs.coil.kt.compose) testImplementation(libs.junit) androidTestImplementation(libs.androidx.test.ext.junit) diff --git a/tv/src/main/java/com/example/tv/ui/AmbientMode.kt b/tv/src/main/java/com/example/tv/ui/AmbientMode.kt new file mode 100644 index 000000000..8fb10d06f --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/AmbientMode.kt @@ -0,0 +1,43 @@ +/* + * 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.tv.ui + +import android.view.WindowManager +import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect + +class AmbientModeActivity : ComponentActivity() { + + @Composable + private fun KeepScreenOn(enable: Boolean = true) { + val activity = LocalActivity.current + DisposableEffect(enable) { + if (enable) { + // [START android_tv_ambient_mode_on] + activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + // [END android_tv_ambient_mode_on] + } + onDispose { + // [START android_tv_ambient_mode_off] + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + // [END android_tv_ambient_mode_off] + } + } + } +} diff --git a/tv/src/main/java/com/example/tv/ui/AudioCapabilities.kt b/tv/src/main/java/com/example/tv/ui/AudioCapabilities.kt new file mode 100644 index 000000000..b33055682 --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/AudioCapabilities.kt @@ -0,0 +1,116 @@ +/* + * 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.tv.ui + +import android.media.AudioAttributes +import android.media.AudioDeviceInfo +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioTrack +import android.os.Build +import android.os.Handler +import android.os.Looper +import androidx.activity.ComponentActivity +import androidx.annotation.RequiresApi + +class AudioCapabilitiesActivity : ComponentActivity() { + private lateinit var audioManager: AudioManager + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + fun anticipatoryAudioRouteCheck() { + // [START android_tv_audio_capabilities_check] + val format = AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_E_AC3) + .setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1) + .setSampleRate(48000) + .build() + val attributes = AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_MEDIA) + .build() + + if (AudioManager.getDirectPlaybackSupport(format, attributes) != + AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED + ) { + // The format and attributes are supported for direct playback + // on the currently active routed audio path + } else { + // The format and attributes are NOT supported for direct playback + // on the currently active routed audio path + } + // [END android_tv_audio_capabilities_check] + } + + private fun findBestSampleRate(profile: Any): Int = 48000 + private fun findBestChannelMask(profile: Any): Int = AudioFormat.CHANNEL_OUT_STEREO + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + // [START android_tv_audio_capabilities_best_format] + private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat { + val preferredFormats = listOf( + AudioFormat.ENCODING_E_AC3, + AudioFormat.ENCODING_AC3, + AudioFormat.ENCODING_PCM_16BIT, + AudioFormat.ENCODING_DEFAULT + ) + val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes) + val bestAudioProfile = preferredFormats.firstNotNullOf { format -> + audioProfiles.firstOrNull { it.format == format } + } + val sampleRate = findBestSampleRate(bestAudioProfile) + val channelMask = findBestChannelMask(bestAudioProfile) + return AudioFormat.Builder() + .setEncoding(bestAudioProfile.format) + .setSampleRate(sampleRate) + .setChannelMask(channelMask) + .build() + } + // [END android_tv_audio_capabilities_best_format] + + private fun restartAudioTrack(info: AudioDeviceInfo?) {} + private fun findDefaultAudioDeviceInfo(): AudioDeviceInfo? = null + private fun needsAudioFormatChange(info: AudioDeviceInfo): Boolean = false + + fun interceptAudioDeviceChanges() { + val audioPlayer = AudioPlayerWrapper() + val handler = Handler(Looper.getMainLooper()) + + // [START android_tv_audio_capabilities_intercept] + // audioPlayer is a wrapper around an AudioTrack + // which calls a callback for an AudioTrack write error + audioPlayer.addAudioTrackWriteErrorListener { + // error code can be checked here, + // in case of write error try to recreate the audio track + restartAudioTrack(findDefaultAudioDeviceInfo()) + } + + audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting -> + audioRouting?.routedDevice?.let { audioDeviceInfo -> + // use the updated audio routed device to determine + // what audio format should be used + if (needsAudioFormatChange(audioDeviceInfo)) { + restartAudioTrack(audioDeviceInfo) + } + } + }, handler) + // [END android_tv_audio_capabilities_intercept] + } +} + +private class AudioPlayerWrapper { + val audioTrack = AudioTrack.Builder().build() + fun addAudioTrackWriteErrorListener(listener: () -> Unit) {} +} diff --git a/tv/src/main/java/com/example/tv/ui/DisplaySettings.kt b/tv/src/main/java/com/example/tv/ui/DisplaySettings.kt new file mode 100644 index 000000000..830313405 --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/DisplaySettings.kt @@ -0,0 +1,72 @@ +/* + * 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.tv.ui + +import android.content.Context +import android.media.MediaFormat +import android.media.quality.MediaQualityManager +import android.media.quality.PictureProfile +import android.os.Build +import android.os.Bundle +import androidx.activity.ComponentActivity + +// [START android_tv_adjust_display_constants] +const val NAME_STANDARD: String = "standard" +const val NAME_VIVID: String = "vivid" +const val NAME_SPORTS: String = "sports" +const val NAME_GAME: String = "game" +const val NAME_MOVIE: String = "movie" +const val NAME_ENERGY_SAVING: String = "energy_saving" +const val NAME_USER: String = "user" +// [END android_tv_adjust_display_constants] + +class DisplaySettingsActivity : ComponentActivity() { + private lateinit var context: Context + private lateinit var mediaCodec: android.media.MediaCodec + private lateinit var mediaQualityManager: MediaQualityManager + + fun queryAndApplySportsProfile() { + // [START android_tv_adjust_display_query] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + val mediaQualityManager: MediaQualityManager = + context.getSystemService(MediaQualityManager::class.java) + val profiles = mediaQualityManager.getAvailablePictureProfiles(null) + for (profile in profiles) { + // If we have a system sports profile, apply it to our media codec + if (profile.profileType == PictureProfile.TYPE_SYSTEM && + profile.name == NAME_SPORTS + ) { + val bundle = Bundle().apply { + putParcelable(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE, profile) + } + mediaCodec.setParameters(bundle) + } + } + } + // [END android_tv_adjust_display_query] + } + + fun getSpecificProfileByName() { + // [START android_tv_adjust_display_get_specific] + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + val profile = mediaQualityManager.getPictureProfile( + PictureProfile.TYPE_SYSTEM, NAME_SPORTS, null + ) + } + // [END android_tv_adjust_display_get_specific] + } +} diff --git a/tv/src/main/java/com/example/tv/ui/Hardware.kt b/tv/src/main/java/com/example/tv/ui/Hardware.kt new file mode 100644 index 000000000..afa5e6cbb --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/Hardware.kt @@ -0,0 +1,99 @@ +/* + * 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.tv.ui + +import android.annotation.SuppressLint +import androidx.activity.ComponentActivity +import android.content.Context +import android.content.pm.PackageManager +import android.location.Address +import android.location.Geocoder +import android.location.Location +import android.location.LocationManager +import android.util.Log +import java.io.IOException + + // [START android_tv_hardware_check_tv] + const val TAG = "DeviceTypeRuntimeCheck" + +// [START_EXCLUDE silent] +class HardwareActivity : ComponentActivity() { + val TAG = "HardwareActivity" + + fun checkTvDevice() { + +// [END_EXCLUDE] + val isTelevision = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + if (isTelevision) { + Log.d(TAG, "Running on a TV Device") + } else { + Log.d(TAG, "Running on a non-TV Device") + } + // [END android_tv_hardware_check_tv] + } + + fun checkHardwareFeatures() { + // [START android_tv_hardware_check_feature] + // Check whether the telephony hardware feature is available. + if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) { + Log.d("HardwareFeatureTest", "Device can make phone calls") + } + + // Check whether android.hardware.touchscreen feature is available. + if (packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) { + Log.d("HardwareFeatureTest", "Device has a touchscreen.") + } + // [END android_tv_hardware_check_feature] + } + + fun checkCamera() { + // [START android_tv_hardware_check_camera] + // Check whether the camera hardware feature is available. + if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) { + Log.d("Camera test", "Camera available!") + } else { + Log.d("Camera test", "No camera available. View and edit features only.") + } + // [END android_tv_hardware_check_camera] + } + + @SuppressLint("MissingPermission") + fun gpsStaticLocation() { + // [START android_tv_hardware_gps_static] + // Request a static location from the location manager. + val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager + val location = locationManager.getLastKnownLocation("static") + if (location == null) { + Log.e(TAG, "Location is null") + return + } + + // Attempt to get postal code from the static location object. + val geocoder = Geocoder(this) + val address: Address? = + try { + geocoder.getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull() + ?.apply { + Log.d(TAG, postalCode) + } + } catch (e: IOException) { + Log.e(TAG, "Geocoder error", e) + null + } + // [END android_tv_hardware_gps_static] + } +} diff --git a/tv/src/main/java/com/example/tv/ui/MediaSession.kt b/tv/src/main/java/com/example/tv/ui/MediaSession.kt new file mode 100644 index 000000000..43bfbd4cb --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/MediaSession.kt @@ -0,0 +1,125 @@ +/* + * 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.tv.ui + +import android.graphics.Bitmap +import android.media.MediaMetadata +import android.media.MediaPlayer +import android.media.session.MediaSession +import android.media.session.PlaybackState +import androidx.activity.ComponentActivity + +private class MediaData( + val displayTitle: String, + val displaySubtitle: String, + val artUri: String, + val title: String, + val artist: String, + val artBitmap: Bitmap +) + +private class MediaSessionCallback : MediaSession.Callback() + +class MediaSessionActivity : ComponentActivity() { + private lateinit var session: MediaSession + private var mState = PlaybackState.STATE_NONE + private var mediaPlayer: MediaPlayer? = null + private var playingQueue: List? = null + private var currentIndexOnQueue = 0 + + fun createMediaSession() { + // [START android_tv_media_session_create] + session = MediaSession(this, "MusicService").apply { + setCallback(MediaSessionCallback()) + setFlags( + MediaSession.FLAG_HANDLES_MEDIA_BUTTONS or MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS + ) + } + // [END android_tv_media_session_create] + } + + private fun tryToGetAudioFocus() {} + + // [START android_tv_media_session_start] + fun handlePlayRequest() { + tryToGetAudioFocus() + + if (!session.isActive) { + session.isActive = true + } + // ... + } + // [END android_tv_media_session_start] + + // [START android_tv_media_session_update_state] + private fun updatePlaybackState() { + val position: Long = + mediaPlayer + ?.takeIf { it.isPlaying } + ?.currentPosition?.toLong() + ?: PlaybackState.PLAYBACK_POSITION_UNKNOWN + + val stateBuilder = PlaybackState.Builder() + .setActions(getAvailableActions()).apply { + setState(mState, position, 1.0f) + } + session.setPlaybackState(stateBuilder.build()) + } + + private fun getAvailableActions(): Long { + var actions = ( + PlaybackState.ACTION_PLAY_PAUSE + or PlaybackState.ACTION_PLAY_FROM_MEDIA_ID + or PlaybackState.ACTION_PLAY_FROM_SEARCH + ) + + playingQueue?.takeIf { it.isNotEmpty() }?.apply { + actions = if (mState == PlaybackState.STATE_PLAYING) { + actions or PlaybackState.ACTION_PAUSE + } else { + actions or PlaybackState.ACTION_PLAY + } + if (currentIndexOnQueue > 0) { + actions = actions or PlaybackState.ACTION_SKIP_TO_PREVIOUS + } + if (currentIndexOnQueue < size - 1) { + actions = actions or PlaybackState.ACTION_SKIP_TO_NEXT + } + } + return actions + } + // [END android_tv_media_session_update_state] + + // [START android_tv_media_session_update_metadata] + private fun updateMetadata(myData: MediaData) { + val metadataBuilder = MediaMetadata.Builder().apply { + // To provide most control over how an item is displayed set the + // display fields in the metadata + putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, myData.displayTitle) + putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, myData.displaySubtitle) + putString(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI, myData.artUri) + // And at minimum the title and artist for legacy support + putString(MediaMetadata.METADATA_KEY_TITLE, myData.title) + putString(MediaMetadata.METADATA_KEY_ARTIST, myData.artist) + // A small bitmap for the artwork is also recommended + putBitmap(MediaMetadata.METADATA_KEY_ART, myData.artBitmap) + // Add any other fields you have for your data as well + } + session.setMetadata(metadataBuilder.build()) + } + // [END android_tv_media_session_update_metadata] +} diff --git a/tv/src/main/java/com/example/tv/ui/Memory.kt b/tv/src/main/java/com/example/tv/ui/Memory.kt new file mode 100644 index 000000000..82c8136f9 --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/Memory.kt @@ -0,0 +1,33 @@ +/* + * 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.tv.ui + +import androidx.activity.ComponentActivity +import androidx.work.Constraints +import androidx.work.NetworkType + +class MemoryActivity : ComponentActivity() { + fun workManagerConstraints() { + // [START android_tv_memory_constraints] + Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresStorageNotLow(true) + .setRequiresDeviceIdle(true) + .build() + // [END android_tv_memory_constraints] + } +} diff --git a/tv/src/main/java/com/example/tv/ui/Multitasking.kt b/tv/src/main/java/com/example/tv/ui/Multitasking.kt new file mode 100644 index 000000000..14d6de44f --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/Multitasking.kt @@ -0,0 +1,49 @@ +/* + * 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.tv.ui + +import android.app.PictureInPictureParams +import android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE +import android.os.Bundle +import android.util.Rational +import android.view.View +import android.widget.Button +import androidx.fragment.app.Fragment + +class MultitaskingFragment : Fragment() { + + private lateinit var pictureInPictureButton: Button + + // [START android_tv_multitasking_enter_pip] + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + pictureInPictureButton.visibility = + if (requireActivity().packageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { + pictureInPictureButton.setOnClickListener { + val aspectRatio = Rational(view.width, view.height) + val params = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build() + val result = requireActivity().enterPictureInPictureMode(params) + } + View.VISIBLE + } else { + View.GONE + } + } + // [END android_tv_multitasking_enter_pip] +} diff --git a/tv/src/main/java/com/example/tv/ui/NowPlaying.kt b/tv/src/main/java/com/example/tv/ui/NowPlaying.kt new file mode 100644 index 000000000..fb2e2592d --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/NowPlaying.kt @@ -0,0 +1,43 @@ +/* + * 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.tv.ui + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.media.session.MediaSession +import androidx.activity.ComponentActivity + +class MyActivity : ComponentActivity() + +class NowPlayingActivity : ComponentActivity() { + private lateinit var session: MediaSession + + fun setSessionActivity() { + val context: Context = this + // [START android_tv_now_playing_set_activity] + val pi: PendingIntent = Intent(context, MyActivity::class.java).let { intent -> + PendingIntent.getActivity( + context, 99 /*request code*/, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } + session.setSessionActivity(pi) + // [END android_tv_now_playing_set_activity] + } +} diff --git a/tv/src/main/java/com/example/tv/ui/TvCompose.kt b/tv/src/main/java/com/example/tv/ui/TvCompose.kt new file mode 100644 index 000000000..828df2297 --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/TvCompose.kt @@ -0,0 +1,207 @@ +/* + * 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. + */ + +@file:OptIn(androidx.tv.material3.ExperimentalTvMaterial3Api::class) + +package com.example.tv.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.tv.material3.Card +import androidx.tv.material3.Carousel +import coil.compose.AsyncImage +import com.example.tv.R +import com.example.tv.data.Movie +import com.example.tv.data.Section + +private object TvEmptyCatalog { + // [START android_tv_compose_browse_define_catalog] + @Composable + fun CatalogBrowser( + featuredContentList: List, + sectionList: List
, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, + ) { + // ToDo: add implementation + } + // [END android_tv_compose_browse_define_catalog] +} + +private object TvLazyColumnCatalog { + // [START android_tv_compose_browse_lazy_column] + @Composable + fun CatalogBrowser( + featuredContentList: List, + sectionList: List
, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, + ) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + items(sectionList) { section -> + Section(section, onItemSelected = onItemSelected) + } + } + } + // [END android_tv_compose_browse_lazy_column] + + @Composable + fun Section( + section: Section, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, + ) { } +} + +private object TvBrowseCatalog { + // [START android_tv_compose_browse_section] + @Composable + fun Section( + section: Section, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, + ) { + Text( + text = section.title, + style = MaterialTheme.typography.headlineSmall, + ) + LazyRow( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(section.movieList) { movie -> + MovieCard( + movie = movie, + onClick = { onItemSelected(movie) } + ) + } + } + } + // [END android_tv_compose_browse_section] + + // [START android_tv_compose_browse_movie_card] + @Composable + fun MovieCard( + movie: Movie, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} + ) { + Card(modifier = modifier, onClick = onClick) { + AsyncImage( + model = movie.thumbnailUrl, + contentDescription = movie.title, + ) + } + } + // [END android_tv_compose_browse_movie_card] + + // [START android_tv_compose_browse_carousel] + @Composable + fun FeaturedCarousel( + featuredContentList: List, + modifier: Modifier = Modifier, + ) { + Carousel( + itemCount = featuredContentList.size, + modifier = modifier, + ) { index -> + val content = featuredContentList[index] + Box { + AsyncImage( + model = content.backgroundImageUrl, + contentDescription = content.description, + placeholder = painterResource( + id = R.drawable.placeholder + ), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + Text(text = content.title) + } + } + } + // [END android_tv_compose_browse_carousel] + + // [START android_tv_compose_browse_full] + @Composable + fun CatalogBrowser( + featuredContentList: List, + sectionList: List
, + modifier: Modifier = Modifier, + onItemSelected: (Movie) -> Unit = {}, + ) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + item { + FeaturedCarousel(featuredContentList) + } + + items(sectionList) { section -> + Section(section, onItemSelected = onItemSelected) + } + } + } + // [END android_tv_compose_browse_full] +} + +// [START android_tv_compose_details] +@Composable +fun DetailsScreen( + movie: Movie, + modifier: Modifier = Modifier, + onStartPlayback: (Movie) -> Unit = {} +) { + Box(modifier = modifier.fillMaxSize()) { + AsyncImage( + modifier = Modifier.fillMaxSize(), + model = movie.backgroundImageUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + ) + Column(modifier = Modifier.padding(32.dp)) { + Text( + text = movie.title, + style = MaterialTheme.typography.headlineMedium + ) + Text(text = movie.description) + Button(onClick = { onStartPlayback(movie) }) { + Text(text = stringResource(id = R.string.startPlayback)) + } + } + } +} +// [END android_tv_compose_details] diff --git a/tv/src/main/java/com/example/tv/ui/TvGames.kt b/tv/src/main/java/com/example/tv/ui/TvGames.kt new file mode 100644 index 000000000..5659ebf08 --- /dev/null +++ b/tv/src/main/java/com/example/tv/ui/TvGames.kt @@ -0,0 +1,58 @@ +/* + * 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.tv.ui + +import android.hardware.input.InputManager +import android.os.Build +import android.os.Bundle +import android.view.InputDevice +import android.view.KeyEvent +import android.view.View +import androidx.annotation.RequiresApi +import androidx.core.content.getSystemService +import androidx.fragment.app.Fragment + +class GamesFragment : Fragment() { + + private var keyUp: Int = 0 + private var keyLeft: Int = 0 + private var keyDown: Int = 0 + private var keyRight: Int = 0 + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + determineKeyboardLayout() + } + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun determineKeyboardLayout() { + // [START android_tv_games_keyboard_layout] + val inputManager: InputManager? = requireActivity().getSystemService() + + inputManager?.inputDeviceIds?.map { inputManager.getInputDevice(it) } + ?.firstOrNull { it?.keyboardType == InputDevice.KEYBOARD_TYPE_ALPHABETIC } + ?.let { inputDevice -> + keyUp = inputDevice.getKeyCodeForKeyLocation(KeyEvent.KEYCODE_W) + keyLeft = inputDevice.getKeyCodeForKeyLocation(KeyEvent.KEYCODE_A) + keyDown = inputDevice.getKeyCodeForKeyLocation(KeyEvent.KEYCODE_S) + keyRight = inputDevice.getKeyCodeForKeyLocation(KeyEvent.KEYCODE_D) + } + // [END android_tv_games_keyboard_layout] + } +} diff --git a/tv/src/main/res/drawable/button.xml b/tv/src/main/res/drawable/button.xml new file mode 100644 index 000000000..407e9f0b1 --- /dev/null +++ b/tv/src/main/res/drawable/button.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/tv/src/main/res/drawable/button_focused.xml b/tv/src/main/res/drawable/button_focused.xml new file mode 100644 index 000000000..04fd9193b --- /dev/null +++ b/tv/src/main/res/drawable/button_focused.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/tv/src/main/res/drawable/button_normal.xml b/tv/src/main/res/drawable/button_normal.xml new file mode 100644 index 000000000..bb0908aba --- /dev/null +++ b/tv/src/main/res/drawable/button_normal.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/tv/src/main/res/drawable/button_pressed.xml b/tv/src/main/res/drawable/button_pressed.xml new file mode 100644 index 000000000..cea800a47 --- /dev/null +++ b/tv/src/main/res/drawable/button_pressed.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/tv/src/main/res/drawable/placeholder.xml b/tv/src/main/res/drawable/placeholder.xml new file mode 100644 index 000000000..467a7cc77 --- /dev/null +++ b/tv/src/main/res/drawable/placeholder.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/tv/src/main/res/values/colors.xml b/tv/src/main/res/values/colors.xml new file mode 100644 index 000000000..2a06718ca --- /dev/null +++ b/tv/src/main/res/values/colors.xml @@ -0,0 +1,19 @@ + + + + #FF0000 + diff --git a/tv/src/main/res/values/create.xml b/tv/src/main/res/values/create.xml new file mode 100644 index 000000000..c4ae4ebb8 --- /dev/null +++ b/tv/src/main/res/values/create.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/tv/src/main/res/values/strings.xml b/tv/src/main/res/values/strings.xml index 45e7a6303..702b3893f 100644 --- a/tv/src/main/res/values/strings.xml +++ b/tv/src/main/res/values/strings.xml @@ -17,4 +17,5 @@ TV Snippets TvMoviesActivity + Start Playback