Android Interface Definition Language (AIDL)

Android Interface Definition Language (AIDL) è simile ad altri IDL: consente di definire l'interfaccia di programmazione concordata sia dal client sia dal servizio per comunicare tra loro tramite la comunicazione tra processi (IPC).

Su Android, un processo in genere non può accedere alla memoria di un altro. Per parlare, devono scomporre i loro oggetti in primitive che il sistema operativo è in grado di comprendere ed eseguire il marshalling degli oggetti oltre quel confine per te. Il codice necessario per eseguire il marshalling è noioso da scrivere, quindi Android lo gestisce al posto tuo con AIDL.

Nota: AIDL è necessario solo se consenti a client di applicazioni diverse di accedere al tuo servizio per IPC e vuoi gestire il multithreading nel tuo servizio. Se non hai bisogno di eseguire IPC in contemporanea su diverse applicazioni, crea la tua interfaccia implementando un Binder. Se vuoi eseguire l'IPC ma non hai bisogno di gestire il multithreading, implementa l'interfaccia utilizzando un Messenger. In ogni caso, assicurati di comprendere i servizi associati prima di implementare un AIDL.

Prima di iniziare a progettare l'interfaccia AIDL, tieni presente che le chiamate a un'interfaccia AIDL sono chiamate di funzione dirette. Non fare supposizioni sul thread in cui ha luogo la chiamata. Ciò che accade è diverso a seconda che la chiamata provenga da un thread nel processo locale o da un processo remoto:

  • Le chiamate effettuate dal processo locale vengono eseguite nello stesso thread che sta effettuando la chiamata. Se questo è il thread dell'interfaccia utente principale, continuerà a essere eseguito nell'interfaccia AIDL. Se si tratta di un altro thread, sarà quello che esegue il codice nel servizio. Pertanto, se solo i thread locali accedono al servizio, puoi controllare completamente quali thread sono in esecuzione al suo interno. In questo caso, però, non utilizzare AIDL. Crea invece l'interfaccia implementando un Binder.
  • Le chiamate da un processo remoto vengono inviate da un pool di thread che la piattaforma gestisce all'interno del tuo processo. Preparati per le chiamate in arrivo da thread sconosciuti, in cui si verificano più chiamate contemporaneamente. In altre parole, un'implementazione di un'interfaccia AIDL deve essere completamente thread-safe. Le chiamate effettuate da un thread per lo stesso oggetto remoto arrivano in ordine al lato destinatario.
  • La parola chiave oneway modifica il comportamento delle chiamate remote. Quando viene usato, le chiamate remote non vengono bloccate. Invia i dati della transazione e restituisce immediatamente il report. L'implementazione dell'interfaccia riceve infine questa richiesta dal pool di thread Binder come una normale chiamata remota. Se oneway viene utilizzato con una chiamata locale, non ha alcun impatto e la chiamata è ancora sincrona.

Definizione di un'interfaccia AIDL

Definisci l'interfaccia AIDL in un file .aidl utilizzando la sintassi del linguaggio di programmazione Java, quindi salvala nel codice sorgente, nella directory src/, sia dell'applicazione che ospita il servizio sia di qualsiasi altra applicazione associata al servizio.

Quando crei ogni applicazione che contiene il file .aidl, gli strumenti SDK Android generano un'interfaccia IBinder in base al file .aidl e la salvano nella directory gen/ del progetto. Il servizio deve implementare l'interfaccia IBinder a seconda dei casi. Le applicazioni client possono quindi associarsi al servizio e chiamare metodi da IBinder per eseguire l'IPC.

Per creare un servizio limitato utilizzando AIDL, segui questi passaggi, descritti nelle sezioni seguenti:

  1. Crea il file .aidl

    Questo file definisce l'interfaccia di programmazione con firme dei metodi.

  2. Implementare l'interfaccia

    Gli strumenti dell'SDK Android generano un'interfaccia nel linguaggio di programmazione Java in base al file .aidl. Questa interfaccia ha una classe astratta interna denominata Stub che estende Binder e implementa metodi dall'interfaccia AIDL. Devi estendere la classe Stub e implementare i metodi.

  3. Esponi l'interfaccia ai clienti

    Implementa Service e sostituisci onBind() per restituire l'implementazione della classe Stub.

Attenzione: eventuali modifiche apportate all'interfaccia AIDL dopo la prima release devono rimanere compatibili con le versioni precedenti per evitare di danneggiare altre applicazioni che utilizzano il tuo servizio. Questo significa che il file .aidl deve essere copiato in altre applicazioni in modo che possano accedere all'interfaccia del servizio, pertanto devi mantenere il supporto per l'interfaccia originale.

Crea il file .aidl

AIDL utilizza una sintassi semplice che consente di dichiarare un'interfaccia con uno o più metodi in grado di accettare parametri e restituire valori. I parametri e i valori restituiti possono essere di qualsiasi tipo, anche altre interfacce generate da AIDL.

Devi creare il file .aidl utilizzando il linguaggio di programmazione Java. Ogni file .aidl deve definire una singola interfaccia e richiede solo la dichiarazione dell'interfaccia e le firme dei metodi.

Per impostazione predefinita, AIDL supporta i seguenti tipi di dati:

  • Tutti i tipi primitivi nel linguaggio di programmazione Java (come int, long, char, boolean e così via)
  • Array di tipi primitivi, come int[]
  • String
  • CharSequence
  • List

    Tutti gli elementi in List devono essere uno dei tipi di dati supportati in questo elenco o una delle altre interfacce o dei parcelable generati da AIDL che hai dichiarato. Una List può essere facoltativamente utilizzata come classe di tipo con parametri, ad esempio List<String>. La classe effettiva effettiva che l'altro lato riceve è sempre un ArrayList, anche se il metodo viene generato per utilizzare l'interfaccia List.

  • Map

    Tutti gli elementi in Map devono essere uno dei tipi di dati supportati in questo elenco o una delle altre interfacce o dei parcelable generati da AIDL che hai dichiarato. Le mappe dei tipi parametrizzati, come quelle del modulo Map<String,Integer>, non sono supportate. La classe effettiva effettiva che riceve l'altro lato è sempre HashMap, anche se il metodo viene generato per utilizzare l'interfaccia Map. Valuta l'utilizzo di Bundle in alternativa a Map.

Devi includere un'istruzione import per ogni tipo aggiuntivo non elencato in precedenza, anche se sono definiti nello stesso pacchetto dell'interfaccia.

Quando definisci l'interfaccia del servizio, tieni presente che:

  • I metodi possono assumere zero o più parametri e possono restituire un valore o void.
  • Tutti i parametri non primitivi richiedono un tag direzionale che indichi la modalità di trasmissione dei dati: in, out o inout (vedi l'esempio di seguito).

    Le interfacce primitive, String, IBinder e generate con AIDL sono in per impostazione predefinita e non possono esserlo diversamente.

    Attenzione: limita la direzione a ciò che è veramente necessario, perché i parametri di marshalling sono costosi.

  • Tutti i commenti del codice inclusi nel file .aidl sono inclusi nell'interfaccia IBinder generata, ad eccezione dei commenti prima delle istruzioni di importazione e pacchetto.
  • Le costanti stringa e int possono essere definite nell'interfaccia AIDL, ad esempio const int VERSION = 1;.
  • Le chiamate al metodo vengono inviate da un codice transact(), che normalmente si basa su un indice del metodo nell'interfaccia. Poiché questo rende difficile il controllo delle versioni, puoi assegnare manualmente il codice transazione a un metodo: void method() = 10;.
  • Gli argomenti e i tipi restituiti che possono essere nulli devono essere annotati utilizzando @nullable.

Ecco un file .aidl di esempio:

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

// Declare any non-default types here with import statements.

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service. */
    int getPid();

    /** Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

Salva il file .aidl nella directory src/ del progetto. Quando crei la tua applicazione, gli strumenti SDK generano il file di interfaccia IBinder nella directory gen/ del progetto. Il nome del file generato corrisponde al nome del file .aidl, ma con un'estensione .java. Ad esempio, IRemoteService.aidl restituisce IRemoteService.java.

Se utilizzi Android Studio, la build incrementale genera la classe binder quasi immediatamente. Se non utilizzi Android Studio, lo strumento Gradle genera la classe binder la prossima volta che crei l'applicazione. Crea il tuo progetto con gradle assembleDebug o gradle assembleRelease non appena hai finito di scrivere il file .aidl, in modo che il tuo codice possa collegarsi alla classe generata.

Implementare l'interfaccia

Quando crei la tua applicazione, gli strumenti SDK Android generano un file dell'interfaccia .java con un nome basato sul file .aidl. L'interfaccia generata include una sottoclasse denominata Stub, che è un'implementazione astratta dell'interfaccia padre, come YourInterface.Stub, e dichiara tutti i metodi presenti nel file .aidl.

Nota: Stub definisce anche alcuni metodi helper, in particolare asInterface(), che richiede un IBinder, di solito quello passato al metodo di callback onServiceConnected() di un client, e restituisce un'istanza dell'interfaccia stub. Per ulteriori dettagli su come effettuare questa trasmissione, consulta la sezione Chiamare un metodo IPC.

Per implementare l'interfaccia generata da .aidl, estendi l'interfaccia Binder generata, ad esempio YourInterface.Stub, e implementa i metodi ereditati dal file .aidl.

Di seguito è riportato un esempio di implementazione di un'interfaccia denominata IRemoteService, definita dall'esempio IRemoteService.aidl precedente, utilizzando un'istanza anonima:

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.
    }
};

binder è un'istanza della classe Stub (Binder), che definisce l'interfaccia IPC per il servizio. Nel passaggio successivo, questa istanza è esposta ai client in modo che possano interagire con il servizio.

Tieni presente alcune regole durante l'implementazione dell'interfaccia AIDL:

  • Non è garantito che le chiamate in arrivo vengano eseguite nel thread principale, quindi devi pensare al multi-threading fin dall'inizio e creare correttamente il tuo servizio per garantire il thread-safe.
  • Per impostazione predefinita, le chiamate IPC sono sincrone. Se sai che il servizio impiega più di qualche millisecondo per completare una richiesta, non chiamarlo dal thread principale dell'attività. L'applicazione potrebbe bloccare l'applicazione e di conseguenza Android potrebbe visualizzare la finestra di dialogo "L'applicazione non risponde". Chiamalo da un thread separato nel client.
  • Solo i tipi di eccezioni elencati nella documentazione di riferimento per Parcel.writeException() vengono inviati al chiamante.

Esporre l'interfaccia ai clienti

Dopo aver implementato l'interfaccia per il servizio, devi esporla ai client in modo che possano associarla. Per esporre l'interfaccia per il tuo servizio, estendi Service e implementa onBind() per restituire un'istanza della classe che implementa il Stub generato, come discusso nella sezione precedente. Ecco un servizio di esempio che espone l'interfaccia di esempio IRemoteService ai client.

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.
        }
    };
}

Ora, quando un client, ad esempio un'attività, chiama bindService() per connettersi a questo servizio, il callback onServiceConnected() del client riceve l'istanza binder restituita dal metodo onBind() del servizio.

Il client deve anche avere accesso alla classe dell'interfaccia. Quindi, se il client e il servizio si trovano in applicazioni separate, l'applicazione del client deve avere una copia del file .aidl nella directory src/, che genera l'interfaccia android.os.Binder e fornisce al client l'accesso ai metodi AIDL.

Quando il client riceve IBinder nel callback onServiceConnected(), deve chiamare YourServiceInterface.Stub.asInterface(service) per trasmettere il parametro restituito al tipo YourServiceInterface:

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 preceding example 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 preceding example 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;
    }
};

Per ulteriore codice di esempio, consulta la classe RemoteService.java in ApiDemos.

Trasferimento di oggetti su IPC

In Android 10 (livello API 29 o versioni successive), puoi definire oggetti Parcelable direttamente in AIDL. Qui sono supportati anche i tipi supportati come argomenti dell'interfaccia AIDL e altri parcelables. Ciò evita il lavoro aggiuntivo di scrittura manuale del codice di marshalling e di una classe personalizzata. Tuttavia, questo crea anche uno struct semplice. Se vuoi utilizzare funzioni di accesso personalizzate o altre funzionalità, implementa Parcelable.

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect {
    int left;
    int top;
    int right;
    int bottom;
}

L'esempio di codice precedente genera automaticamente una classe Java con campi di numeri interi left, top, right e bottom. Tutto il codice di marshalling pertinente viene implementato automaticamente e l'oggetto può essere utilizzato direttamente senza dover aggiungere alcuna implementazione.

Puoi anche inviare una classe personalizzata da un processo all'altro tramite un'interfaccia IPC. Tuttavia, assicurati che il codice della tua classe sia disponibile sull'altro lato del canale IPC e che la tua classe supporti l'interfaccia Parcelable. Supportare Parcelable è importante perché consente al sistema Android di scomporre gli oggetti in primitive che possono essere marshallizzate nei vari processi.

Per creare una classe personalizzata che supporti Parcelable, segui questi passaggi:

  1. Fai in modo che la tua classe implementi l'interfaccia Parcelable.
  2. Implementa writeToParcel, che prende lo stato attuale dell'oggetto e lo scrive in un Parcel.
  3. Aggiungi alla classe un campo statico denominato CREATOR, che corrisponde a un oggetto che implementa l'interfaccia Parcelable.Creator.
  4. Infine, crea un file .aidl che dichiari la tua classe parcelable, come mostrato per il seguente file Rect.aidl.

    Se utilizzi un processo di compilazione personalizzato, non aggiungere il file .aidl alla build. Analogamente a un file di intestazione in linguaggio C, questo file .aidl non viene compilato.

AIDL utilizza questi metodi e campi nel codice generato per eseguire il marshall e il marshalling degli oggetti.

Ad esempio, ecco un file Rect.aidl per creare una classe Rect composta da pacchetti:

package android.graphics;

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

Ecco un esempio di come la classe Rect implementa il protocollo 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) { null }
        }
    }

    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;
    }
}

Il marshalling nella classe Rect è semplice. Dai un'occhiata agli altri metodi su Parcel per vedere gli altri tipi di valori che puoi scrivere in un elemento Parcel.

Avviso: ricorda le implicazioni sulla sicurezza della ricezione di dati da altri processi. In questo caso, l'Rect legge quattro numeri dalla Parcel, ma spetta a te assicurarti che rientrino nell'intervallo accettabile di valori per qualsiasi operazione che il chiamante stia tentando di eseguire. Per ulteriori informazioni su come proteggere la tua applicazione dal malware, consulta i Suggerimenti per la sicurezza.

Metodi con argomenti Bundle contenenti Parcelables

Se un metodo accetta un oggetto Bundle che dovrebbe contenere parcelables, assicurati di impostare il classloader di Bundle chiamando Bundle.setClassLoader(ClassLoader) prima di tentare di leggere da Bundle. In caso contrario, si verifica ClassNotFoundException anche se il parcelable è definito correttamente nella tua applicazione.

Considera ad esempio il seguente file .aidl di esempio:

// 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);
}
Come mostrato nella seguente implementazione, ClassLoader è esplicitamente impostato nel Bundle prima di leggere 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.
    }
};

Chiamare un metodo IPC

Per chiamare un'interfaccia remota definita con AIDL, segui questi passaggi nella classe di chiamata:

  1. Includi il file .aidl nella directory del progetto src/.
  2. Dichiara un'istanza dell'interfaccia IBinder, generata in base all'AIDL.
  3. Implementa ServiceConnection.
  4. Chiama Context.bindService(), trasmettendo la tua implementazione di ServiceConnection.
  5. Nell'implementazione di onServiceConnected(), ricevi un'istanza IBinder, chiamata service. Chiama YourInterfaceName.Stub.asInterface((IBinder)service) per trasmettere il parametro restituito al tipo YourInterface.
  6. Richiama i metodi definiti nell'interfaccia. Blocca sempre le eccezioni DeadObjectException, che vengono generate quando la connessione si interrompe. Inoltre, puoi eseguire il trap delle eccezioni SecurityException, che vengono generate quando i due processi coinvolti nella chiamata al metodo IPC hanno definizioni AIDL in conflitto.
  7. Per disconnetterti, chiama Context.unbindService() con l'istanza della tua interfaccia.

Quando chiami un servizio IPC, tieni presente quanto segue:

  • Gli oggetti vengono conteggiati come riferimento nei processi.
  • Puoi inviare oggetti anonimi come argomenti del metodo.

Per saperne di più sull'associazione a un servizio, consulta la Panoramica dei servizi associati.

Di seguito è riportato un esempio di codice che dimostra la chiamata a un servizio creato con AIDL, estratto dall'esempio di servizio remoto nel progetto ApiDemos.

Kotlin

private const val BUMP_MSG = 1

class Binding : Activity() {

    /** The primary interface you call on the service.  */
    private var mService: IRemoteService? = null

    /** Another interface you 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 is
            // 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 crashes before we can
                // 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 is
            // unexpectedly disconnected&mdash;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 lets other applications 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
                // crashes.
            }

            // 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 the service, we need to know its
        // PID.  Conveniently, the service has a call that returns
        // that information.
        try {
            secondaryService?.pid?.also { pid ->
                // Note that, though this API lets us request to
                // kill any process based on its PID, the kernel
                // still imposes standard restrictions on which PIDs you
                // can actually 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 are also 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.
            // For purposes of this 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 is
         * NOT 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 interact with it before doing anything.
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.remote_service_binding)

        // Watch for button taps.
        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 are 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 interact with it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button taps.
        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 is
            // 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 crashes before we can 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 is
            // unexpectedly disconnected&mdash;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 lets other applications 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
                        // crashes.
                    }
                }

                // 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 returns
            // that information.
            if (secondaryService != null) {
                try {
                    int pid = secondaryService.getPid();
                    // Note that, though this API lets us request to
                    // kill any process based on its PID, the kernel
                    // still imposes standard restrictions on which PIDs you
                    // can actually 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 are also 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.
                    // For purposes of this 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 is
         * NOT 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);
            }
        }
    }
}