Google은 흑인 공동체를 위한 인종 간 평등을 진전시키기 위해 노력하고 있습니다. Google에서 어떤 노력을 하고 있는지 확인하세요.

안드로이드 인터페이스 정의 언어(AIDL)

AIDL(Android Interface Definition Language)은 전에 다뤄본 다른 IDL과 유사합니다. 클라이언트와 서비스가 모두 동의한 프로그래밍 인터페이스를 정의하여 프로세스 간 통신(IPC)으로 서로 통신하게 할 수 있습니다. Android에서는 일반적으로 한 프로세스가 다른 프로세스의 메모리에 액세스할 수 없습니다. 따라서 객체들을 운영체제가 이해할 수 있는 원시 유형으로 해체하고 해당 경계에 걸쳐 마샬링해야 합니다. 이 마샬링을 위해 코드를 작성하는 것은 상당히 지루한 작업인데, Android는 AIDL을 이용해 그 작업을 대신 처리합니다.

참고: AIDL은 클라이언트가 다른 애플리케이션에서 IPC의 서비스에 액세스하도록 허용하는 경우와 서비스에서 멀티스레딩을 처리하는 경우에만 사용해야 합니다. 다른 여러 애플리케이션에서 동시에 IPC를 수행할 필요가 없다면 바인더를 구현하여 인터페이스를 생성해야 합니다. 또는, IPC를 수행하기를 원하지만 멀티스레딩을 처리할 필요가 없는 경우에는 메신저를 사용하여 인터페이스를 구현하세요. 어떤 경우에도 AIDL을 구현하기 전에 바인딩된 서비스를 먼저 이해해야 합니다.

AIDL 인터페이스 디자인을 시작하기 전에 AIDL 인터페이스에 대한 호출은 직접 함수 호출임을 명심하세요. 호출이 발생하는 스레드에 대해 어떠한 가정도 해서는 안 됩니다. 결과는 해당 호출이 로컬 프로세스에서 온 것인지, 원격 프로세스에서 온 것인지에 따라 달라집니다. 구체적인 사항은 다음과 같습니다.

  • 로컬 프로세스에서 온 호출은 호출한 스레드와 같은 스레드에서 실행됩니다. 이 스레드가 기본 UI 스레드인 경우, AIDL 인터페이스에서 해당 스레드가 계속 실행됩니다. 다른 스레드인 경우, 서비스에서 코드를 실행하는 스레드입니다. 따라서 로컬 스레드만 서비스에 액세스하고 있다면, 서비스에서 실행되고 있는 스레드를 완벽히 제어할 수 있습니다. (하지만 이 경우에는 AIDL을 전혀 사용할 필요가 없습니다. 대신 바인더 구현을 통해 인터페이스를 생성하세요.)
  • 원격 프로세스에서 오는 호출은 플랫폼이 본인의 프로세스 내에서 유지관리하는 스레드 풀에서 발송됩니다. 여러 호출이 동시에 발생하므로 알 수 없는 스레드로부터 들어오는 호출에 대비해야 합니다. 다시 말해서, AIDL 인터페이스 구현은 스레드로부터 완벽하게 안전해야 합니다. 동일한 원격 객체에 있는 하나의 스레드에서 실행한 호출은 수신기 쪽에 순서대로 도착합니다.
  • oneway 키워드는 원격 호출의 동작을 수정합니다. 원격 호출을 사용하면 차단하지 않고, 단순히 트랜잭션 데이터를 전송하고 즉시 반환하기만 합니다. 최종적으로 인터페이스의 구현은 정상적인 원격 호출로서 Binder 스레드 풀에서 보내는 정기적인 호출인 것처럼 수신합니다. 지역 호출에서 oneway를 사용하는 것은 아무런 영향을 주지 않으며 호출은 여전히 동기적입니다.

AIDL 인터페이스 정의

AIDL 인터페이스는 .aidl 파일에서 Java 프로그래밍 언어 구문을 사용해서 정의한 다음, 서비스를 호스팅하는 애플리케이션 및 서비스로 바인딩되는 모든 다른 애플리케이션의 소스 코드(src/ 디렉토리)에 저장해야 합니다.

Android SDK 도구는 .aidl 파일이 포함된 각 애플리케이션을 빌드할 때마다 .aidl 파일을 기반으로 IBinder 인터페이스를 생성하고 프로젝트의 gen/ 디렉토리에 저장합니다. 서비스는 IBinder 인터페이스를 적절히 구현해야 합니다. 그러면 클라이언트 애플리케이션이 서비스에 바인딩하고 IBinder에서 메서드를 호출하여 IPC를 수행할 수 있습니다.

AIDL을 사용하여 바인딩된 서비스를 생성하려면 다음 단계를 따르세요.

  1. .aidl 파일 생성

    이 파일은 메서드 서명으로 프로그래밍 인터페이스를 정의합니다.

  2. 인터페이스 구현

    Android SDK 도구는 .aidl 파일을 기반으로 Java 프로그래밍 언어로 인터페이스를 생성합니다. 이 인터페이스는 Binder를 확장하고 AIDL 인터페이스로부터 메서드를 구현하는 Stub이라는 내부 추상 클래스를 가지고 있습니다. Stub 클래스를 확장하고 메서드를 구현해야 합니다.

  3. 클라이언트에게 인터페이스 노출

    Service를 구현하고 onBind()를 재정의하여 Stub 클래스의 구현을 반환합니다.

주의: 최초 릴리스 후 AIDL 인터페이스를 변경하는 경우, 서비스를 사용하는 다른 애플리케이션이 차단되지 않도록 반드시 이전 버전과의 호환성을 유지해야 합니다. 즉, 다른 애플리케이션이 서비스의 인터페이스에 액세스하려면 .aidl 파일을 복사해야 하기 때문에 원래의 인터페이스에 대한 지원을 유지해야 합니다.

1. .aidl 파일 생성

AIDL은 매개변수를 가져와서 값을 반환할 수 있는 하나 이상의 메서드를 사용하여 인터페이스를 선언할 수 있는 간단한 구문을 사용합니다. 다른 AIDL로 생성된 인터페이스를 비롯하여 모든 유형의 매개변수와 반환 값을 사용할 수 있습니다.

.aidl 파일은 Java 프로그래밍 언어로 작성해야 합니다. 각 .aidl 파일에서 한 개의 인터페이스만 정의해야 하며, 인터페이스 선언과 메서드 서명만 필요합니다.

기본적으로 AIDL은 다음 데이터 유형을 지원합니다.

  • Java 프로그래밍 언어의 모든 원시 데이터 유형(예: int, long, char, boolean 등)
  • String
  • CharSequence
  • List

    List의 모든 요소는 이 목록에 있는 지원 데이터 유형 중 하나이거나, 다른 AIDL로 생성한 인터페이스이거나, 개발자가 선언한 parcelable이어야 합니다. List는 선택적으로 '제네릭' 클래스(예: List<String>)로 사용할 수 있습니다. 메서드가 List 인터페이스를 사용하도록 생성되었더라도 상대방이 실제로 받는 구체적인 클래스는 언제나 ArrayList입니다.

  • Map

    Map의 모든 요소는 이 목록에 있는 지원 데이터 유형 중 하나이거나, 다른 AIDL로 생성한 인터페이스이거나, 개발자가 선언한 parcelable이어야 합니다. Map<String,Integer>와 같은 형태의 제네릭 맵은 지원되지 않습니다. 메서드가 Map 인터페이스를 사용하도록 생성되었더라도 상대방이 실제로 받는 구체적인 클래스는 언제나 HashMap입니다.

위에 열거되지 않은 추가 유형의 경우, 인터페이스와 동일한 패키지에 정의되어 있더라도 import 문을 포함해야 합니다.

서비스 인터페이스를 정의할 때는 다음 사항에 유의하세요.

  • 메서드는 0개 이상의 매개변수를 취하고 값 또는 void를 반환할 수 있습니다.
  • 모든 비원시 매개변수에는 데이터가 이동하는 방향을 나타내는 방향 태그가 필요합니다. 방향 태그는 in, out 또는 inout입니다(아래 예시 참조).

    원시 유형은 기본적으로 in이며 다른 유형은 사용할 수 없습니다.

    주의: 매개변수 마샬링은 시스템 리소스를 상당히 소모하기 때문에 꼭 필요한 대상으로만 방향을 제한해야 합니다.

  • .aidl 파일에 포함된 코드 주석은 import 및 package 문 앞의 주석을 제외하고 모두 생성된 IBinder 인터페이스에 포함됩니다.
  • 문자열과 int 상수는 AIDL 인터페이스에서 정의할 수 있습니다. 예: const int VERSION = 1;.
  • 메서드 호출은 transact() 코드로 전송되는데, 일반적으로 인터페이스의 메서드 색인에 기초합니다. 그래서 버전 지정이 어렵기 때문에 메서드 void method() = 10;에 트랜잭션 코드를 직접 할당해야 합니다.
  • @nullable를 사용하여 Null 가능 인수 또는 반환 유형에 주석을 표시합니다.

다음은 .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)
}

.aidl 파일을 프로젝트의 src/ 디렉토리에 저장하면 애플리케이션을 빌드할 때 SDK 도구가 프로젝트의 gen/ 디렉토리에 IBinder 인터페이스를 생성합니다. 생성되는 파일 이름은 .aidl 파일 이름과 일치하지만 .java 확장자를 가집니다(예를 들어, IRemoteService.aidlIRemoteService.java).

Android Studio를 사용하는 경우 증분 빌드는 거의 즉시 바인더 클래스를 생성합니다. Android Studio를 사용하지 않는 경우, 다음에 애플리케이션을 빌드할 때 Gradle 도구가 바인더 클래스를 생성합니다. .aidl 파일 작성을 끝내는 즉시 gradle assembleDebug(또는 gradle assembleRelease)로 프로젝트를 빌드해야 코드가 생성된 클래스에 연결할 수 있습니다.

2. 인터페이스 구현

애플리케이션을 빌드할 때 Android SDK 도구는 .aidl 파일 이름을 딴 .java 인터페이스 파일을 생성합니다. 생성되는 인터페이스는 상위 인터페이스의 추상 구현인 Stub이라는 하위 클래스를 포함하며(예: YourInterface.Stub), .aidl 파일에 있는 모든 메서드를 선언합니다.

참고: Stub은 몇 개의 도우미 메서드도 정의합니다. 그중에서 가장 주목할 메서드는 asInterface()이며, 이것은 IBinder 메서드(대개 클라이언트의 onServiceConnected() 콜백 메서드에 전달되는 메서드)를 취하고 스텁 인터페이스의 인스턴스를 반환합니다. 이 캐스트를 만드는 자세한 방법은 IPC 메서드 호출 섹션을 참조하세요.

.aidl로부터 생성된 인터페이스를 구현하려면, 생성된 Binder 인터페이스(예: YourInterface.Stub)를 확장하고 .aidl 파일에서 상속한 메서드를 구현합니다.

아래의 코드는 익명의 인스턴스를 사용하여 (위의 IRemoteService.aidl 예시에서 정의된) IRemoteService라는 인터페이스를 구현하는 예시입니다.

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

이제 binderStub 클래스의 인스턴스이며(Binder), 서비스의 RPC 인터페이스를 정의합니다. 다음 단계에서는 이 인스턴스를 클라이언트에 노출하여 서비스와 상호작용할 수 있도록 합니다.

AIDL 인터페이스를 구현할 때 알아두어야 할 몇 가지 규칙이 있습니다.

  • 들어오는 호출이 메인 스레드에서 실행된다는 보장이 없으므로 처음부터 멀티스레딩을 염두에 두고 서비스가 스레드로부터 안전하도록 적절히 빌드해야 합니다.
  • 기본적으로 RPC 호출은 동기적입니다. 서비스가 요청을 완료하는 데 몇 밀리초 이상 걸린다는 것을 알고 있다면 액티비티의 메인 스레드에서 호출해서는 안 됩니다. 애플리케이션 작동이 중단될 수도 있기 때문입니다(Android가 "애플리케이션이 응답하지 않습니다"라는 메시지를 표시할 수도 있습니다). 클라이언트의 별도 스레드에서 호출해야 합니다.
  • 어떤 예외가 발생해도 호출자에게 되돌려 보내지 않습니다.

3. 클라이언트에게 인터페이스 노출

서비스의 인터페이스를 구현한 후에는 클라이언트가 바인딩할 수 있도록 노출해야 합니다. (이전 섹션에서 설명한 것처럼) 서비스의 인터페이스를 노출하려면 Service를 확장하고 onBind()를 구현하여 생성된 Stub을 구현하는 클래스의 인스턴스를 반환합니다. 다음은 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
        }
    };
}

이제 클라이언트(예: 액티비티)가 이 서비스에 연결하기 위해 bindService()를 호출하면, 해당 클라이언트의 onServiceConnected() 콜백이 서비스의 onBind() 메서드가 반환하는 binder 인스턴스를 받습니다.

클라이언트는 인터페이스 클래스에 대한 액세스 권한도 있어야 하므로 클라이언트와 서비스가 별도의 애플리케이션에 있는 경우, 클라이언트의 애플리케이션은 (AIDL 메서드에 대한 클라이언트 액세스 권한을 제공하는 android.os.Binder 인터페이스를 생성하는) .aidl 파일의 사본이 src/ 디렉토리에 있어야 합니다.

클라이언트가 onServiceConnected() 콜백에서 IBinder를 받으면, YourServiceInterface.Stub.asInterface(service)를 호출하여 반환된 매개변수를 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 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;
    }
};

추가 샘플 코드는 ApiDemosRemoteService.java 클래스를 참조하세요.

IPC를 통해 객체 전달

IPC 인터페이스를 통해 한 프로세스에서 다른 프로세스로 보내려는 클래스가 있다면, 그렇게 할 수 있습니다. 그렇지만 IPC 채널의 상대방에게 클래스의 코드를 제공하고, 클래스는 Parcelable 인터페이스를 지원해야 합니다. Parcelable 인터페이스를 지원하는 것은 중요합니다. Android 시스템이 객체를 프로세스 전체적으로 마샬링할 수 있는 원시 유형으로 해체할 수 있기 때문입니다.

Parcelable 프로토콜을 지원하는 클래스를 만들려면 다음과 같이 해야 합니다.

  1. 클래스에서 Parcelable 인터페이스를 구현합니다.
  2. 객체의 현재 상태를 취해 Parcel에 기록하는 writeToParcel을 구현합니다.
  3. Parcelable.Creator 인터페이스를 구현하는 객체인 클래스에 CREATOR라는 정적 필드를 추가합니다.
  4. 마지막으로, parcelable 클래스를 선언하는 .aidl 파일을 생성합니다(아래의 Rect.aidl 파일 참조).

    사용자 지정 빌드 프로세스를 사용하는 경우, 빌드에 .aidl 파일을 추가하지 마세요. C 언어의 헤더 파일과 마찬가지로 이 .aidl 파일은 컴파일되지 않습니다.

AIDL은 코드에서 이러한 메서드와 필드를 사용하여 객체를 마샬링하거나 마샬링을 취소합니다.

예를 들면 아래는 parcelable 클래스 Rect를 만들기 위한 Rect.aidl 파일입니다.

package android.graphics;

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

그리고 다음은 Rect 클래스가 어떻게 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;
    }
}

Rect 클래스에서의 마샬링은 아주 간단합니다. Parcel의 다른 메서드들을 살펴보고 Parcel에 쓸 수 있는 다른 종류의 값이 있는지 확인해보세요.

경고: 다른 프로세스로부터 데이터를 수신하면 보안에 영향을 줄 수 있다는 점에 유의하세요. 이 경우 RectParcel로부터 4개의 숫자를 읽어오는데, 이 값이 호출자가 의도하는 값의 범위 내에 있는지 확인하는 것은 개발자의 몫입니다. 보안 및 권한에서 애플리케이션을 멀웨어로부터 안전하게 지키는 방법에 대해 알아보세요.

Parcelable이 포함된 번들 인수를 사용한 메서드

aidl 인터페이스에 번들을 인수로 받아들이는 메서드가 포함되어 있고 그 번들에 parcelable이 포함될 것으로 예상되는 경우, 해당 번들에서 읽기 작업을 시도하기 전에 Bundle.setClassLoader(ClassLoader)를 호출하여 번들에 classloader를 설정했는지 확인해야 합니다. 그렇지 않으면 parcelable이 애플리케이션에 올바르게 정의되어 있더라도 ClassNotFoundException가 발생하게 됩니다. 예를 들어,

.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);
}
아래의 구현에서 확인하였듯이 ClassLoaderRect를 읽기 전에 Bundle에서 명시적으로 설정됩니다.

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

IPC 메서드 호출

AIDL로 정의한 원격 인터페이스를 호출할 때, 호출하는 클래스는 반드시 다음 단계를 따라야 합니다.

  1. 프로젝트 src/ 디렉토리의 .aidl 파일을 포함합니다.
  2. AIDL을 기반으로 생성된 IBinder 인터페이스의 인스턴스를 선언합니다.
  3. ServiceConnection을 구현합니다.
  4. Context.bindService()를 호출하고 ServiceConnection 구현에 전달합니다.
  5. onServiceConnected() 구현에서 IBinder 인스턴스(service)를 받게 됩니다. YourInterfaceName.Stub.asInterface((IBinder)service)를 호출하여 반환된 매개변수를 YourInterface 유형으로 캐스팅합니다.
  6. 인터페이스에서 정의한 메서드를 호출합니다. 항상 DeadObjectException 예외를 트랩해야 합니다. 이 예외는 연결이 끊어지면 발생합니다. SecurityException 예외도 트랩해야 합니다. 이 예외는 IPC 메서드에 포함된 두 개의 프로세스에 상충하는 AIDL 정의가 있을 때 발생합니다.
  7. 연결을 끊으려면 인터페이스의 인스턴스를 사용하여 Context.unbindService()를 호출합니다.

IPC 서비스 호출에 대한 몇 가지 코멘트:

  • 객체는 여러 프로세스에 걸쳐 카운트되는 참조입니다.
  • 익명의 객체를 메서드 인수로 보낼 수 있습니다.

서비스에 바인딩하는 자세한 방법은 바인딩된 서비스 문서를 참조하세요.

다음은 AIDL로 생성된 서비스 호출을 보여주는 몇 가지 샘플 코드로, ApiDemos 프로젝트의 Remote Service 샘플에서 발췌했습니다.

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