Skip to content

Commit 08e7f0d

Browse files
committed
Create sample TV app playing Datadog On episodes
1 parent 0b008be commit 08e7f0d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1542
-2
lines changed

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ tasks.register("assembleSampleRelease") {
9494
dependsOn(
9595
":sample:kotlin:assembleUs1Release",
9696
":sample:wear:assembleUs1Release",
97-
":sample:vendor-lib:assembleRelease"
97+
":sample:vendor-lib:assembleRelease",
98+
":sample:tv:assembleRelease"
9899
)
99100
}
100101

sample/tv/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import com.datadog.gradle.config.AndroidConfig
88
import com.datadog.gradle.config.configureFlavorForTvApp
99
import com.datadog.gradle.config.dependencyUpdateConfig
1010
import com.datadog.gradle.config.java17
11-
import com.datadog.gradle.config.javadocConfig
1211
import com.datadog.gradle.config.junitConfig
1312
import com.datadog.gradle.config.kotlinConfig
1413
import com.datadog.gradle.config.taskConfig
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
4+
~ This product includes software developed at Datadog (https://www.datadoghq.com/).
5+
~ Copyright 2016-Present Datadog, Inc.
6+
-->
7+
8+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
9+
xmlns:tools="http://schemas.android.com/tools">
10+
11+
<uses-feature android:name="android.software.leanback" android:required="true" />
12+
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
13+
14+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
15+
16+
<application
17+
android:allowBackup="true"
18+
android:name=".TvSampleApplication"
19+
android:icon="@mipmap/ic_launcher"
20+
android:label="@string/app_name"
21+
android:banner="@drawable/banner"
22+
android:roundIcon="@mipmap/ic_launcher_round"
23+
android:supportsRtl="true"
24+
android:theme="@style/SampleTv.Theme">
25+
26+
<activity
27+
android:name=".HomeActivity"
28+
android:exported="true"
29+
android:label="@string/app_name"
30+
android:resizeableActivity="true"
31+
tools:targetApi="33">
32+
33+
<intent-filter>
34+
<action android:name="android.intent.action.MAIN" />
35+
36+
<category android:name="android.intent.category.LAUNCHER" />
37+
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
38+
</intent-filter>
39+
</activity>
40+
41+
<activity android:name=".PlayerActivity" />
42+
</application>
43+
44+
</manifest>
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.tv.sample
8+
9+
import android.annotation.SuppressLint
10+
import android.os.Build
11+
import android.view.LayoutInflater
12+
import android.view.View
13+
import android.view.ViewGroup
14+
import android.widget.TextView
15+
import androidx.recyclerview.widget.RecyclerView
16+
import com.datadog.android.tv.sample.model.Episode
17+
18+
internal class EpisodeRecyclerView private constructor() {
19+
20+
class Adapter(
21+
val onEpisodeSelected: (Episode?, Int) -> Unit
22+
) : RecyclerView.Adapter<ViewHolder>() {
23+
24+
private val data: MutableList<Episode> = mutableListOf()
25+
26+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
27+
val itemView = LayoutInflater.from(parent.context).inflate(
28+
R.layout.episode_item,
29+
parent,
30+
false
31+
)
32+
return ViewHolder(itemView, onEpisodeSelected)
33+
}
34+
35+
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
36+
holder.render(data[position])
37+
}
38+
39+
override fun getItemCount(): Int {
40+
return data.size
41+
}
42+
43+
@SuppressLint("NotifyDataSetChanged")
44+
internal fun updateData(newData: List<Episode>) {
45+
data.clear()
46+
data.addAll(newData)
47+
notifyDataSetChanged()
48+
onEpisodeSelected(null, -1)
49+
}
50+
}
51+
52+
class ViewHolder(
53+
itemView: View,
54+
onEpisodeSelected: (Episode, Int) -> Unit
55+
) : RecyclerView.ViewHolder(itemView) {
56+
57+
lateinit var episode: Episode
58+
59+
val textView = itemView.findViewById<TextView>(R.id.title)
60+
61+
init {
62+
itemView.setOnClickListener {
63+
updateEpisodeHighlight(true)
64+
onEpisodeSelected(episode, 0)
65+
}
66+
itemView.setOnFocusChangeListener { view, b ->
67+
updateEpisodeHighlight(b)
68+
onEpisodeSelected(episode, 0)
69+
}
70+
}
71+
72+
private fun updateEpisodeHighlight(selected: Boolean) {
73+
val colorRes = if (selected) R.color.dd_purple_200 else R.color.text_default
74+
val textColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
75+
textView.resources.getColor(colorRes, null)
76+
} else {
77+
@Suppress("DEPRECATION")
78+
textView.resources.getColor(colorRes)
79+
}
80+
textView.setTextColor(textColor)
81+
}
82+
83+
fun render(episode: Episode) {
84+
this.episode = episode
85+
textView.text = episode.title
86+
}
87+
}
88+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.tv.sample
8+
9+
import android.content.ComponentName
10+
import android.content.Intent
11+
import android.net.Uri
12+
import android.os.Bundle
13+
import android.view.View
14+
import android.widget.TextView
15+
import androidx.appcompat.app.AppCompatActivity
16+
import androidx.recyclerview.widget.LinearLayoutManager
17+
import androidx.recyclerview.widget.RecyclerView
18+
import com.datadog.android.rum.resource.asRumResource
19+
import com.datadog.android.tv.sample.model.Episode
20+
import com.datadog.android.tv.sample.model.EpisodeList
21+
import com.google.gson.Gson
22+
23+
/**
24+
* Main activity when launching the app, shows the list of available episodes.
25+
*/
26+
class HomeActivity : AppCompatActivity() {
27+
28+
private val adapter = EpisodeRecyclerView.Adapter { episode, view ->
29+
onEpisodeSelected(episode)
30+
}
31+
32+
private lateinit var episodesRecyclerView: RecyclerView
33+
private lateinit var episodeTitle: TextView
34+
private lateinit var episodeDescription: TextView
35+
private lateinit var episodeSpeakers: TextView
36+
private lateinit var playView: View
37+
38+
override fun onCreate(savedInstanceState: Bundle?) {
39+
super.onCreate(savedInstanceState)
40+
41+
setContentView(R.layout.activity_tv)
42+
episodesRecyclerView = findViewById(R.id.episodes)
43+
episodeTitle = findViewById(R.id.episode_title)
44+
episodeDescription = findViewById(R.id.episode_description)
45+
episodeSpeakers = findViewById(R.id.episode_speakers)
46+
playView = findViewById(R.id.play)
47+
48+
episodesRecyclerView.layoutManager = LinearLayoutManager(this)
49+
episodesRecyclerView.adapter = adapter
50+
}
51+
52+
override fun onResume() {
53+
super.onResume()
54+
Thread {
55+
val rawReader = resources.openRawResource(R.raw.episodes)
56+
.asRumResource("https://api.example.com/episodes")
57+
.reader()
58+
val episodeList = Gson().fromJson(rawReader, EpisodeList::class.java)
59+
runOnUiThread {
60+
adapter.updateData(episodeList.episodes.sortedBy { it.recordDate })
61+
}
62+
}.start()
63+
}
64+
65+
private fun onEpisodeSelected(episode: Episode?) {
66+
episodeTitle.text = episode?.title.orEmpty()
67+
episodeDescription.text = episode?.description?.joinToString("\n").orEmpty()
68+
episodeSpeakers.text = episode?.speakers?.joinToString(", ").orEmpty()
69+
70+
playView.isEnabled = !(episode?.video.isNullOrBlank())
71+
72+
playView.setOnClickListener {
73+
if (episode != null) {
74+
val intent = Intent()
75+
intent.component = ComponentName(this, PlayerActivity::class.java)
76+
intent.data = Uri.parse(episode.video)
77+
startActivity(intent)
78+
}
79+
}
80+
}
81+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2016-Present Datadog, Inc.
5+
*/
6+
7+
package com.datadog.android.tv.sample
8+
9+
import android.os.Bundle
10+
import android.widget.Toast
11+
import androidx.appcompat.app.AppCompatActivity
12+
import com.datadog.android.rum.GlobalRumMonitor
13+
import com.datadog.android.rum.RumErrorSource
14+
import com.google.android.exoplayer2.ExoPlayer
15+
import com.google.android.exoplayer2.MediaItem
16+
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
17+
import com.google.android.exoplayer2.source.ProgressiveMediaSource
18+
import com.google.android.exoplayer2.ui.StyledPlayerView
19+
import org.schabi.newpipe.extractor.services.youtube.YoutubeService
20+
import org.schabi.newpipe.extractor.stream.StreamExtractor
21+
import timber.log.Timber
22+
import kotlin.random.Random
23+
24+
/**
25+
* An activity playing a video stream from a Youtube URL.
26+
*
27+
* This activity looks for the URL in the `Intent`'s `data` property.
28+
*/
29+
class PlayerActivity : AppCompatActivity() {
30+
31+
private lateinit var videoPlayerView: StyledPlayerView
32+
private lateinit var videoPlayer: ExoPlayer
33+
34+
override fun onCreate(savedInstanceState: Bundle?) {
35+
super.onCreate(savedInstanceState)
36+
setContentView(R.layout.activity_player)
37+
38+
val okHttpClient = (applicationContext as TvSampleApplication).okHttpClient
39+
videoPlayerView = findViewById(R.id.video_player_view)
40+
videoPlayer = ExoPlayer.Builder(this)
41+
.setMediaSourceFactory(ProgressiveMediaSource.Factory(OkHttpDataSource.Factory(okHttpClient)))
42+
.build()
43+
videoPlayer.addListener(RumPlayerListener())
44+
}
45+
46+
override fun onResume() {
47+
super.onResume()
48+
val intentUri = intent.data?.toString()
49+
Toast.makeText(this, "Playing $intentUri", Toast.LENGTH_SHORT).show()
50+
51+
if (intentUri == null) {
52+
finish()
53+
return
54+
}
55+
56+
videoPlayerView.player = videoPlayer
57+
58+
Thread {
59+
loadAndPlayVideo(intentUri)
60+
}.start()
61+
}
62+
63+
@Suppress("TooGenericExceptionCaught")
64+
private fun loadAndPlayVideo(intentUri: String) {
65+
try {
66+
val extractor = extractStreamingInformation(intentUri)
67+
val videoStreams = extractor.videoStreams
68+
val streamIdx = Random.Default.nextInt(videoStreams.size)
69+
val videoStream = videoStreams[streamIdx]
70+
val mediaItem = MediaItem.Builder()
71+
.setUri(videoStream.getUrl())
72+
.setMediaId(intentUri)
73+
.build()
74+
runOnUiThread {
75+
videoPlayer.setMediaItem(mediaItem)
76+
videoPlayer.playWhenReady = true
77+
videoPlayer.prepare()
78+
}
79+
} catch (t: Throwable) {
80+
GlobalRumMonitor.get().addError(
81+
"Unable to stream video",
82+
RumErrorSource.SOURCE,
83+
t,
84+
emptyMap()
85+
)
86+
}
87+
}
88+
89+
private fun extractStreamingInformation(intentUri: String?): StreamExtractor {
90+
val streamingService = YoutubeService(1)
91+
val extractor = streamingService.getStreamExtractor(intentUri)
92+
Timber.i("Fetching stream for $intentUri")
93+
extractor.fetchPage()
94+
return extractor
95+
}
96+
97+
override fun onPause() {
98+
super.onPause()
99+
videoPlayer.stop()
100+
videoPlayer.release()
101+
}
102+
}

0 commit comments

Comments
 (0)