Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

Lenguaje de definición de la interfaz de Android (AIDL)

El Lenguaje de definición de la interfaz de Android (AIDL) es similar a otros IDL con los que posiblemente hayas trabajo. Te permite definir la interfaz de programación que el cliente y el servicio acuerdan utilizar para establecer comunicación entre procesos (IPC). Por lo general, en Android, un proceso no puede acceder a la memoria de otro proceso. Por esto, para comunicarse, tienen que descomponer sus objetos en primitivas que el sistema operativo pueda comprender y tienen que ordenar los objetos entre esos límites para ti. Como es tedioso escribir el código para ese ordenamiento, Android se encarga de eso por ti con AIDL.

Nota: Solo es necesario usar AIDL si permites que los clientes de distintas aplicaciones tengan acceso a tu servicio para IPC y deseas controlar varios subprocesos en tu servicio. Si no tienes que establecer una IPC concurrente entre diferentes aplicaciones, debes crear la interfaz implementando un Binder o, si deseas realizar una IPC, pero no necesitas manejar varios subprocesos, implementa la interfaz con un Messenger. Sin importar cuál utilices, asegúrate de comprender los servicios vinculados antes de implementar un AIDL.

Antes de comenzar a diseñar tu interfaz de AIDL, ten en cuenta que las llamadas a una interfaz de AIDL son llamadas de funciones directas. No debes realizar suposiciones sobre el subproceso en el que se produce la llamada. Lo que sucede es diferente dependiendo de si la llamada proviene de un subproceso del proceso local o de un proceso remoto. En particular:

  • Las llamadas realizadas desde el proceso local se ejecutan en el mismo subproceso que realiza la llamada. Si es el subproceso de la IU principal, ese subproceso continúa ejecutándose en la interfaz de AIDL. Si es otro subproceso, ese es el que ejecuta el código en el servicio. Por lo tanto, si algún subproceso local accede al servicio, puedes controlar completamente qué subprocesos se están ejecutando en él (pero, si es así, no debes usar el AIDL; en cambio, debes crear la interfaz implementado un Binder).
  • Las llamadas realizadas desde un proceso remoto se despachan desde un grupo de subprocesos que la plataforma mantiene dentro de tu proceso. Debes estar preparado para llamadas entrantes desde subprocesos desconocidos y para que se produzcan varias llamadas al mismo tiempo. En otras palabras, una implementación de una interfaz de AIDL debe ser completamente segura para subprocesos. Las llamadas que se realicen desde un subproceso del mismo objeto remoto aparecerán en orden en el extremo del receptor.
  • La palabra clave oneway modifica el comportamiento de las llamadas remotas. Cuando se hace una llamada remota, esta no produce un bloqueo; simplemente, envía los datos de la transacción y se muestra de inmediato. La implementación de la interfaz finalmente recibe el envío como una llamada normal del grupo de subprocesos Binder en forma de llamada remota normal. Si oneway se usa con una llamada local, no se produce ningún efecto y la llamada sigue siendo sincrónica.

Cómo definir una interfaz de AIDL

Debes definir la interfaz de AIDL en un archivo .aidl con sintaxis del lenguaje de programación Java y guardarla en el código fuente (en el directorio src/) de la aplicación que aloja el servicio y cualquier otra aplicación que cree un enlace con el servicio.

Cuando creas cada aplicación que contiene el archivo .aidl, las herramientas del SDK de Android generan una interfaz IBinder basada en el archivo .aidl y la guardan en el directorio gen/ del proyecto. El servicio debe implementar la interfaz IBinder según corresponda. Las aplicaciones cliente pueden enlazarse con el servicio y llamar a los métodos desde la interfaz IBinder para establecer IPC.

Para crear un servicio enlazado con AIDL, sigue estos pasos:

  1. Crear el archivo .aidl

    Este archivo define la interfaz de programación con firmas de métodos.

  2. Implementar la interfaz

    Las herramientas del SDK de Android generan una interfaz con el lenguaje de programación Java según tu archivo .aidl. Esta interfaz tiene una clase abstracta interna denominada Stub que extiende la interfaz Binder e implementa los métodos de la interfaz de AIDL. Debes extender la clase Stub e implementar los métodos.

  3. Exponer la interfaz a los clientes

    Implementa un Service y sobrescribe el onBind() para mostrar tu implementación de la clase Stub.

Advertencia: Los cambios que realices en la interfaz de AIDL después del primer lanzamiento deben tener compatibilidad con versiones anteriores para evitar romper otras aplicaciones que usan tu servicio. Es decir, debido a que tu archivo .aidl se debe copiar a otras aplicaciones para que estas accedan a la interfaz de tu servicio, debes mantener la compatibilidad con la interfaz original.

1. Crea el archivo .aidl

El AIDL usa una sintaxis simple que te permite declarar una interfaz con uno o más métodos que pueden tomar parámetros y devolver valores. Los parámetros y los valores de devolución pueden ser de cualquier tipo, incluidas otras interfaces generadas con AIDL.

Debes desarrollar el archivo .aidl con lenguaje de programación Java. Cada archivo .aidl debe definir una única interfaz y solo requiere la declaración de la interfaz y las firmas de los métodos.

De manera predeterminada, el AIDL admite los siguientes tipos de datos:

  • Todos los tipos de primitivas en el lenguaje de programación Java (como int, long, char, boolean, etc.)
  • String
  • CharSequence
  • List

    Todos los elementos de List deben pertenecer a uno de los tipos de datos admitidos que figuran en esta lista o una de las otras interfaces generadas con AIDL o los tipos parcelable que hayas declarado. También es posible usar un List como clase "genérica" (por ejemplo, List<String>). La clase real concreta que el otro lado recibe es siempre un ArrayList, aunque el método se genera para usar la interfaz List.

  • Map

    Todos los elementos de Map deben pertenecer a uno de los tipos de datos admitidos que figuran en esta lista o una de las otras interfaces generadas con AIDL o los tipos Parcelable que hayas declarado. No se admiten los mapas genéricos (como los que tienen la forma Map<String,Integer>). La clase real concreta que el otro lado recibe es siempre un HashMap, aunque el método se genera para usar la interfaz Map.

Debes incluir una instrucción import para cada tipo adicional que no figure en la lista precedente, incluso si estos están definidos en el mismo paquete que tu interfaz.

Cuando definas la interfaz de tu servicio, ten en cuenta lo siguiente:

  • Los métodos pueden tomar cero o más parámetros, y devolver un valor o un resultado nulo.
  • Todos los parámetros que no sean primitivas requieren una etiqueta direccional que indique la dirección que deben seguir los datos. Por ejemplo, in, out o inout (consulta el ejemplo que aparece a continuación).

    Las primitivas son in de manera predeterminada y no pueden ser de otra manera.

    Advertencia: Debes limitar la dirección a lo que es realmente necesario, ya que el ordenamiento de parámetros es costoso.

  • Todos los comentarios del código que contiene el archivo .aidl se incluyen en la interfaz IBinder generada (a excepción de los comentarios antes de las instrucciones de importación y paquetes).
  • Las constantes "string" e "int" se pueden definir en la interfaz del AIDL. Por ejemplo: const int VERSION = 1;.
  • Las llamadas a los métodos se envían a través de código transact(), que normalmente se basa en un índice de métodos en la interfaz. Debido a que esto dificulta la creación de versiones, podrías asignar manualmente el código de transacción a un método: void method() = 10;.
  • Anota los argumentos anulables o devuelve los tipos usando @nullable.

A continuación, te mostramos un ejemplo de archivo .aidl:

// IRemoteService.aidl
package com.example.android

// Declare any non-default types here with import statements
/** Example service interface */
internal interface IRemoteService {
    /** Request the process ID of this service, to do evil things with it. */
    val pid:Int

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    fun basicTypes(anInt:Int, aLong:Long, aBoolean:Boolean, aFloat:Float,
                 aDouble:Double, aString:String)
}

Guarda tu archivo .aidl en el directorio src/ del proyecto y, cuando crees la aplicación, las herramientas del SDK generarán la interfaz IBinder en el directorio gen/ de tu proyecto. El nombre del archivo generado coincide con el del .aidl, aunque con la extensión .java (por ejemplo, con IRemoteService.aidl, se genera IRemoteService.java).

Si usas Android Studio, la generación incremental genera la clase binder casi inmediatamente. Si no usas Android Studio, la herramienta Gradle generará la clase binder la próxima vez que crees tu aplicación; debes crear tu proyecto con gradle assembleDebug (o gradle assembleRelease) en cuanto termines de escribir el archivo .aidl para que el código pueda realizar vínculos con la clase generada.

2. Implementa la interfaz

Cuando crees tu aplicación, las herramientas del SDK de Android generarán un archivo de interfaz .java, cuyo nombre se derivará del archivo .aidl. La interfaz generada incluye una subclase denominada Stub, que es una implementación abstracta de la interfaz principal (por ejemplo, YourInterface.Stub) y declara todos los métodos del archivo .aidl.

Nota: Stub también identifica algunos métodos de ayuda, principalmente asInterface(), que toma un IBinder (generalmente, el que se pasó a un método de devolución de llamada onServiceConnected() del cliente) y devuelve una instancia de la interfaz del código auxiliar. Para obtener más detalles sobre cómo realizar esta conversión, consulta la sección Cómo llamar a un método de IPC.

Para implementar la interfaz generada del .aidl, extiende la interfaz Binder generada (por ejemplo, YourInterface.Stub) e implementa los métodos heredados del archivo .aidl.

A continuación, se muestra un ejemplo de implementación de una interfaz denominada IRemoteService (definida por el ejemplo IRemoteService.aidl anterior) con una instancia anónima:

Kotlin

private val binder = object : IRemoteService.Stub() {

    override fun getPid(): Int =
            Process.myPid()

    override fun basicTypes(
            anInt: Int,
            aLong: Long,
            aBoolean: Boolean,
            aFloat: Float,
            aDouble: Double,
            aString: String
    ) {
        // Does nothing
    }
}

Java

private final IRemoteService.Stub binder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing
    }
};

Ahora, binder es una instancia de la clase Stub (un Binder), que define la interfaz de RPC del servicio. En el siguiente paso, se expone esta instancia a los clientes para que puedan interactuar con el servicio.

Existen algunas reglas que debes tener en cuenta a la hora de implementar tu interfaz de AIDL.

  • No se garantiza que las llamadas entrantes se ejecuten en el subproceso principal, por lo que debes considerar varios subprocesos desde el comienzo y desarrollar tu servicio adecuadamente a fin de que sea seguro para los subprocesos.
  • De manera predeterminada, las RPC son síncronas. Si sabes que el servicio requiere más de unos pocos milisegundos para completar un pedido, no debes llamarlo desde el subproceso principal de la actividad, ya que podría bloquear la aplicación (Android puede mostrar un cuadro de diálogo que indique que la aplicación no responde); usualmente debes llamarlo desde un subproceso independiente del cliente.
  • No se devolverá al emisor ninguna excepción que produzcas.

3. Expón la interfaz a los clientes

Una vez implementada la interfaz de tu servicio, tienes que exponerla a los clientes para que puedan enlazarse con ella. Para exponer la interfaz de tu servicio, extiende el Service e implementa onBind() para mostrar una instancia de la clase que implemente el Stub generado (como en la sección anterior). A continuación, se muestra un ejemplo del servicio que expone la interfaz IRemoteService de ejemplo a los clientes.

Kotlin

class RemoteService : Service() {

    override fun onCreate() {
        super.onCreate()
    }

    override fun onBind(intent: Intent): IBinder {
        // Return the interface
        return binder
    }


    private val binder = object : IRemoteService.Stub() {
        override fun getPid(): Int {
            return Process.myPid()
        }

        override fun basicTypes(
                anInt: Int,
                aLong: Long,
                aBoolean: Boolean,
                aFloat: Float,
                aDouble: Double,
                aString: String
        ) {
            // Does nothing
        }
    }
}

Java

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface
        return binder;
    }

    private final IRemoteService.Stub binder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing
        }
    };
}

Cuando un cliente (como una actividad) llama a bindService() para conectarse a este servicio, la devolución de llamada onServiceConnected() del cliente recibe la instancia de binder que muestra el método onBind() del servicio.

El cliente también debe tener acceso a la clase de la interfaz. Por lo tanto, si el cliente y el servicio están en aplicaciones separadas, la aplicación del cliente debe tener una copia del archivo .aidl en el directorio src/ (que genera la interfaz android.os.Binder, lo cual le brinda al cliente acceso a los métodos del AIDL).

Cuando el cliente recibe el IBinder en la devolución de llamada onServiceConnected(), debe llamar a YourServiceInterface.Stub.asInterface(service) para convertir el parámetro mostrado al tipo YourServiceInterface. Por ejemplo:

Kotlin

var iRemoteService: IRemoteService? = null

val mConnection = object : ServiceConnection {

    // Called when the connection with the service is established
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        iRemoteService = IRemoteService.Stub.asInterface(service)
    }

    // Called when the connection with the service disconnects unexpectedly
    override fun onServiceDisconnected(className: ComponentName) {
        Log.e(TAG, "Service has unexpectedly disconnected")
        iRemoteService = null
    }
}

Java

IRemoteService iRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        iRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        iRemoteService = null;
    }
};

Para acceder a otros ejemplos de código, consulta la clase RemoteService.java en ApiDemos.

Cómo pasar objetos por IPC

Si deseas enviar una clase de un proceso a otro mediante una interfaz de IPC, puedes hacerlo. Sin embargo, debes asegurarte de que el código de la clase esté disponible para el otro lado del canal de la IPC y que tu clase admita la interfaz Parcelable. Admitir la interfaz Parcelable es importante porque le permite al sistema Android descomponer objetos en primitivas que se pueden ordenar entre los procesos.

Para crear una clase que admita el protocolo Parcelable, debes hacer lo siguiente:

  1. Haz que tu clase implemente la interfaz Parcelable.
  2. Implementa writeToParcel, que toma el estado actual del objeto y lo escribe en un Parcel.
  3. Agrega un campo estático denominado CREATOR a la clase, que es un objeto que implementa la interfaz Parcelable.Creator.
  4. Por último, crea un archivo .aidl que declare tu clase parcelable (como se muestra para el archivo Rect.aidl, a continuación).

    Si usas un proceso de creación personalizado, no agregues el archivo .aidl a tu compilación. Casi al igual que en el caso del archivo de encabezado en lenguaje C, este archivo .aidl no se compila.

El AIDL usa estos métodos y campos en el código que genera para ordenar tus objetos y anular el orden.

Por ejemplo, este es un archivo Rect.aidl para crear una clase Rect que se pueda convertir en parcelable:

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

Este es un ejemplo de cómo la clase Rect implementa el protocolo Parcelable.

Kotlin

import android.os.Parcel
import android.os.Parcelable

class Rect() : Parcelable {
    var left: Int = 0
    var top: Int = 0
    var right: Int = 0
    var bottom: Int = 0

    companion object CREATOR : Parcelable.Creator<Rect> {
        override fun createFromParcel(parcel: Parcel): Rect {
            return Rect(parcel)
        }

        override fun newArray(size: Int): Array<Rect> {
            return Array(size) { Rect() }
        }
    }

    private constructor(inParcel: Parcel) : this() {
        readFromParcel(inParcel)
    }

    override fun writeToParcel(outParcel: Parcel, flags: Int) {
        outParcel.writeInt(left)
        outParcel.writeInt(top)
        outParcel.writeInt(right)
        outParcel.writeInt(bottom)
    }

    private fun readFromParcel(inParcel: Parcel) {
        left = inParcel.readInt()
        top = inParcel.readInt()
        right = inParcel.readInt()
        bottom = inParcel.readInt()
    }

    override fun describeContents(): Int {
        return 0
    }
}

Java

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }

    public int describeContents() {
        return 0;
    }
}

El ordenamiento en la clase Rect es bastante simple. Observa los otros métodos de Parcel para ver los otros tipos de valores que puedes escribir en un Parcel.

Advertencia: No olvides las implicancias de seguridad relacionadas con la recepción de datos de otros procesos. En este caso, Rect lee cuatro números de Parcel, pero depende de ti asegurarte de que estén dentro del intervalo aceptable de valores para lo que el emisor intente hacer. Para obtener más información sobre cómo mantener tu aplicación segura contra software malicioso, consulta Seguridad y permisos.

Métodos con argumentos Bundle que contienen Parcelables

Si tu interfaz aidl incluye métodos que aceptan Bundle como argumentos que se espera que contengan parcelables, asegúrate de configurar el cargador de clase del Bundle llamando a Bundle.setClassLoader(ClassLoader) antes de intentar leer desde el Bundle. De lo contrario, te encontrarás con ClassNotFoundException, aunque el parcelable esté correctamente definido en tu aplicación. Por ejemplo,

si tienes un archivo .aidl:

// IRectInsideBundle.aidl
package com.example.android;

/** Example service interface */
interface IRectInsideBundle {
    /** Rect parcelable is stored in the bundle with key "rect" */
    void saveRect(in Bundle bundle);
}
Como se puede ver a partir de la implementación que aparece a continuación, se estableció ClassLoader de forma explícita en Bundle antes de leer Rect

Kotlin

private val binder = object : IRectInsideBundle.Stub() {
    override fun saveRect(bundle: Bundle) {
      bundle.classLoader = classLoader
      val rect = bundle.getParcelable<Rect>("rect")
      process(rect) // Do more with the parcelable
    }
}

Java

private final IRectInsideBundle.Stub binder = new IRectInsideBundle.Stub() {
    public void saveRect(Bundle bundle){
        bundle.setClassLoader(getClass().getClassLoader());
        Rect rect = bundle.getParcelable("rect");
        process(rect); // Do more with the parcelable.
    }
};

Cómo llamar a un método de IPC

Estos son los pasos que una clase que realiza una llamada debe realizar para llamar a una interfaz remota definida con el AIDL.

  1. Incluye el archivo .aidl en el directorio src/ del proyecto.
  2. Declara una instancia de la interfaz IBinder (generada según el AIDL).
  3. Implementa ServiceConnection.
  4. Llama a Context.bindService(), pasando la implementación de ServiceConnection.
  5. En tu implementación de onServiceConnected(), recibirás una instancia de IBinder (denominada service). Llama a YourInterfaceName.Stub.asInterface((IBinder)service) para convertir el parámetro devuelto en un tipo YourInterface.
  6. Llama a los métodos que definiste en la interfaz. Siempre debes capturar excepciones DeadObjectException, que se producen cuando se pierde la conexión. También debes capturar las excepciones de SecurityException, que se lanzan cuando los dos procesos involucrados en la llamada al método de IPC tienen definiciones contradictorias con el AIDL.
  7. Para la desconexión, llama a Context.unbindService() con la instancia de tu interfaz.

Algunos comentarios sobre llamar a un servicio de IPC:

  • Los objetos se cuentan como referencia entre procesos.
  • Puedes enviar objetos anónimos como argumentos de métodos.

Para obtener más información sobre enlaces a un servicio, consulta el documento Servicios enlazados.

Aquí se muestra un código de ejemplo que demuestra la llamada a un servicio creado con el AIDL, tomado del ejemplo del servicio remoto en el proyecto ApiDemos.

Kotlin

private const val BUMP_MSG = 1

class Binding : Activity() {

    /** The primary interface we will be calling on the service.  */
    private var mService: IRemoteService? = null

    /** Another interface we use on the service.  */
    internal var secondaryService: ISecondary? = null

    private lateinit var killButton: Button
    private lateinit var callbackText: TextView
    private lateinit var handler: InternalHandler

    private var isBound: Boolean = false

    /**
     * Class for interacting with the main interface of the service.
     */
    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service)
            killButton.isEnabled = true
            callbackText.text = "Attached."

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService?.registerCallback(mCallback)
            } catch (e: RemoteException) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_connected,
                    Toast.LENGTH_SHORT
            ).show()
        }

        override fun onServiceDisconnected(className: ComponentName) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null
            killButton.isEnabled = false
            callbackText.text = "Disconnected."

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT
            ).show()
        }
    }

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private val secondaryConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service)
            killButton.isEnabled = true
        }

        override fun onServiceDisconnected(className: ComponentName) {
            secondaryService = null
            killButton.isEnabled = false
        }
    }

    private val mBindListener = View.OnClickListener {
        // Establish a couple connections with the service, binding
        // by interface names.  This allows other applications to be
        // installed that replace the remote service by implementing
        // the same interface.
        val intent = Intent(this@Binding, RemoteService::class.java)
        intent.action = IRemoteService::class.java.name
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
        intent.action = ISecondary::class.java.name
        bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
        isBound = true
        callbackText.text = "Binding."
    }

    private val unbindListener = View.OnClickListener {
        if (isBound) {
            // If we have received the service, and hence registered with
            // it, then now is the time to unregister.
            try {
                mService?.unregisterCallback(mCallback)
            } catch (e: RemoteException) {
                // There is nothing special we need to do if the service
                // has crashed.
            }

            // Detach our existing connection.
            unbindService(mConnection)
            unbindService(secondaryConnection)
            killButton.isEnabled = false
            isBound = false
            callbackText.text = "Unbinding."
        }
    }

    private val killListener = View.OnClickListener {
        // To kill the process hosting our service, we need to know its
        // PID.  Conveniently our service has a call that will return
        // to us that information.
        try {
            secondaryService?.pid?.also { pid ->
                // Note that, though this API allows us to request to
                // kill any process based on its PID, the kernel will
                // still impose standard restrictions on which PIDs you
                // are actually able to kill.  Typically this means only
                // the process running your application and any additional
                // processes created by that app as shown here; packages
                // sharing a common UID will also be able to kill each
                // other's processes.
                Process.killProcess(pid)
                callbackText.text = "Killed service process."
            }
        } catch (ex: RemoteException) {
            // Recover gracefully from the process hosting the
            // server dying.
            // Just for purposes of the sample, put up a notification.
            Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
        }
    }

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private val mCallback = object : IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        override fun valueChanged(value: Int) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
        }
    }

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.remote_service_binding)

        // Watch for button clicks.
        var button: Button = findViewById(R.id.bind)
        button.setOnClickListener(mBindListener)
        button = findViewById(R.id.unbind)
        button.setOnClickListener(unbindListener)
        killButton = findViewById(R.id.kill)
        killButton.setOnClickListener(killListener)
        killButton.isEnabled = false

        callbackText = findViewById(R.id.callback)
        callbackText.text = "Not attached."
        handler = InternalHandler(callbackText)
    }

    private class InternalHandler(
            textView: TextView,
            private val weakTextView: WeakReference<TextView> = WeakReference(textView)
    ) : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
                else -> super.handleMessage(msg)
            }
        }
    }
}

Java

public static class Binding extends Activity {
    /** The primary interface we will be calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary secondaryService = null;

    Button killButton;
    TextView callbackText;

    private InternalHandler handler;
    private boolean isBound;

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to poke it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button clicks.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(unbindListener);
        killButton = (Button)findViewById(R.id.kill);
        killButton.setOnClickListener(killListener);
        killButton.setEnabled(false);

        callbackText = (TextView)findViewById(R.id.callback);
        callbackText.setText("Not attached.");
        handler = new InternalHandler(callbackText);
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service has been
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            killButton.setEnabled(true);
            callbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            mService = null;
            killButton.setEnabled(false);
            callbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection secondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service);
            killButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            secondaryService = null;
            killButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names.  This allows other applications to be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE);
            isBound = true;
            callbackText.setText("Binding.");
        }
    };

    private OnClickListener unbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (isBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // has crashed.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(secondaryConnection);
                killButton.setEnabled(false);
                isBound = false;
                callbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener killListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently our service has a call that will return
            // to us that information.
            if (secondaryService != null) {
                try {
                    int pid = secondaryService.getPid();
                    // Note that, though this API allows us to request to
                    // kill any process based on its PID, the kernel will
                    // still impose standard restrictions on which PIDs you
                    // are actually able to kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here; packages
                    // sharing a common UID will also be able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    callbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // Just for purposes of the sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here will
         * NOT be running in our main thread like most other things -- so,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private static class InternalHandler extends Handler {
        private final WeakReference<TextView> weakTextView;

        InternalHandler(TextView textView) {
            weakTextView = new WeakReference<>(textView);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    TextView textView = weakTextView.get();
                    if (textView != null) {
                        textView.setText("Received from service: " + msg.arg1);
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}