Android
Integration for phone and tablet apps. The shared flow is described under WebView: shared flow; this page has ready-to-use Android code.
What you'll need
- A WebView with JavaScript enabled.
- A video player — the example uses Media3 ExoPlayer.
- The SDK page address for the
androidplatform:https://cdn.adsdk.ru/android/v3/.
Screen layout
xml
<!-- res/layout/activity_ad.xml -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black">
<androidx.media3.ui.PlayerView
android:id="@+id/adPlayer"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:use_controller="false" />
<!-- On top: receives taps on the SDK buttons -->
<WebView
android:id="@+id/adWebView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>Full example
Set up the WebView and open the SDK page
kotlin
class AdActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var player: ExoPlayer
private val main = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ad)
player = ExoPlayer.Builder(this).build()
findViewById<PlayerView>(R.id.adPlayer).player = player
player.addListener(playerListener)
webView = findViewById(R.id.adWebView)
webView.setBackgroundColor(Color.TRANSPARENT)
webView.settings.javaScriptEnabled = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.addJavascriptInterface(AdEvents(), "Android")
webView.loadUrl(adPageUrl(vastUrl = intent.getStringExtra(EXTRA_VAST_URL)!!))
}
private fun adPageUrl(vastUrl: String): String =
Uri.parse("https://cdn.adsdk.ru/android/v3/").buildUpon()
.appendQueryParameter("url", vastUrl)
.appendQueryParameter("lang", "ru")
.appendQueryParameter("controls", """["ad-skip-btn","ad-click-btn","ad-more-menu"]""")
.build()
.toString()
/** Executes an SDK method. evaluateJavascript can only be called from the main thread. */
private fun sdk(call: String) = main.post {
webView.evaluateJavascript("window.myAdController.$call", null)
}
}Receive events
The SDK calls methods on the Android object. Declare the ones you need — the SDK skips the rest.
Threads
@JavascriptInterface methods are called on the WebView's background thread. Move anything that touches the player or the UI to the main thread — in the example, that's main.post { }.
kotlin
private inner class AdEvents {
private var isVpaid = false
// Arrives before AdLoaded: the creative type tells you who will play it
@JavascriptInterface
fun AdCreativeLoaded(
srcUrl: String, type: String, bannerId: String, isNoBanner: Boolean,
isPod: Boolean, podLength: Int, isLast: Boolean, erid: String, durationMsec: Int,
) {
isVpaid = type == "application/javascript"
}
@JavascriptInterface
fun AdLoaded(mediaFileUrl: String) = main.post {
if (isVpaid) {
sdk("playAd()") // VPAID plays inside the WebView
} else {
player.setMediaItem(MediaItem.fromUri(mediaFileUrl))
player.prepare()
player.playWhenReady = true
}
}
@JavascriptInterface
fun AdSkipped(adNumber: Int) = main.post { player.stop() }
@JavascriptInterface
fun AdClick(url: String, adNumber: Int) = main.post {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
}
@JavascriptInterface
fun AdError(code: String, message: String, type: String,
isNoBanner: Boolean, isLast: Boolean, adNumber: Int) = main.post { close() }
@JavascriptInterface
fun AdDestroyed(isLast: Boolean) = main.post {
player.stop()
if (isLast) close()
}
}Report playback to the SDK
kotlin
private val progress = object : Runnable {
override fun run() {
if (player.duration > 0) {
sdk("timeupdateAd(${player.currentPosition / 1000.0}, ${player.duration / 1000.0})")
}
main.postDelayed(this, PROGRESS_INTERVAL_MS)
}
}
private val playerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
sdk("playAd()") // both the first start and resuming after a pause
main.post(progress)
} else {
main.removeCallbacks(progress)
if (!player.playWhenReady) sdk("pauseAd()") // user-initiated pause, not buffering
}
}
override fun onPlaybackStateChanged(state: Int) {
if (state == Player.STATE_ENDED) {
val durationSec = player.duration / 1000.0
sdk("timeupdateAd($durationSec, $durationSec)") // the SDK will send AdComplete
}
}
}
private companion object {
const val PROGRESS_INTERVAL_MS = 250L
const val EXTRA_VAST_URL = "vast_url"
}Finish the ad
kotlin
private fun close() {
main.removeCallbacks(progress)
finish()
}
override fun onPause() {
super.onPause()
player.pause() // the listener will send pauseAd()
}
override fun onDestroy() {
webView.removeJavascriptInterface("Android")
webView.destroy()
player.release()
super.onDestroy()
}Event signatures
The WebView looks up an interface method by name and argument count. A method with a different argument count won't be called. See the full list under Events → Android.
| Event | Method |
|---|---|
| Ad loaded | AdCreativeLoaded(9 arguments), then AdLoaded(mediaFileUrl) |
| Playback started | AdStarted(adNumber) |
| Quartiles | AdFirstQuartile(adNumber), AdMidpoint(adNumber), AdThirdQuartile(adNumber) |
| Became skippable | AdSkippableStateChanged(status) |
| Ad watched to the end | AdComplete(adNumber) |
| Playback finished | AdDestroyed(isLast) |
| Error | AdError(code, message, type, isNoBanner, isLast, adNumber) |
Common issues
| Symptom | Cause |
|---|---|
| SDK buttons are visible, no ad | The WebView background isn't transparent, or the WebView sits below the player. |
No AdStarted | playAd() wasn't called after the player started. |
No quartiles or AdComplete | timeupdateAd() isn't being called. |
| An event doesn't arrive | The method's argument count doesn't match the signature. |
| The app crashes in the event handler | The player was touched off the main thread. |
More in Troubleshooting.