Skip to content

Commit 5d1f6d7

Browse files
committed
Add code snippets for Android TV documentation
1 parent 4fe2def commit 5d1f6d7

20 files changed

Lines changed: 992 additions & 0 deletions

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectr
282282
roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" }
283283
roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" }
284284
roborazzi-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" }
285+
tv-compose-foundation = { module = "androidx.tv:tv-foundation", version = "1.0.0-rc01" }
285286
tv-compose-material = { module = "androidx.tv:tv-material", version.ref = "tvComposeMaterial3" }
286287
truth = { module = "com.google.truth:truth", version.ref = "truth" }
287288
validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" }

tv/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ dependencies {
6161
implementation(libs.androidx.compose.ui.tooling.preview)
6262
implementation(libs.androidx.compose.material3)
6363
implementation(libs.tv.compose.material)
64+
implementation(libs.tv.compose.foundation)
65+
implementation(libs.androidx.fragment.ktx)
66+
implementation(libs.androidx.work.runtime.ktx)
6467
implementation(libs.coil.kt.compose)
6568
testImplementation(libs.junit)
6669
androidTestImplementation(libs.androidx.test.ext.junit)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.tv.ui
18+
19+
import android.view.WindowManager
20+
import androidx.fragment.app.Fragment
21+
22+
class PlaybackFragment : Fragment() {
23+
24+
fun ambientModeOn() {
25+
// [START android_tv_ambient_mode_on]
26+
requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
27+
// [END android_tv_ambient_mode_on]
28+
}
29+
30+
fun ambientModeOff() {
31+
// [START android_tv_ambient_mode_off]
32+
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
33+
// [END android_tv_ambient_mode_off]
34+
}
35+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.tv.ui
18+
19+
import android.media.AudioAttributes
20+
import android.media.AudioDeviceInfo
21+
import android.media.AudioFormat
22+
import android.media.AudioManager
23+
import android.media.AudioTrack
24+
import android.os.Build
25+
import android.os.Handler
26+
import android.os.Looper
27+
import androidx.activity.ComponentActivity
28+
import androidx.annotation.RequiresApi
29+
30+
class AudioCapabilitiesActivity : ComponentActivity() {
31+
private lateinit var audioManager: AudioManager
32+
33+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
34+
fun anticipatoryAudioRouteCheck() {
35+
// [START android_tv_audio_capabilities_check]
36+
val format = AudioFormat.Builder()
37+
.setEncoding(AudioFormat.ENCODING_E_AC3)
38+
.setChannelMask(AudioFormat.CHANNEL_OUT_5POINT1)
39+
.setSampleRate(48000)
40+
.build()
41+
val attributes = AudioAttributes.Builder()
42+
.setUsage(AudioAttributes.USAGE_MEDIA)
43+
.build()
44+
45+
if (AudioManager.getDirectPlaybackSupport(format, attributes) !=
46+
AudioManager.DIRECT_PLAYBACK_NOT_SUPPORTED
47+
) {
48+
// The format and attributes are supported for direct playback
49+
// on the currently active routed audio path
50+
} else {
51+
// The format and attributes are NOT supported for direct playback
52+
// on the currently active routed audio path
53+
}
54+
// [END android_tv_audio_capabilities_check]
55+
}
56+
57+
private fun findBestSampleRate(profile: Any): Int = 48000
58+
private fun findBestChannelMask(profile: Any): Int = AudioFormat.CHANNEL_OUT_STEREO
59+
60+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
61+
// [START android_tv_audio_capabilities_best_format]
62+
private fun findBestAudioFormat(audioAttributes: AudioAttributes): AudioFormat {
63+
val preferredFormats = listOf(
64+
AudioFormat.ENCODING_E_AC3,
65+
AudioFormat.ENCODING_AC3,
66+
AudioFormat.ENCODING_PCM_16BIT,
67+
AudioFormat.ENCODING_DEFAULT
68+
)
69+
val audioProfiles = audioManager.getDirectProfilesForAttributes(audioAttributes)
70+
val bestAudioProfile = preferredFormats.firstNotNullOf { format ->
71+
audioProfiles.firstOrNull { it.format == format }
72+
}
73+
val sampleRate = findBestSampleRate(bestAudioProfile)
74+
val channelMask = findBestChannelMask(bestAudioProfile)
75+
return AudioFormat.Builder()
76+
.setEncoding(bestAudioProfile.format)
77+
.setSampleRate(sampleRate)
78+
.setChannelMask(channelMask)
79+
.build()
80+
}
81+
// [END android_tv_audio_capabilities_best_format]
82+
83+
private fun restartAudioTrack(info: AudioDeviceInfo?) {}
84+
private fun findDefaultAudioDeviceInfo(): AudioDeviceInfo? = null
85+
private fun needsAudioFormatChange(info: AudioDeviceInfo): Boolean = false
86+
87+
fun interceptAudioDeviceChanges() {
88+
val audioPlayer = AudioPlayerWrapper()
89+
val handler = Handler(Looper.getMainLooper())
90+
91+
// [START android_tv_audio_capabilities_intercept]
92+
// audioPlayer is a wrapper around an AudioTrack
93+
// which calls a callback for an AudioTrack write error
94+
audioPlayer.addAudioTrackWriteErrorListener {
95+
// error code can be checked here,
96+
// in case of write error try to recreate the audio track
97+
restartAudioTrack(findDefaultAudioDeviceInfo())
98+
}
99+
100+
audioPlayer.audioTrack.addOnRoutingChangedListener({ audioRouting ->
101+
audioRouting?.routedDevice?.let { audioDeviceInfo ->
102+
// use the updated audio routed device to determine
103+
// what audio format should be used
104+
if (needsAudioFormatChange(audioDeviceInfo)) {
105+
restartAudioTrack(audioDeviceInfo)
106+
}
107+
}
108+
}, handler)
109+
// [END android_tv_audio_capabilities_intercept]
110+
}
111+
}
112+
113+
private class AudioPlayerWrapper {
114+
val audioTrack = AudioTrack.Builder().build()
115+
fun addAudioTrackWriteErrorListener(listener: () -> Unit) {}
116+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.tv.ui
18+
19+
import android.content.Context
20+
import android.media.MediaFormat
21+
import android.media.quality.MediaQualityManager
22+
import android.media.quality.PictureProfile
23+
import android.os.Build
24+
import android.os.Bundle
25+
import androidx.activity.ComponentActivity
26+
27+
// [START android_tv_adjust_display_constants]
28+
const val NAME_STANDARD: String = "standard"
29+
const val NAME_VIVID: String = "vivid"
30+
const val NAME_SPORTS: String = "sports"
31+
const val NAME_GAME: String = "game"
32+
const val NAME_MOVIE: String = "movie"
33+
const val NAME_ENERGY_SAVING: String = "energy_saving"
34+
const val NAME_USER: String = "user"
35+
// [END android_tv_adjust_display_constants]
36+
37+
class DisplaySettingsActivity : ComponentActivity() {
38+
private lateinit var context: Context
39+
private lateinit var mediaCodec: android.media.MediaCodec
40+
private lateinit var mediaQualityManager: MediaQualityManager
41+
42+
fun queryAndApplySportsProfile() {
43+
// [START android_tv_adjust_display_query]
44+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
45+
val mediaQualityManager: MediaQualityManager =
46+
context.getSystemService(MediaQualityManager::class.java)
47+
val profiles = mediaQualityManager.getAvailablePictureProfiles(null)
48+
for (profile in profiles) {
49+
// If we have a system sports profile, apply it to our media codec
50+
if (profile.profileType == PictureProfile.TYPE_SYSTEM &&
51+
profile.name == NAME_SPORTS
52+
) {
53+
val bundle = Bundle().apply {
54+
putParcelable(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE, profile)
55+
}
56+
mediaCodec.setParameters(bundle)
57+
}
58+
}
59+
}
60+
// [END android_tv_adjust_display_query]
61+
}
62+
63+
fun getSpecificProfileByName() {
64+
// [START android_tv_adjust_display_get_specific]
65+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
66+
val profile = mediaQualityManager.getPictureProfile(
67+
PictureProfile.TYPE_SYSTEM, NAME_SPORTS, null
68+
)
69+
}
70+
// [END android_tv_adjust_display_get_specific]
71+
}
72+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2026 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.tv.ui
18+
19+
import android.annotation.SuppressLint
20+
import androidx.activity.ComponentActivity
21+
import android.content.Context
22+
import android.content.pm.PackageManager
23+
import android.location.Address
24+
import android.location.Geocoder
25+
import android.location.Location
26+
import android.location.LocationManager
27+
import android.util.Log
28+
import java.io.IOException
29+
30+
// [START android_tv_hardware_check_tv]
31+
const val TAG = "DeviceTypeRuntimeCheck"
32+
33+
// [START_EXCLUDE silent]
34+
class HardwareActivity : ComponentActivity() {
35+
val TAG = "HardwareActivity"
36+
37+
fun checkTvDevice() {
38+
39+
// [END_EXCLUDE]
40+
val isTelevision = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
41+
if (isTelevision) {
42+
Log.d(TAG, "Running on a TV Device")
43+
} else {
44+
Log.d(TAG, "Running on a non-TV Device")
45+
}
46+
// [END android_tv_hardware_check_tv]
47+
}
48+
49+
fun checkHardwareFeatures() {
50+
// [START android_tv_hardware_check_feature]
51+
// Check whether the telephony hardware feature is available.
52+
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)) {
53+
Log.d("HardwareFeatureTest", "Device can make phone calls")
54+
}
55+
56+
// Check whether android.hardware.touchscreen feature is available.
57+
if (packageManager.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
58+
Log.d("HardwareFeatureTest", "Device has a touchscreen.")
59+
}
60+
// [END android_tv_hardware_check_feature]
61+
}
62+
63+
fun checkCamera() {
64+
// [START android_tv_hardware_check_camera]
65+
// Check whether the camera hardware feature is available.
66+
if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
67+
Log.d("Camera test", "Camera available!")
68+
} else {
69+
Log.d("Camera test", "No camera available. View and edit features only.")
70+
}
71+
// [END android_tv_hardware_check_camera]
72+
}
73+
74+
@SuppressLint("MissingPermission")
75+
fun gpsStaticLocation() {
76+
// [START android_tv_hardware_gps_static]
77+
// Request a static location from the location manager.
78+
val locationManager = this.getSystemService(Context.LOCATION_SERVICE) as LocationManager
79+
val location = locationManager.getLastKnownLocation("static")
80+
if (location == null) {
81+
Log.e(TAG, "Location is null")
82+
return
83+
}
84+
85+
// Attempt to get postal code from the static location object.
86+
val geocoder = Geocoder(this)
87+
val address: Address? =
88+
try {
89+
geocoder.getFromLocation(location.latitude, location.longitude, 1)?.firstOrNull()
90+
?.apply {
91+
Log.d(TAG, postalCode)
92+
}
93+
} catch (e: IOException) {
94+
Log.e(TAG, "Geocoder error", e)
95+
null
96+
}
97+
// [END android_tv_hardware_gps_static]
98+
}
99+
}

0 commit comments

Comments
 (0)