Android Interface Definition Language (AIDL).

Android Interface Definition Language (AIDL) jest podobny do innych IDL: pozwala zdefiniować interfejs programowania, który będzie akceptowany zarówno przez klienta, jak i usługę, na potrzeby komunikacji między procesami (IPC).

Na Androidzie jeden proces nie ma normalnie dostępu do pamięci innego procesu. Mówiąc, trzeba rozłożyć obiekty na podstawowe, które system operacyjny może zrozumieć, i przesunąć je w obrębie tej granicy. Napisanie takiego kodu jest żmudne, dlatego Android obsługuje je za pomocą AIDL.

Uwaga: AIDL jest konieczne tylko wtedy, gdy zezwalasz klientom z różnych aplikacji na dostęp do usługi dla IPC i chcesz obsługiwać wielowątkowość w swojej usłudze. Jeśli nie musisz przeprowadzać równoczesnych testów IPC w różnych aplikacjach, utwórz interfejs, zaimplementując Binder. Jeśli chcesz wykonać kod IPC, ale nie musisz obsługiwać wielowątkowości, zaimplementuj swój interfejs za pomocą Messenger. Niezależnie od tego upewnij się, że znasz powiązane usługi, zanim wdrożysz AIDL.

Zanim zaczniesz projektować interfejs AIDL, pamiętaj, że wywołania interfejsu AIDL są bezpośrednimi wywołaniami funkcji. Nie należy wyciągać wniosków na temat wątku, w którym następuje wywołanie. To, co się stanie, zależy od tego, czy wywołanie pochodzi z wątku w procesie lokalnym, czy z procesu zdalnego:

  • Wywołania z procesu lokalnego są wykonywane w tym samym wątku, z którego pochodzą. Jeśli jest to Twój główny wątek UI, będzie on nadal wykonywany w interfejsie AIDL. Jeśli jest to inny wątek, będzie to ten, który wykonuje kod w usłudze. Dzięki temu, jeśli dostęp do usługi mają tylko wątki lokalne, możesz w pełni kontrolować, które z nich są w niej wykonywane. W takim przypadku w ogóle nie używaj AIDL. Zamiast tego utwórz interfejs, zaimplementując Binder.
  • Wywołania z procesu zdalnego są wysyłane z puli wątków, którą platforma obsługuje w ramach własnego procesu. Przygotuj się na połączenia przychodzące z nieznanych wątków, które mogą odbywać się w tym samym czasie. Inaczej mówiąc, implementacja interfejsu AIDL musi być całkowicie bezpieczna w wątku. Wywołania wykonywane z jednego wątku w tym samym obiekcie zdalnym są w kolejności po stronie odbiorcy.
  • Słowo kluczowe oneway zmienia działanie wywołań zdalnych. Wywołanie zdalne nie jest wtedy blokowane. Przesyła dane transakcji, a następnie natychmiast je zwraca. Implementacja interfejsu otrzymuje w końcu jako zwykłe wywołanie z puli wątków Binder jako zwykłe wywołanie zdalne. Jeśli używany jest interfejs oneway w połączeniu z wywołaniem lokalnym, nie ma to wpływu na działanie usługi, a wywołanie jest nadal synchroniczne.

Definiowanie interfejsu AIDL

Zdefiniuj interfejs AIDL w pliku .aidl przy użyciu składni języka programowania Java, a następnie zapisz go w kodzie źródłowym, w katalogu src/, zarówno aplikacji hostującej usługę, jak i dowolnej innej aplikacji powiązanej z usługą.

Gdy kompilujesz każdą aplikację, która zawiera plik .aidl, narzędzia Android SDK generują interfejs IBinder na podstawie pliku .aidl i zapisują go w katalogu gen/ projektu. Usługa musi odpowiednio implementować interfejs IBinder. Aplikacje klienckie mogą następnie powiązać się z usługą i wywołać metody z interfejsu IBinder, aby wykonać IPC.

Aby utworzyć usługę ograniczoną za pomocą AIDL, wykonaj te czynności opisane w kolejnych sekcjach:

  1. Tworzenie pliku .aidl

    Ten plik definiuje interfejs programowania z podpisami metod.

  2. Implementowanie interfejsu

    Narzędzia pakietu Android SDK generują interfejs w języku programowania Java na podstawie pliku .aidl. Ten interfejs zawiera wewnętrzną klasę abstrakcyjną o nazwie Stub, która stanowi rozszerzenie Binder i implementuje metody z Twojego interfejsu AIDL. Musisz rozszerzyć klasę Stub i wdrożyć metody.

  3. Udostępnianie interfejsu klientom

    Zaimplementuj obiekt Service i zastąp onBind(), aby zwrócić implementację klasy Stub.

Uwaga: wszystkie zmiany wprowadzone w interfejsie AIDL po pierwszej wersji muszą pozostać zgodne wstecznie, aby uniknąć uszkodzenia innych aplikacji korzystających z Twojej usługi. Oznacza to, że plik .aidl musi zostać skopiowany do innych aplikacji, aby mogły uzyskać dostęp do interfejsu usługi, dlatego musisz utrzymywać obsługę oryginalnego interfejsu.

Tworzenie pliku .aidl

AIDL wykorzystuje prostą składnię, która umożliwia zadeklarowanie interfejsu za pomocą co najmniej 1 metody, która może pobierać parametry i zwracać wartości. Parametry i zwracane wartości mogą być dowolnego typu, nawet w przypadku innych interfejsów generowanych przez AI.

Plik .aidl musisz utworzyć w języku programowania Java. Każdy plik .aidl musi definiować jeden interfejs i wymaga jedynie deklaracji interfejsu i podpisów metod.

Domyślnie AIDL obsługuje te typy danych:

  • Wszystkie typy podstawowe w języku programowania Java (np. int, long, char, boolean itd.)
  • Tablice typów podstawowych, np. int[]
  • String
  • CharSequence
  • List

    Wszystkie elementy List muszą należeć do jednego z obsługiwanych typów danych na tej liście lub jednego z innych zadeklarowanych przez Ciebie interfejsów lub pakietów wygenerowanych przez AIDL. Typu List można opcjonalnie używać jako klasy typu z parametrami, np. List<String>. Rzeczywista klasa konkretna, którą otrzymuje druga strona, to zawsze ArrayList, chociaż metoda jest generowana, aby korzystać z interfejsu List.

  • Map

    Wszystkie elementy Map muszą należeć do jednego z obsługiwanych typów danych na tej liście lub jednego z innych zadeklarowanych przez Ciebie interfejsów lub pakietów wygenerowanych przez AIDL. Mapy z parametrami, takie jak te w formacie Map<String,Integer>, nie są obsługiwane. Rzeczywista klasa betonowa odbierana przez drugą stronę to zawsze HashMap, choć metoda jest generowana na potrzeby interfejsu Map. Rozważ użycie elementu Bundle jako alternatywy dla Map.

Musisz umieścić instrukcję import dla każdego dodatkowego typu, którego nie ma na liście, nawet jeśli są one zdefiniowane w tym samym pakiecie co Twój interfejs.

Podczas definiowania interfejsu usługi pamiętaj o tych kwestiach:

  • Metody mogą przyjmować zero lub więcej parametrów i zwracać wartości lub unieważnienia.
  • Wszystkie parametry inne niż podstawowe wymagają tagu kierunkowego wskazującego kierunek przesyłania danych: in, out lub inout (zobacz przykład poniżej).

    Interfejsy podstawowe, String, IBinder i interfejsy wygenerowane przez AIDL mają domyślnie wartość in i nie można ich zmienić.

    Uwaga: ogranicz kierunek do tego, co jest naprawdę potrzebne, ponieważ parametry marshallingu są drogie.

  • Wszystkie komentarze do kodu zawarte w pliku .aidl są uwzględniane w wygenerowanym interfejsie IBinder z wyjątkiem komentarzy poprzedzających instrukcję importu i pakietu.
  • Ciągi i stałe int można zdefiniować w interfejsie AIDL, np. const int VERSION = 1;.
  • Wywołania metod są wysyłane przez kod transact(), który zwykle opiera się na indeksie metod w interfejsie. Utrudnia to obsługę wersji, dlatego możesz ręcznie przypisać kod transakcji do metody: void method() = 10;.
  • Argumenty null i zwracane typy muszą być oznaczone adnotacjami za pomocą funkcji @nullable.

Oto przykładowy plik .aidl:

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

Zapisz plik .aidl w katalogu src/ projektu. Gdy skompilujesz aplikację, narzędzia pakietu SDK wygenerują plik interfejsu IBinder w katalogu gen/ projektu. Nazwa wygenerowanego pliku jest taka sama jak nazwa pliku .aidl, ale ma rozszerzenie .java. Na przykład wartość IRemoteService.aidl może skutkować IRemoteService.java.

Jeśli używasz Android Studio, kompilacja przyrostowa generuje klasę powiązania niemal natychmiast. Jeśli nie używasz Android Studio, narzędzie Gradle wygeneruje klasę bindowania przy następnym tworzeniu aplikacji. Utwórz projekt w gradle assembleDebug lub gradle assembleRelease, gdy tylko skończysz pisanie pliku .aidl, aby Twój kod mógł utworzyć link do wygenerowanej klasy.

Implementacja interfejsu

Gdy tworzysz aplikację, narzędzia pakietu Android SDK generują plik interfejsu .java o nazwie odpowiadającej plikowi .aidl. Wygenerowany interfejs zawiera podklasę o nazwie Stub, która jest abstrakcyjną implementacją interfejsu nadrzędnego, np. YourInterface.Stub, i deklaruje wszystkie metody z pliku .aidl.

Uwaga: Stub definiuje też kilka metod pomocniczych, a zwłaszcza asInterface(), które wymagają metody IBinder (zazwyczaj przekazywanej do metody wywołania zwrotnego onServiceConnected() klienta) i zwraca instancję interfejsu skróconego. Więcej informacji na temat rzutowania znajdziesz w sekcji Wywoływanie metody IPC.

Aby zaimplementować interfejs wygenerowany z .aidl, rozszerz wygenerowany interfejs Binder, np. YourInterface.Stub, i zaimplementuj metody odziedziczone z pliku .aidl.

Oto przykład implementacji interfejsu IRemoteService zdefiniowanego w poprzednim przykładzie IRemoteService.aidl przy użyciu instancji anonimowej:

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 jest teraz instancją klasy Stub (Binder), która określa interfejs IPC usługi. W następnym kroku ta instancja będzie widoczna dla klientów, którzy mogą korzystać z usługi.

Implementując interfejs AIDL, pamiętaj o kilku zasadach:

  • Nie ma gwarancji, że połączenia przychodzące zostaną wykonane w wątku głównym, więc musisz od początku zastanowić się nad wielowątkowością i odpowiednio przygotować usługę tak, aby była bezpieczna w wątku.
  • Domyślnie wywołania IPC są synchroniczne. Jeśli wiesz, że wykonanie żądania przez usługę zajmuje więcej niż kilka milisekund, nie wywołuj go z głównego wątku aktywności. Aplikacja może zawiesić aplikację, co spowoduje wyświetlenie w Androidzie okna „Aplikacja nie odpowiada”. Wywołaj ją w osobnym wątku w kliencie.
  • Tylko typy wyjątków wymienione w dokumentacji referencyjnej Parcel.writeException() są wysyłane z powrotem do elementu wywołującego.

Udostępnianie interfejsu klientom

Po zaimplementowaniu interfejsu usługi musisz udostępnić go klientom, aby mogli się z nim powiązać. Aby udostępnić interfejs swojej usługi, rozszerz Service i zaimplementuj onBind(), co zwróci instancję klasy, która implementuje wygenerowany obiekt Stub. Jak omówiliśmy w poprzedniej sekcji. Oto przykładowa usługa, która udostępnia klientom przykładowy interfejs IRemoteService.

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

Teraz, gdy klient, na przykład aktywność, wywoła metodę bindService(), aby połączyć się z tą usługą, wywołanie zwrotne onServiceConnected() klienta odbierze wystąpienie binder zwrócone przez metodę onBind() usługi.

Klient musi też mieć dostęp do klasy interfejsu. Jeśli klient i usługa znajdują się w osobnych aplikacjach, to aplikacja kliencka musi mieć w katalogu src/ kopię pliku .aidl, który generuje interfejs android.os.Binder zapewniający klientowi dostęp do metod AIDL.

Gdy klient otrzyma parametr IBinder w wywołaniu zwrotnym onServiceConnected(), musi wywołać YourServiceInterface.Stub.asInterface(service), aby rzutować zwrócony parametr na typ 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;
    }
};

Więcej przykładowego kodu znajdziesz w klasie RemoteService.java w ApiDemos.

Przekazywanie obiektów przez IPC

W Androidzie 10 (poziom interfejsu API 29 lub wyższy) możesz definiować obiekty Parcelable bezpośrednio w AIDL. Obsługiwane są również typy, które są obsługiwane jako argumenty interfejsu AIDL i inne paczki. Pozwala to uniknąć dodatkowej pracy związanej z ręcznym pisaniem kodu marshalla i klasy niestandardowej. Powoduje to jednak również utworzenie samej struktury. Jeśli chcesz korzystać z niestandardowych akcesorów lub innych funkcji, zaimplementuj 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;
}

Poprzedni przykładowy kod automatycznie generuje klasę Javy z polami liczb całkowitych left, top, right i bottom. Cały odpowiedni kod organizacyjny jest implementowany automatycznie, a obiektu można używać bezpośrednio bez konieczności dodawania implementacji.

Możesz też wysyłać klasę niestandardową z jednego procesu do innego przez interfejs IPC. Upewnij się jednak, że klasa jest dostępna po drugiej stronie kanału IPC, a klasa musi obsługiwać interfejs Parcelable. Obsługa Parcelable jest ważna, ponieważ umożliwia systemowi Android rozkładanie obiektów na podstawowe, które mogą być łączone w różnych procesach.

Aby utworzyć klasę niestandardową obsługującą Parcelable, wykonaj te czynności:

  1. Zadbaj o to, aby zajęcia korzystały z interfejsu Parcelable.
  2. Wdróż writeToParcel, który pobiera aktualny stan obiektu i zapisuje go w Parcel.
  3. Dodaj do klasy pole statyczne o nazwie CREATOR, które jest obiektem implementującym interfejs Parcelable.Creator.
  4. Na koniec utwórz plik .aidl deklarujący klasę Twoich paczek, jak w poniższym pliku Rect.aidl.

    Jeśli korzystasz z niestandardowego procesu kompilacji, nie dodawaj do kompilacji pliku .aidl. Ten plik .aidl nie jest skompilowany podobnie jak plik nagłówka w języku C.

AIDL wykorzystuje w generowanym kodzie te metody i pola do porządkowania i usuwania obiektów.

Oto na przykład plik Rect.aidl umożliwiający utworzenie klasy Rect z możliwością przetwarzania:

package android.graphics;

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

Oto przykład implementacji protokołu Parcelable przez klasa Rect.

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

Organizowanie w klasie Rect jest proste. Zapoznaj się z pozostałymi metodami w Parcel, aby poznać inne rodzaje wartości, które możesz zapisywać w polu Parcel.

Ostrzeżenie: pamiętaj o wpływie odbierania danych z innych procesów na bezpieczeństwo. W tym przypadku Rect odczytuje 4 cyfry z Parcel, ale to Ty musisz się upewnić, że ich wartości mieszczą się w akceptowalnym zakresie wartości możliwych do wykonania. Więcej informacji o zabezpieczaniu aplikacji przed złośliwym oprogramowaniem znajdziesz w artykule Wskazówki dotyczące bezpieczeństwa.

Metody z argumentami pakietu zawierającymi interfejs Parcelable

Jeśli metoda akceptuje obiekt Bundle, który powinien zawierać obiekty parcelable, ustaw moduł ładowania klasy Bundle przez wywołanie Bundle.setClassLoader(ClassLoader) przed próbą odczytu z interfejsu Bundle. W przeciwnym razie napotkasz kod ClassNotFoundException, mimo że plik parcelable jest prawidłowo zdefiniowany w Twojej aplikacji.

Przeanalizujmy ten przykładowy plik .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);
}
Jak widać w poniższej implementacji, właściwość ClassLoader jest wyraźnie ustawiona w Bundle przed odczytaniem parametru 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.
    }
};

Wywoływanie metody IPC

Aby wywołać interfejs zdalny zdefiniowany za pomocą AIDL, wykonaj te czynności w klasie wywołującej:

  1. Umieść plik .aidl w katalogu projektu src/.
  2. Zadeklaruj instancję interfejsu IBinder, która jest generowana na podstawie AILIDL.
  3. Wdróż ServiceConnection.
  4. Wywołaj Context.bindService(), przekazując implementację ServiceConnection.
  5. W implementacji onServiceConnected() otrzymujesz wystąpienie IBinder o nazwie service. Wywołaj YourInterfaceName.Stub.asInterface((IBinder)service), aby rzutować zwrócony parametr na typ YourInterface.
  6. Wywołaj metody zdefiniowane w interfejsie. Zawsze wykrywaj wyjątki DeadObjectException, które są zgłaszane po przerwaniu połączenia. Pułapka obejmuje też wyjątki SecurityException, które są wywoływane, gdy 2 procesy związane z wywołaniem metody IPC mają sprzeczne definicje AIDL.
  7. Aby się rozłączyć, wywołaj polecenie Context.unbindService() przy użyciu instancji interfejsu.

Podczas wywoływania usługi IPC pamiętaj o tych kwestiach:

  • Obiekty są zliczane w procesach.
  • Jako argumenty metody możesz wysyłać anonimowe obiekty.

Więcej informacji o powiązaniu z usługą znajdziesz w artykule Omówienie usług granicznych.

Oto przykładowy kod, który pokazuje wywoływanie usługi utworzonej przez AIDL, pobranej z przykładowej usługi zdalnej w projekcie 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);
            }
        }
    }
}