How to Share

The EmbedClient sample module in the book’s sample project, along with the EmbedServer module, demonstrate how to work with SurfaceControlViewHost.

In the terminology being used in this chapter:

Client Setup

The first thing that the client needs is a SurfaceView, which will be where the embedded UI will be rendered. EmbedClient has an activity_main layout resource that consists of a SurfaceView and a Button:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:padding="8dp"
  tools:context=".MainActivity">

  <SurfaceView
    android:id="@+id/surface"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/connect"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <Button
    android:id="@+id/connect"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    android:text="@string/connect"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
EmbedClient, As Initially Launched
EmbedClient, As Initially Launched

Android 11 adds a getHostToken() method to SurfaceView, returning an IBinder that represents the SurfaceView. The client needs to get that “host token”, along with the ID of the Display used for that SurfaceView, and the dimensions of that SurfaceView over to the server app. MainActivity delegates this to a MainMotor viewmodel, by calling a bind() function when the user clicks that “Connect to Server” button:

package com.commonsware.android.r.embed.client

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.observe
import com.commonsware.android.r.embed.client.databinding.ActivityMainBinding
import org.koin.androidx.viewmodel.ext.android.viewModel

class MainActivity : AppCompatActivity() {
  private val motor: MainMotor by viewModel()

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = ActivityMainBinding.inflate(layoutInflater)

    setContentView(binding.root)

    binding.surface.setZOrderOnTop(true)

    motor.surfacePackage.observe(this) {
      binding.surface.setChildSurfacePackage(it)
    }

    binding.connect.setOnClickListener {
      motor.bind(
        binding.surface.hostToken,
        binding.surface.display.displayId,
        binding.surface.width,
        binding.surface.height
      )
    }
  }
}

IBinder can go into a Bundle, and the Int values for the display ID, width, and height can all be transferred easily between processes. We want to ensure that the server process runs as long as our client needs it, so EmbedClient and EmbedServer use the bound service pattern, with EmbedServer hosting the service. In particular, EmbedServer will expose a Messenger as its Binder, so EmbedClient can send a Message to it with the IBinder and Int values. So, MainMotor has a bindToService() function that uses suspendCoroutine to make the asynchronous act of binding to a service and getting the Messenger appear synchronous:

  private suspend fun bindToService(): MessengerConnection {
    return withContext(Dispatchers.Default) {
      suspendCoroutine<MessengerConnection> { continuation ->
        context.bindService(
          Intent().setClassName(
            "com.commonsware.android.r.embed.server",
            "com.commonsware.android.r.embed.server.ViewService"
          ),
          MessengerConnection { if (isActive) continuation.resume(it) },
          Context.BIND_AUTO_CREATE
        )
      }
    }
  }
}

private class MessengerConnection(private val onConnected: (MessengerConnection) -> Unit) :
  ServiceConnection {
  var messenger: Messenger? = null

  override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
    messenger = Messenger(binder)
    onConnected(this)
  }

  override fun onServiceDisconnected(name: ComponentName?) {
    messenger = null
  }
}

The bind() function that MainActivity calls then binds to the service and sends a Message with our four pieces of data:

  fun bind(
    hostToken: IBinder?,
    displayId: Int,
    width: Int,
    height: Int
  ) {
    viewModelScope.launch {
      conn = bindToService()

      conn?.messenger?.send(Message.obtain().apply {
        data = bundleOf(
          KEY_HOST_TOKEN to hostToken,
          KEY_DISPLAY_ID to displayId,
          KEY_WIDTH to width,
          KEY_HEIGHT to height
        )
        replyTo = messenger
      })
    }
  }

Setting up the Bundle is a bit clunky, because the bundleOf() implementation in androidx.core:core-ktx:1.2.0 does not support IBinder, the data type of our “host token”. So, we have to add that via a separate call to putBinder().

Also note that our Message includes another Messenger in the replyTo property. This Messenger will be used by the server to send data back to the client. We will look more at that part of the process later in this chapter.

Also, as was seen in an earlier chapter, we need to whitelist the server app in order to be able to bind to it:

  <queries>
    <package android:name="com.commonsware.android.r.embed.server" />
  </queries>

Otherwise, any bindService() call will fail, even with a valid Intent.

Server Setup

The job of the server is to set up the SurfaceControlViewHost and the UI to be displayed in the client.

All of that is handled by the ViewService being bound to by the client. While the app has an activity, that is simply for convenience when launching this sample from the IDE — the activity plays no role in the UI being served up.

The UI in question consists of a really big Button, plus a TextView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  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/white"
  android:padding="8dp">

  <Button
    android:id="@+id/button"
    android:layout_width="0dp"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/time"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

  <TextView
    android:id="@+id/time"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

In onCreate() of ViewService, we use view binding to inflate() that layout and configure the widgets:

class ViewService : Service() {
  private lateinit var messenger: Messenger
  private val handlerThread = HandlerThread("ViewService")
  private lateinit var binding: EmbeddedBinding

  override fun onCreate() {
    super.onCreate()

    handlerThread.start()

    binding = EmbeddedBinding.inflate(LayoutInflater.from(this))
    var count = 0

    binding.button.text = getString(R.string.caption, count)
    binding.button.setOnClickListener {
      Log.d("ViewService", "button clicked")
      count += 1
      binding.button.text = getString(R.string.caption, count)
    }
    binding.time.text = Date().toString()

    messenger = Messenger(ViewHandler(this, binding, handlerThread.looper))

    Log.d("ViewService", "onCreate() finished")
  }

  override fun onBind(p0: Intent?): IBinder = messenger.binder
}

The Message from EmbedClient will be received by handleMessage() on the ViewHandler implementation of Handler. Our job is to process that message and, for the first message, set up the SurfaceControlViewHost:

private class ViewHandler(
  private val context: Context,
  private val binding: EmbeddedBinding,
  looper: Looper
) : Handler(looper) {
  private var host: SurfaceControlViewHost? = null

  override fun handleMessage(msg: Message) {
    Log.d("ViewService", "handleMessage() called")

    msg.data.apply {
      if (host == null) {
        val hostToken = getBinder(KEY_HOST_TOKEN)
        val displayId = getInt(KEY_DISPLAY_ID)
        val width = getInt(KEY_WIDTH)
        val height = getInt(KEY_HEIGHT)
        val display = context.getSystemService(DisplayManager::class.java)
          .getDisplay(displayId)

        host = SurfaceControlViewHost(context, display, hostToken).apply {
          setView(binding.root, width, height)

          val pkg = surfacePackage

          msg.replyTo.send(Message.obtain().apply {
            data = bundleOf(KEY_SURFACE_PACKAGE to pkg)
          })
        }
      } else {
        binding.time.text = Date().toString()
      }
    }
  }
}

If we have not set up the host previously, we grab the values out of the Message and obtain the Display object for our display ID. We then:

If, on the other hand, we already have the host set up from before, we just update the TextView to show the now-current Date.

Client Completion

The replyTo Messenger that we attached to the outbound message is set up in the init block of MainMotor:

class MainMotor(private val context: Context) : ViewModel() {
  private var conn: MessengerConnection? = null
  private val _surfacePackage =
    MutableLiveData<SurfaceControlViewHost.SurfacePackage>()
  val surfacePackage: LiveData<SurfaceControlViewHost.SurfacePackage> =
    _surfacePackage
  private val handlerThread = HandlerThread("EmbedClient")
  private val handler: Handler
  private val messenger: Messenger

  init {
    handlerThread.start()
    handler = PackageHandler(handlerThread.looper) {
      _surfacePackage.postValue(it)
    }
    messenger = Messenger(handler)
  }

The PackageHandler simply calls the supplied callback upon receipt of a Message, extracting out the SurfacePackage sent by the server:

private class PackageHandler(
  looper: Looper,
  private val onPackageReceipt: (SurfaceControlViewHost.SurfacePackage) -> Unit
) : Handler(looper) {
  override fun handleMessage(msg: Message) {
    val pkg = msg.data.getParcelable<SurfaceControlViewHost.SurfacePackage>(
      KEY_SURFACE_PACKAGE
    )

    pkg?.let { onPackageReceipt(it) }
  }
}

The SurfacePackage is supplied to MainActivity via LiveData, and MainActivity calls setChildSurfacePackage() on the SurfaceView to attach it:

    motor.surfacePackage.observe(this) {
      binding.surface.setChildSurfacePackage(it)
    }

The Results

If you install both apps, then launch EmbedClient and click the “Connect to Server” button, you will see the EmbedServer-supplied UI in what had been the big open area of the SurfaceView:

EmbedClient, After Connecting to Server
EmbedClient, After Connecting to Server

If you click the “Connect to Server” button again, the TextView text will show the now-current date, illustrating that the connection between client and server is live. All the server is doing on these subsequent button clicks is updating the text in the TextView — it is not doing anything else to “push” a new rendition of the UI to the client. That is handled by SurfaceControlViewHost and the underlying shared Surface.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.