Overview
Android TV and Google TV applications use the Permutive Android SDK . The same SDK that powers mobile Android applications fully supports Android TV, providing identical APIs for tracking, identity management, and ad targeting.
Same SDK, Same APIs: The Permutive Android SDK works seamlessly on Android TV. All features documented in the Android SDK are available on Android TV.
Requirements
Requirement Version Android API 21+ (Android 5.0 Lollipop) Compile SDK 34+ Java 8+ (JVM target 1.8) Kotlin 1.6+ (if using Kotlin)
Installation
Add the Permutive SDK to your build.gradle:
dependencies {
// Core SDK (required)
implementation ( "com.permutive.android:core:1.10.0" )
// Google Ads integration (optional)
implementation ( "com.permutive.android:google-ads:2.2.0" )
}
Initialization
Initialize the SDK in your Application class:
import android.app.Application
import com.permutive.android.Permutive
class MyTVApplication : Application () {
lateinit var permutive: Permutive
private set
override fun onCreate () {
super . onCreate ()
permutive = Permutive. Builder (
applicationContext = this ,
apiKey = "<your api key>" ,
workspaceId = "<your workspace id>"
). build ()
}
}
For detailed initialization options, see Android Initialization .
Video Tracking
Video tracking on Android TV uses the MediaTracker API. This is the primary use case for CTV applications.
import com.permutive.android.MediaTracker
class VideoActivity : AppCompatActivity () {
private val permutive by lazy {
(application as MyTVApplication).permutive
}
private lateinit var videoTracker: MediaTracker
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
videoTracker = permutive. trackVideoView (
durationMilliseconds = 3600000 , // 1 hour
videoProperties = MediaTracker. VideoProperties (
title = "Show Title - S1E1" ,
genre = listOf ( "Drama" , "Thriller" ),
contentType = listOf ( "Series" , "Episode" ),
seasonNumber = 1 ,
episodeNumber = 1 ,
runtime = 3600
),
pageProperties = MediaTracker. PageProperties (
title = "Now Playing" ,
url = Uri. parse ( "https://example.com/shows/title/s1e1" )
)
)
}
override fun onDestroy () {
super . onDestroy ()
videoTracker. stop () // Important: Always stop to send VideoComplete
}
}
Integrating with ExoPlayer
Android TV applications typically use ExoPlayer (now part of Media3) for video playback:
import com.permutive.android.MediaTracker
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.common.Player
class VideoPlayerActivity : AppCompatActivity () {
private val permutive by lazy {
(application as MyTVApplication).permutive
}
private lateinit var videoTracker: MediaTracker
private lateinit var exoPlayer: ExoPlayer
override fun onCreate (savedInstanceState: Bundle ?) {
super . onCreate (savedInstanceState)
setContentView (R.layout.activity_video_player)
setupVideoTracker ()
setupExoPlayer ()
}
private fun setupVideoTracker () {
videoTracker = permutive. trackVideoView (
durationMilliseconds = videoDurationMs,
videoProperties = MediaTracker. VideoProperties (
title = "Video Title" ,
genre = listOf ( "Documentary" ),
contentType = listOf ( "Feature" )
)
)
}
private fun setupExoPlayer () {
exoPlayer = ExoPlayer. Builder ( this ). build ()
exoPlayer. addListener ( object : Player . Listener {
override fun onPlaybackStateChanged (state: Int ) {
when (state) {
Player.STATE_READY -> {
if (exoPlayer.isPlaying) {
videoTracker. play ()
}
}
Player.STATE_BUFFERING -> {
videoTracker. pause ()
}
}
}
override fun onIsPlayingChanged (isPlaying: Boolean ) {
if (isPlaying) {
videoTracker. play ()
} else {
videoTracker. pause ()
}
}
})
// Track seeking
exoPlayer. addAnalyticsListener ( object : AnalyticsListener {
override fun onPositionDiscontinuity (
eventTime: AnalyticsListener .EventTime,
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
videoTracker. play (exoPlayer.currentPosition)
}
}
})
}
override fun onPause () {
super . onPause ()
videoTracker. pause ()
exoPlayer. pause ()
}
override fun onResume () {
super . onResume ()
if (exoPlayer.isPlaying) {
videoTracker. play ()
}
}
override fun onDestroy () {
super . onDestroy ()
videoTracker. stop ()
exoPlayer. release ()
}
}
Method Description Events Tracked play()Start/resume tracking Videoview (first call)play(positionMs)Start at specific position (seeking) - pause()Pause tracking (buffering, paused) - stop()End tracking permanently VideoComplete
Always call stop(): Failing to call stop() when the video ends or the user exits will prevent VideoComplete events from being tracked.
For complete MediaTracker documentation, see Android Video Tracking .
Video Properties
The SDK provides standard video properties for consistent tracking:
MediaTracker. VideoProperties (
title = "Video Title" ,
genre = listOf ( "Action" , "Thriller" ),
contentType = listOf ( "Movie" , "Feature" ),
ageRating = "PG-13" ,
runtime = 7200 , // Runtime in seconds
country = "US" ,
originalLanguage = "en" ,
audioLanguage = "en" ,
areSubtitlesEnabled = true ,
subtitlesLanguage = "en" ,
seasonNumber = 1 ,
episodeNumber = 5 ,
consecutiveEpisodes = 3 ,
iabCategories = listOf ( "IAB1" , "IAB2" )
)
Video Ad Tracking
Track video advertisements within your content:
// When ad starts
val adTracker = videoTracker. trackAdView (
durationMs = 30000 , // 30 second ad
adProperties = AdTracker. AdProperties (
title = "Product Ad" ,
durationInSeconds = 30 ,
campaignId = "campaign_123" ,
creativeId = "creative_456"
)
)
adTracker. play ()
// When ad completes
adTracker. completion ()
See Android Video Ad Tracking for details.
Identity Management
Set user identities for cross-device tracking:
// userId: Your application's user identifier
// hashedEmail: SHA256 hash of the user's email address
permutive. setIdentity (
listOf (
Alias. create ( "user_id" , userId, priority = 0 ),
Alias. create ( "email_sha256" , hashedEmail, priority = 1 )
)
)
See Android Identity Management for details.
Cohorts and Targeting
Accessing Cohorts
// Get all cohorts
val cohorts = permutive.cohorts
// Get activations for a specific ad platform
val gamActivations = permutive. activations ( "gam" )
Google Ad Manager Integration
import com.permutive.android.googleads.addPermutiveTargeting
val adRequest = AdManagerAdRequest. Builder ()
. addPermutiveTargeting (permutive)
. build ()
adView. loadAd (adRequest)
See Android Google Ad Manager for complete integration details.
Android TV-Specific Considerations
Android TV uses the Leanback library for its 10-foot UI. Ensure your tracking implementation doesn’t interfere with Leanback components: class VideoFragment : VideoSupportFragment () {
private lateinit var videoTracker: MediaTracker
override fun onStart () {
super . onStart ()
// VideoSupportFragment handles player lifecycle
// Just sync tracking with player state
}
}
Android TV uses D-pad navigation. Tracking works independently of navigation, but ensure you handle the back button appropriately: override fun onBackPressed () {
// Stop tracking before navigating away
videoTracker. stop ()
super . onBackPressed ()
}
If your app supports PiP mode, continue tracking during PiP: override fun onPictureInPictureModeChanged (
isInPictureInPictureMode: Boolean ,
newConfig: Configuration
) {
super . onPictureInPictureModeChanged (isInPictureInPictureMode, newConfig)
// Continue tracking - no changes needed
// MediaTracker tracks independently of UI mode
}
Android TV supports AAID. Use the AAID provider for automatic advertising ID tracking: dependencies {
implementation ( "com.permutive.android:aaid-provider:1.10.0" )
}
val permutive = Permutive. Builder ( this , apiKey, workspaceId)
. enableAaidProvider ()
. build ()
See AAID Provider for details.
Android TV apps may continue video playback when the home button is pressed. Handle this appropriately: override fun onStop () {
super . onStop ()
if (!isChangingConfigurations) {
// User left the app
if (exoPlayer.isPlaying) {
// Playback continues in background
// Keep tracking active
} else {
videoTracker. pause ()
}
}
}
Best Practices
Initialize SDK early in Application.onCreate()
Create MediaTracker when video is ready to play
Call stop() when video ends or user exits
Sync play()/pause() with actual player state
Handle buffering states with pause()
Include video metadata for richer cohorts
Use Leanback-compatible patterns
Don’t create multiple MediaTrackers simultaneously
Don’t forget to call stop() when done
Don’t skip the duration parameter
Don’t block the main thread with SDK calls
Don’t send PII in event properties
Don’t interfere with Leanback focus handling
Troubleshooting
Problem: Permutive instance is null or initialization fails.Solutions:
Verify API key and workspace ID are correct
Ensure initialization happens in Application.onCreate()
Check for initialization errors in Logcat
Enable debug mode: permutive.setDeveloperMode(true)
Events not appearing in dashboard
Problem: Video events don’t show in Permutive dashboard.Solutions:
Verify MediaTracker was created successfully
Ensure play() is called when video starts
Ensure stop() is called when video ends
Wait 5-10 minutes for events to process
Check Logcat for “Accepted: 1 / 1” messages
Engagement time incorrect
Problem: Engagement metrics don’t match expected values.Solutions:
Sync play()/pause() calls with actual player state
Call pause() during buffering
Provide accurate duration when creating MediaTracker
VideoComplete not tracked
Problem: VideoComplete events not appearing.Solutions:
Ensure stop() is called in onDestroy()
Handle back button to call stop() before navigation
Verify app isn’t killed before event sends