Import SettingsLib/Ipc library

Bug: 365886533
Flag: EXEMPT Import library
Test: Local build
Change-Id: I467c97d5f6e0f40204d9d379b8865e3f9913d82d
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp
new file mode 100644
index 0000000..61adb46
--- /dev/null
+++ b/packages/SettingsLib/Ipc/Android.bp
@@ -0,0 +1,22 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "SettingsLibIpc-srcs",
+    srcs: ["src/**/*.kt"],
+}
+
+android_library {
+    name: "SettingsLibIpc",
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+    srcs: [":SettingsLibIpc-srcs"],
+    static_libs: [
+        "androidx.collection_collection",
+        "guava",
+        "kotlinx-coroutines-android",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Ipc/AndroidManifest.xml b/packages/SettingsLib/Ipc/AndroidManifest.xml
new file mode 100644
index 0000000..fc48a7d
--- /dev/null
+++ b/packages/SettingsLib/Ipc/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.ipc">
+
+    <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Ipc/README.md b/packages/SettingsLib/Ipc/README.md
new file mode 100644
index 0000000..ea2c3a1b5
--- /dev/null
+++ b/packages/SettingsLib/Ipc/README.md
@@ -0,0 +1,116 @@
+# Service IPC library
+
+This library provides a kind of IPC (inter-process communication) framework
+based on Android
+[bound service](https://developer.android.com/develop/background-work/services/bound-services)
+with [Messenger](https://developer.android.com/reference/android/os/Messenger).
+
+Following benefits are offered by the library to improve and simplify IPC
+development:
+
+-   Enforce permission check for every API implementation to avoid security
+    vulnerability.
+-   Allow modular API development for better code maintenance (no more huge
+    Service class).
+-   Prevent common mistakes, e.g. Service context leaking, ServiceConnection
+    management.
+
+## Overview
+
+In this manner of IPC,
+[Service](https://developer.android.com/reference/android/app/Service) works
+with [Handler](https://developer.android.com/reference/android/os/Handler) to
+deal with different types of
+[Message](https://developer.android.com/reference/android/os/Message) objects.
+
+Under the hood, each API is represented as a `Message` object:
+
+-   [what](https://developer.android.com/reference/android/os/Message#what):
+    used to identify API.
+-   [data](https://developer.android.com/reference/android/os/Message#getData\(\)):
+    payload of the API parameters and result.
+
+This could be mapped to the `ApiHandler` interface abstraction exactly.
+Specifically, the API implementation needs to provide:
+
+-   An unique id for the API.
+-   How to marshall/unmarshall the request and response.
+-   Whether the given request is permitted.
+
+## Threading model
+
+`MessengerService` starts a dedicated
+[HandlerThread](https://developer.android.com/reference/android/os/HandlerThread)
+to handle requests. `ApiHandler` implementation uses Kotlin `suspend`, which
+allows flexible threading model on top of the
+[Kotlin coroutines](https://kotlinlang.org/docs/coroutines-overview.html).
+
+## Usage
+
+The service provider should extend `MessengerService` and provide API
+implementations. In `AndroidManifest.xml`, declare `<service>` with permission,
+intent filter, etc. if needed.
+
+Meanwhile, the service client implements `MessengerServiceClient` with API
+descriptors to make requests.
+
+Here is an example:
+
+```kotlin
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import kotlinx.coroutines.runBlocking
+
+class EchoService :
+  MessengerService(
+    listOf(EchoApiImpl),
+    PermissionChecker { _, _, _ -> true },
+  )
+
+class EchoServiceClient(context: Context) : MessengerServiceClient(context) {
+  override val serviceIntentFactory: () -> Intent
+    get() = { Intent("example.intent.action.ECHO") }
+
+  fun echo(data: String?): String? =
+    runBlocking { invoke(context.packageName, EchoApi, data).await() }
+}
+
+object EchoApi : ApiDescriptor<String?, String?> {
+  private val codec =
+    object : MessageCodec<String?> {
+      override fun encode(data: String?) =
+        Bundle(1).apply { putString("data", data) }
+
+      override fun decode(data: Bundle): String? = data.getString("data", null)
+    }
+
+  override val id: Int
+    get() = 1
+
+  override val requestCodec: MessageCodec<String?>
+    get() = codec
+
+  override val responseCodec: MessageCodec<String?>
+    get() = codec
+}
+
+// This is not needed by EchoServiceClient.
+object EchoApiImpl : ApiHandler<String?, String?>,
+                     ApiDescriptor<String?, String?> by EchoApi {
+  override suspend fun invoke(
+    application: Application,
+    myUid: Int,
+    callingUid: Int,
+    request: String?,
+  ): String? = request
+
+  override fun hasPermission(
+    application: Application,
+    myUid: Int,
+    callingUid: Int,
+    request: String?,
+  ): Boolean = (request?.length ?: 0) <= 5
+}
+```
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiException.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiException.kt
new file mode 100644
index 0000000..42772a4
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiException.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+/** Exception raised when handle request. */
+sealed class ApiException : Exception {
+    constructor() : super()
+
+    constructor(cause: Throwable?) : super(cause)
+
+    constructor(message: String, cause: Throwable?) : super(message, cause)
+}
+
+/** Exception occurred on client side. */
+open class ApiClientException : ApiException {
+    constructor() : super()
+
+    constructor(cause: Throwable?) : super(cause)
+
+    constructor(message: String, cause: Throwable?) : super(message, cause)
+}
+
+/** Client has already been closed. */
+class ClientClosedException : ApiClientException()
+
+/** Api to request is invalid, e.g. negative identity number. */
+class ClientInvalidApiException(message: String) : ApiClientException(message, null)
+
+/**
+ * Exception when bind service failed.
+ *
+ * This exception may be raised for following reasons:
+ * - Context used to bind service has finished its lifecycle (e.g. activity stopped).
+ * - Service not found.
+ * - Permission denied.
+ */
+class ClientBindServiceException(cause: Throwable?) : ApiClientException(cause)
+
+/** Exception when encode request. */
+class ClientEncodeException(cause: Throwable) : ApiClientException(cause)
+
+/** Exception when decode response. */
+class ClientDecodeException(cause: Throwable) : ApiClientException(cause)
+
+/** Exception when send message. */
+class ClientSendException(message: String, cause: Throwable) : ApiClientException(message, cause)
+
+/** Service returns unknown error code. */
+class ClientUnknownResponseCodeException(code: Int) :
+    ApiClientException("Unknown code: $code", null)
+
+/** Exception returned from service. */
+open class ApiServiceException : ApiException() {
+    companion object {
+        internal const val CODE_OK = 0
+        internal const val CODE_PERMISSION_DENIED = 1
+        internal const val CODE_UNKNOWN_API = 2
+        internal const val CODE_INTERNAL_ERROR = 3
+
+        internal fun of(code: Int): ApiServiceException? =
+            when (code) {
+                CODE_PERMISSION_DENIED -> ServicePermissionDeniedException()
+                CODE_UNKNOWN_API -> ServiceUnknownApiException()
+                CODE_INTERNAL_ERROR -> ServiceInternalException()
+                else -> null
+            }
+    }
+}
+
+/** Exception indicates the request is rejected due to permission deny. */
+class ServicePermissionDeniedException : ApiServiceException()
+
+/** Exception indicates API request is unknown. */
+class ServiceUnknownApiException : ApiServiceException()
+
+/** Exception indicates internal issue occurred when service handles the request. */
+class ServiceInternalException : ApiServiceException()
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
new file mode 100644
index 0000000..802141d
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/ApiHandler.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.app.Application
+import android.os.Bundle
+
+/**
+ * Codec to marshall/unmarshall data between given type and [Bundle].
+ *
+ * The implementation must be threadsafe and stateless.
+ */
+interface MessageCodec<T> {
+    /** Converts given data to [Bundle]. */
+    fun encode(data: T): Bundle
+
+    /** Converts [Bundle] to an object of given data type. */
+    fun decode(data: Bundle): T
+}
+
+/**
+ * Descriptor of API.
+ *
+ * Used by both [MessengerService] and [MessengerServiceClient] to identify API and encode/decode
+ * messages.
+ */
+interface ApiDescriptor<Request, Response> {
+    /**
+     * Identity of the API.
+     *
+     * The id must be:
+     * - Positive: the negative numbers are reserved for internal messages.
+     * - Unique within the [MessengerService].
+     * - Permanent to achieve backward compatibility.
+     */
+    val id: Int
+
+    /** Codec for request. */
+    val requestCodec: MessageCodec<Request>
+
+    /** Codec for response. */
+    val responseCodec: MessageCodec<Response>
+}
+
+/**
+ * Handler of API.
+ *
+ * This is the API implementation portion, which is used by [MessengerService] only.
+ * [MessengerServiceClient] does NOT need this interface at all to make request.
+ *
+ * The implementation must be threadsafe.
+ */
+interface ApiHandler<Request, Response> : ApiDescriptor<Request, Response> {
+    /**
+     * Returns if the request is permitted.
+     *
+     * @return `false` if permission is denied, otherwise `true`
+     */
+    fun hasPermission(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: Request,
+    ): Boolean
+
+    /**
+     * Invokes the API.
+     *
+     * The API is invoked from Service handler thread, do not perform time-consuming task. Start
+     * coroutine in another thread if it takes time to complete.
+     */
+    suspend fun invoke(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: Request,
+    ): Response
+}
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessageCodecs.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessageCodecs.kt
new file mode 100644
index 0000000..4b7572b
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessageCodecs.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.os.Bundle
+
+/** [MessageCodec] for [Int]. */
+object IntMessageCodec : MessageCodec<Int> {
+    override fun encode(data: Int): Bundle = Bundle(1).apply { putInt(null, data) }
+
+    override fun decode(data: Bundle): Int = data.getInt(null)
+}
+
+/** [MessageCodec] for [Set<Int>]. */
+class IntSetMessageCodec : MessageCodec<Set<Int>> {
+    override fun encode(data: Set<Int>): Bundle =
+        Bundle(1).apply { putIntArray(null, data.toIntArray()) }
+
+    override fun decode(data: Bundle): Set<Int> = data.getIntArray(null)?.toSet() ?: setOf()
+}
+
+/** [MessageCodec] for [Set<String>]. */
+class StringSetMessageCodec : MessageCodec<Set<String>> {
+    override fun encode(data: Set<String>): Bundle =
+        Bundle(1).apply { putStringArray(null, data.toTypedArray()) }
+
+    override fun decode(data: Bundle): Set<String> = data.getStringArray(null)?.toSet() ?: setOf()
+}
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerService.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerService.kt
new file mode 100644
index 0000000..0bdae38
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerService.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.app.Application
+import android.app.Service
+import android.content.Intent
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import android.os.Process
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.android.asCoroutineDispatcher
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+
+/**
+ * [Messenger] based bound service for IPC.
+ *
+ * A dedicated [HandlerThread] is created to handle all requests.
+ *
+ * @param apiHandlers API handlers associated with the service
+ * @param permissionChecker Checker for permission
+ * @param name name of the handler thread
+ */
+open class MessengerService(
+    private val apiHandlers: List<ApiHandler<*, *>>,
+    private val permissionChecker: PermissionChecker,
+    name: String = TAG,
+) : Service() {
+    @VisibleForTesting internal val handlerThread = HandlerThread(name)
+    @VisibleForTesting internal lateinit var handler: IncomingHandler
+    private lateinit var messenger: Messenger
+
+    override fun onCreate() {
+        super.onCreate()
+        handlerThread.start()
+        handler =
+            IncomingHandler(
+                handlerThread.looper,
+                applicationContext as Application,
+                apiHandlers.toSortedArray(),
+                permissionChecker,
+            )
+        messenger = Messenger(handler)
+        Log.i(TAG, "onCreate HandlerThread ${handlerThread.threadId}")
+    }
+
+    override fun onBind(intent: Intent): IBinder? {
+        // this method is executed only once even there is more than 1 client
+        Log.i(TAG, "onBind $intent")
+        return messenger.binder
+    }
+
+    override fun onUnbind(intent: Intent): Boolean {
+        // invoked when ALL clients are unbound
+        Log.i(TAG, "onUnbind $intent")
+        handler.coroutineScope.cancel()
+        return super.onUnbind(intent)
+    }
+
+    override fun onDestroy() {
+        Log.i(TAG, "onDestroy HandlerThread ${handlerThread.threadId}")
+        handlerThread.quitSafely()
+        super.onDestroy()
+    }
+
+    @VisibleForTesting
+    internal class IncomingHandler(
+        looper: Looper,
+        private val application: Application,
+        private val apiHandlers: Array<ApiHandler<*, *>>,
+        private val permissionChecker: PermissionChecker,
+    ) : Handler(looper) {
+        @VisibleForTesting internal val myUid = Process.myUid()
+        val coroutineScope = CoroutineScope(asCoroutineDispatcher().immediate + SupervisorJob())
+
+        override fun handleMessage(msg: Message) {
+            coroutineScope.launch { handle(msg) }
+        }
+
+        @VisibleForTesting
+        internal suspend fun handle(msg: Message) {
+            Log.d(TAG, "receive request $msg")
+            val replyTo = msg.replyTo
+            if (replyTo == null) {
+                Log.e(TAG, "Ignore msg without replyTo: $msg")
+                return
+            }
+            val apiId = msg.what
+            val txnId = msg.arg1
+            val callingUid = msg.sendingUid
+            val data = msg.data
+            // WARNING: never access "msg" beyond this point as it may be recycled by Looper
+            val response = Message.obtain(null, apiId, txnId, ApiServiceException.CODE_OK)
+            try {
+                if (permissionChecker.check(application, myUid, callingUid)) {
+                    @Suppress("UNCHECKED_CAST")
+                    val apiHandler = findApiHandler(apiId) as? ApiHandler<Any, Any>
+                    if (apiHandler != null) {
+                        val request = apiHandler.requestCodec.decode(data)
+                        if (apiHandler.hasPermission(application, myUid, callingUid, request)) {
+                            val result = apiHandler.invoke(application, myUid, callingUid, request)
+                            response.data = apiHandler.responseCodec.encode(result)
+                        } else {
+                            response.arg2 = ApiServiceException.CODE_PERMISSION_DENIED
+                        }
+                    } else {
+                        response.arg2 = ApiServiceException.CODE_UNKNOWN_API
+                        Log.e(TAG, "Unknown request [txnId=$txnId,apiId=$apiId]")
+                    }
+                } else {
+                    response.arg2 = ApiServiceException.CODE_PERMISSION_DENIED
+                }
+            } catch (e: Exception) {
+                response.arg2 = ApiServiceException.CODE_INTERNAL_ERROR
+                Log.e(TAG, "Internal error when handle [txnId=$txnId,apiId=$apiId]", e)
+            }
+            try {
+                replyTo.send(response)
+            } catch (e: Exception) {
+                Log.w(TAG, "Fail to send response for [txnId=$txnId,apiId=$apiId]", e)
+                // nothing to do
+            }
+        }
+
+        @VisibleForTesting
+        internal fun findApiHandler(id: Int): ApiHandler<*, *>? {
+            var low = 0
+            var high = apiHandlers.size
+            while (low < high) {
+                val mid = (low + high).ushr(1) // safe from overflows
+                val api = apiHandlers[mid]
+                when {
+                    api.id < id -> low = mid + 1
+                    api.id > id -> high = mid
+                    else -> return api
+                }
+            }
+            return null
+        }
+    }
+
+    companion object {
+        @VisibleForTesting internal const val TAG = "MessengerService"
+    }
+}
+
+@VisibleForTesting
+internal fun List<ApiHandler<*, *>>.toSortedArray() =
+    toTypedArray().also { array ->
+        if (array.isEmpty()) return@also
+        array.sortBy { it.id }
+        if (array[0].id < 0) throw IllegalArgumentException("negative id: ${array[0]}")
+        for (index in 1 until array.size) {
+            if (array[index - 1].id == array[index].id) {
+                throw IllegalArgumentException("conflict id: ${array[index - 1]} ${array[index]}")
+            }
+        }
+    }
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
new file mode 100644
index 0000000..7ffefed
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MessengerServiceClient.kt
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.Bundle
+import android.os.DeadObjectException
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.IBinder
+import android.os.Looper
+import android.os.Message
+import android.os.Messenger
+import android.util.Log
+import androidx.annotation.OpenForTesting
+import androidx.annotation.VisibleForTesting
+import androidx.collection.ArrayMap
+import com.google.common.base.Ticker
+import java.util.concurrent.atomic.AtomicInteger
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CompletionHandler
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.DisposableHandle
+
+/**
+ * Client to communicate with [MessengerService].
+ *
+ * A dedicated [HandlerThread] is created to handle requests **sequentially**, there is only one
+ * ongoing request per package.
+ *
+ * Must call [close] before [context] is destroyed to avoid context leaking. Note that
+ * [MessengerService] is automatically unbound when context lifecycle is stopped. Further request
+ * will result in service binding exception.
+ *
+ * @param context context used for service binding, note that context lifecycle affects the IPC
+ *   service lifecycle
+ * @param serviceConnectionIdleMs idle time in milliseconds before closing the service connection
+ * @param name name of the handler thread
+ */
+abstract class MessengerServiceClient
+@JvmOverloads
+constructor(
+    protected val context: Context,
+    private val serviceConnectionIdleMs: Long = 30000L,
+    name: String = TAG,
+    private val metricsLogger: MetricsLogger? = null,
+) : AutoCloseable {
+    /** Per package [ServiceConnection]. */
+    @VisibleForTesting internal val messengers = ArrayMap<String, Connection>()
+    private val handlerThread = HandlerThread(name)
+    @VisibleForTesting internal val handler: Handler
+
+    init {
+        handlerThread.start()
+        val looper = handlerThread.looper
+        handler = Handler(looper)
+    }
+
+    /**
+     * Factory for service [Intent] creation.
+     *
+     * A typical implementation is create [Intent] with specific action.
+     */
+    protected abstract val serviceIntentFactory: () -> Intent
+
+    override fun close() = close(true)
+
+    fun close(join: Boolean) {
+        handler.post {
+            val exception = ClientClosedException()
+            val connections = messengers.values.toTypedArray()
+            for (connection in connections) connection.close(exception)
+        }
+        handlerThread.quitSafely()
+        if (join) handlerThread.join()
+    }
+
+    /**
+     * Invokes given API.
+     *
+     * @param packageName package name of the target service
+     * @param apiDescriptor descriptor of API
+     * @param request request parameter
+     * @return Deferred object of the response, which could be used for [Deferred.await],
+     *   [Deferred.cancel], etc.
+     * @exception ApiException
+     */
+    // TODO: support timeout
+    fun <Request, Response> invoke(
+        packageName: String,
+        apiDescriptor: ApiDescriptor<Request, Response>,
+        request: Request,
+    ): Deferred<Response> {
+        if (apiDescriptor.id < 0) throw ClientInvalidApiException("Invalid id: ${apiDescriptor.id}")
+        if (
+            packageName == context.packageName &&
+                Looper.getMainLooper().thread === Thread.currentThread()
+        ) {
+            // Deadlock as it might involve service creation, which requires main thread
+            throw IllegalStateException("Invoke on main thread causes deadlock")
+        }
+        val wrapper = RequestWrapper(packageName, apiDescriptor, request, txnId.getAndIncrement())
+        metricsLogger?.run {
+            wrapper.logIpcEvent(this, IpcEvent.ENQUEUED)
+            wrapper.deferred.invokeOnCompletion {
+                wrapper.logIpcEvent(this, IpcEvent.COMPLETED, it)
+            }
+        }
+        if (!handler.post { getConnection(packageName).enqueueRequest(wrapper) }) {
+            wrapper.completeExceptionally(ClientClosedException())
+        }
+        return wrapper.deferred
+    }
+
+    private fun getConnection(packageName: String) =
+        messengers.getOrPut(packageName) {
+            Connection(
+                handler.looper,
+                context,
+                packageName,
+                serviceConnectionIdleMs,
+                serviceIntentFactory,
+                messengers,
+                metricsLogger,
+            )
+        }
+
+    @VisibleForTesting
+    internal data class RequestWrapper<Request, Response>(
+        val packageName: String,
+        val apiDescriptor: ApiDescriptor<Request, Response>,
+        val request: Request,
+        val txnId: Int,
+        val deferred: CompletableDeferred<Response> = CompletableDeferred(),
+    ) {
+        val data: Bundle
+            get() = request.let { apiDescriptor.requestCodec.encode(it) }
+
+        fun completeExceptionally(e: Exception) {
+            deferred.completeExceptionally(e)
+        }
+
+        fun logIpcEvent(
+            metricsLogger: MetricsLogger,
+            event: @IpcEvent Int,
+            cause: Throwable? = null,
+        ) {
+            try {
+                metricsLogger.logIpcEvent(
+                    packageName,
+                    txnId,
+                    apiDescriptor.id,
+                    event,
+                    cause,
+                    ticker.read(),
+                )
+            } catch (e: Exception) {
+                Log.e(TAG, "fail to log ipc event: $event", e)
+            }
+        }
+    }
+
+    // NOTE: All ServiceConnection callbacks are invoked from main thread.
+    @OpenForTesting
+    @VisibleForTesting
+    internal open class Connection(
+        looper: Looper,
+        private val context: Context,
+        private val packageName: String,
+        private val serviceConnectionIdleMs: Long,
+        private val serviceIntentFactory: () -> Intent,
+        private val messengers: ArrayMap<String, Connection>,
+        private val metricsLogger: MetricsLogger?,
+    ) : Handler(looper), ServiceConnection {
+        private val clientMessenger = Messenger(this)
+        internal val pendingRequests = ArrayDeque<RequestWrapper<*, *>>()
+        internal var serviceMessenger: Messenger? = null
+        internal open var connectionState: Int = STATE_INIT
+
+        internal var disposableHandle: DisposableHandle? = null
+        private val requestCompletionHandler =
+            object : CompletionHandler {
+                override fun invoke(cause: Throwable?) {
+                    sendEmptyMessage(MSG_CHECK_REQUEST_STATE)
+                }
+            }
+
+        override fun handleMessage(msg: Message) {
+            if (msg.what < 0) {
+                handleClientMessage(msg)
+                return
+            }
+            Log.d(TAG, "receive response $msg")
+            val request = pendingRequests.removeFirstOrNull()
+            if (request == null) {
+                Log.w(TAG, "Pending request is empty when got response")
+                return
+            }
+            if (msg.arg1 != request.txnId || request.apiDescriptor.id != msg.what) {
+                Log.w(TAG, "Mismatch ${request.apiDescriptor.id}, response=$msg")
+                // add request back for retry
+                pendingRequests.addFirst(request)
+                return
+            }
+            handleServiceMessage(request, msg)
+        }
+
+        internal open fun handleClientMessage(msg: Message) {
+            when (msg.what) {
+                MSG_ON_SERVICE_CONNECTED -> {
+                    if (connectionState == STATE_BINDING) {
+                        connectionState = STATE_CONNECTED
+                        serviceMessenger = Messenger(msg.obj as IBinder)
+                        drainPendingRequests()
+                    } else {
+                        Log.w(TAG, "Got onServiceConnected when state is $connectionState")
+                    }
+                }
+                MSG_REBIND_SERVICE -> {
+                    if (pendingRequests.isEmpty()) {
+                        removeMessages(MSG_CLOSE_ON_IDLE)
+                        close(null)
+                    } else {
+                        // died when binding, reset state for rebinding
+                        if (msg.obj != null && connectionState == STATE_BINDING) {
+                            connectionState = STATE_CONNECTED
+                        }
+                        rebindService()
+                    }
+                }
+                MSG_CLOSE_ON_IDLE -> {
+                    if (pendingRequests.isEmpty()) close(null)
+                }
+                MSG_CHECK_REQUEST_STATE -> {
+                    val request = pendingRequests.firstOrNull()
+                    if (request != null && request.deferred.isCompleted) {
+                        drainPendingRequests()
+                    }
+                }
+                else -> Log.e(TAG, "Unknown msg: $msg")
+            }
+        }
+
+        internal open fun handleServiceMessage(request: RequestWrapper<*, *>, response: Message) {
+            @Suppress("UNCHECKED_CAST") val deferred = request.deferred as CompletableDeferred<Any?>
+            if (deferred.isCompleted) {
+                drainPendingRequests()
+                return
+            }
+            metricsLogger?.let { request.logIpcEvent(it, IpcEvent.RESPONSE_RECEIVED) }
+            disposableHandle?.dispose()
+            if (response.arg2 == ApiServiceException.CODE_OK) {
+                try {
+                    deferred.complete(request.apiDescriptor.responseCodec.decode(response.data))
+                } catch (e: Exception) {
+                    request.completeExceptionally(ClientDecodeException(e))
+                }
+            } else {
+                val errorCode = response.arg2
+                val exception = ApiServiceException.of(errorCode)
+                if (exception != null) {
+                    request.completeExceptionally(exception)
+                } else {
+                    request.completeExceptionally(ClientUnknownResponseCodeException(errorCode))
+                }
+            }
+            drainPendingRequests()
+        }
+
+        fun enqueueRequest(request: RequestWrapper<*, *>) {
+            if (connectionState == STATE_CLOSED) {
+                request.completeExceptionally(ClientClosedException())
+                return
+            }
+            pendingRequests.add(request)
+            if (pendingRequests.size == 1) {
+                removeMessages(MSG_CLOSE_ON_IDLE)
+                drainPendingRequests()
+            }
+        }
+
+        override fun onServiceConnected(name: ComponentName, service: IBinder) {
+            Log.i(TAG, "onServiceConnected $name")
+            metricsLogger?.logServiceEvent(ServiceEvent.ON_SERVICE_CONNECTED)
+            sendMessage(obtainMessage(MSG_ON_SERVICE_CONNECTED, service))
+        }
+
+        override fun onServiceDisconnected(name: ComponentName) {
+            // Service process crashed or killed, the connection remains alive, will receive
+            // onServiceConnected when the Service is next running
+            Log.i(TAG, "onServiceDisconnected $name")
+            metricsLogger?.logServiceEvent(ServiceEvent.ON_SERVICE_DISCONNECTED)
+            sendMessage(obtainMessage(MSG_REBIND_SERVICE))
+        }
+
+        override fun onBindingDied(name: ComponentName) {
+            Log.i(TAG, "onBindingDied $name")
+            metricsLogger?.logServiceEvent(ServiceEvent.ON_BINDING_DIED)
+            // When service is connected and peer happens to be updated, both onServiceDisconnected
+            // and onBindingDied callbacks are invoked.
+            if (!hasMessages(MSG_REBIND_SERVICE)) {
+                sendMessage(obtainMessage(MSG_REBIND_SERVICE, true))
+            }
+        }
+
+        internal open fun drainPendingRequests() {
+            disposableHandle = null
+            if (pendingRequests.isEmpty()) {
+                closeOnIdle(serviceConnectionIdleMs)
+                return
+            }
+            val serviceMessenger = this.serviceMessenger
+            if (serviceMessenger == null) {
+                bindService()
+                return
+            }
+            do {
+                val request = pendingRequests.first()
+                if (request.deferred.isCompleted) {
+                    pendingRequests.removeFirst()
+                } else {
+                    sendServiceMessage(serviceMessenger, request)
+                    return
+                }
+            } while (pendingRequests.isNotEmpty())
+            closeOnIdle(serviceConnectionIdleMs)
+        }
+
+        internal open fun closeOnIdle(idleMs: Long) {
+            if (idleMs <= 0 || !sendEmptyMessageDelayed(MSG_CLOSE_ON_IDLE, idleMs)) {
+                close(null)
+            }
+        }
+
+        internal open fun sendServiceMessage(
+            serviceMessenger: Messenger,
+            request: RequestWrapper<*, *>,
+        ) {
+            fun completeExceptionally(exception: Exception) {
+                pendingRequests.removeFirst()
+                request.completeExceptionally(exception)
+                drainPendingRequests()
+            }
+            val message =
+                obtainMessage(request.apiDescriptor.id, request.txnId, 0).apply {
+                    replyTo = clientMessenger
+                }
+            try {
+                message.data = request.data
+            } catch (e: Exception) {
+                completeExceptionally(ClientEncodeException(e))
+                return
+            }
+            Log.d(TAG, "send $message")
+            try {
+                sendServiceMessage(serviceMessenger, message)
+                metricsLogger?.let { request.logIpcEvent(it, IpcEvent.REQUEST_SENT) }
+                disposableHandle = request.deferred.invokeOnCompletion(requestCompletionHandler)
+            } catch (e: DeadObjectException) {
+                Log.w(TAG, "Got DeadObjectException")
+                rebindService()
+            } catch (e: Exception) {
+                completeExceptionally(ClientSendException("Fail to send $message", e))
+            }
+        }
+
+        @Throws(Exception::class)
+        internal open fun sendServiceMessage(serviceMessenger: Messenger, message: Message) =
+            serviceMessenger.send(message)
+
+        internal fun bindService() {
+            if (connectionState == STATE_BINDING || connectionState == STATE_CLOSED) {
+                Log.w(TAG, "Ignore bindService $packageName, state: $connectionState")
+                return
+            }
+            connectionState = STATE_BINDING
+            Log.i(TAG, "bindService $packageName")
+            val intent = serviceIntentFactory.invoke()
+            intent.setPackage(packageName)
+            metricsLogger?.logServiceEvent(ServiceEvent.BIND_SERVICE)
+            bindService(intent)?.let { close(it) }
+        }
+
+        private fun bindService(intent: Intent): Exception? =
+            try {
+                if (context.bindService(intent, this, Context.BIND_AUTO_CREATE)) {
+                    null
+                } else {
+                    ClientBindServiceException(null)
+                }
+            } catch (e: Exception) {
+                ClientBindServiceException(e)
+            }
+
+        internal open fun rebindService() {
+            Log.i(TAG, "rebindService $packageName")
+            metricsLogger?.logServiceEvent(ServiceEvent.REBIND_SERVICE)
+            unbindService()
+            bindService()
+        }
+
+        internal fun close(exception: Exception?) {
+            Log.i(TAG, "close connection $packageName", exception)
+            connectionState = STATE_CLOSED
+            messengers.remove(packageName, this)
+            unbindService()
+            if (pendingRequests.isNotEmpty()) {
+                val reason = exception ?: ClientClosedException()
+                do {
+                    pendingRequests.removeFirst().deferred.completeExceptionally(reason)
+                } while (pendingRequests.isNotEmpty())
+            }
+        }
+
+        private fun unbindService() {
+            disposableHandle?.dispose()
+            disposableHandle = null
+            serviceMessenger = null
+            metricsLogger?.logServiceEvent(ServiceEvent.UNBIND_SERVICE)
+            try {
+                // "IllegalArgumentException: Service not registered" may be raised when peer app is
+                // just updated (e.g. upgraded)
+                context.unbindService(this)
+            } catch (e: Exception) {
+                Log.w(TAG, "exception raised when unbindService", e)
+            }
+        }
+
+        private fun MetricsLogger.logServiceEvent(event: @ServiceEvent Int) {
+            try {
+                logServiceEvent(packageName, event, ticker.read())
+            } catch (e: Exception) {
+                Log.e(TAG, "fail to log service event: $event", e)
+            }
+        }
+    }
+
+    companion object {
+        private const val TAG = "MessengerServiceClient"
+        private val ticker: Ticker by lazy { Ticker.systemTicker() }
+
+        @VisibleForTesting internal const val STATE_INIT = 0
+        @VisibleForTesting internal const val STATE_BINDING = 1
+        @VisibleForTesting internal const val STATE_CONNECTED = 2
+        @VisibleForTesting internal const val STATE_CLOSED = 3
+
+        @VisibleForTesting internal const val MSG_ON_SERVICE_CONNECTED = -1
+        @VisibleForTesting internal const val MSG_REBIND_SERVICE = -2
+        @VisibleForTesting internal const val MSG_CLOSE_ON_IDLE = -3
+        @VisibleForTesting internal const val MSG_CHECK_REQUEST_STATE = -4
+
+        @VisibleForTesting internal val txnId = AtomicInteger()
+    }
+}
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MetricsLogger.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MetricsLogger.kt
new file mode 100644
index 0000000..795a920
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/MetricsLogger.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import androidx.annotation.IntDef
+
+/** Interface for metrics logging. */
+interface MetricsLogger {
+
+    /**
+     * Logs service connection event.
+     *
+     * @param packageName package name of the service connection
+     * @param event service event type
+     * @param elapsedRealtimeNanos nanoseconds since boot, including time spent in sleep
+     * @see [android.os.SystemClock.elapsedRealtimeNanos]
+     */
+    fun logServiceEvent(packageName: String, event: @ServiceEvent Int, elapsedRealtimeNanos: Long)
+
+    /**
+     * Logs ipc call event.
+     *
+     * @param packageName package name of the service connection
+     * @param txnId unique transaction id of the ipc call
+     * @param ipc ipc API id
+     * @param event ipc event type
+     * @param cause cause when ipc request completed, provided only when [event] is
+     *   [IpcEvent.COMPLETED]
+     * @param elapsedRealtimeNanos nanoseconds since boot, including time spent in sleep
+     * @see [android.os.SystemClock.elapsedRealtimeNanos]
+     */
+    fun logIpcEvent(
+        packageName: String,
+        txnId: Int,
+        ipc: Int,
+        event: Int,
+        cause: Throwable?,
+        elapsedRealtimeNanos: Long,
+    )
+}
+
+/** Service connection events (for client). */
+@Target(AnnotationTarget.TYPE)
+@IntDef(
+    ServiceEvent.BIND_SERVICE,
+    ServiceEvent.UNBIND_SERVICE,
+    ServiceEvent.REBIND_SERVICE,
+    ServiceEvent.ON_SERVICE_CONNECTED,
+    ServiceEvent.ON_SERVICE_DISCONNECTED,
+    ServiceEvent.ON_BINDING_DIED,
+)
+@Retention(AnnotationRetention.SOURCE)
+annotation class ServiceEvent {
+    companion object {
+        /** Event of [android.content.Context.bindService] call. */
+        const val BIND_SERVICE = 0
+
+        /** Event of [android.content.Context.unbindService] call. */
+        const val UNBIND_SERVICE = 1
+
+        /** Event to rebind service. */
+        const val REBIND_SERVICE = 2
+
+        /** Event of [android.content.ServiceConnection.onServiceConnected] callback. */
+        const val ON_SERVICE_CONNECTED = 3
+
+        /** Event of [android.content.ServiceConnection.onServiceDisconnected] callback. */
+        const val ON_SERVICE_DISCONNECTED = 4
+
+        /** Event of [android.content.ServiceConnection.onBindingDied] callback. */
+        const val ON_BINDING_DIED = 5
+    }
+}
+
+/** Events of a ipc call. */
+@Target(AnnotationTarget.TYPE)
+@IntDef(IpcEvent.ENQUEUED, IpcEvent.REQUEST_SENT, IpcEvent.RESPONSE_RECEIVED, IpcEvent.COMPLETED)
+@Retention(AnnotationRetention.SOURCE)
+annotation class IpcEvent {
+    companion object {
+        /** Event of IPC request enqueued. */
+        const val ENQUEUED = 0
+
+        /** Event of IPC request has been sent to service. */
+        const val REQUEST_SENT = 1
+
+        /** Event of IPC response received from service. */
+        const val RESPONSE_RECEIVED = 2
+
+        /** Event of IPC request completed. */
+        const val COMPLETED = 3
+    }
+}
diff --git a/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/PermissionChecker.kt b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/PermissionChecker.kt
new file mode 100644
index 0000000..da9c955
--- /dev/null
+++ b/packages/SettingsLib/Ipc/src/com/android/settingslib/ipc/PermissionChecker.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.app.Application
+import android.content.pm.PackageManager
+import androidx.collection.mutableIntIntMapOf
+
+/** Checker for permission. */
+fun interface PermissionChecker {
+    /**
+     * Checks permission.
+     *
+     * @param application application context
+     * @param myUid uid of current process
+     * @param callingUid uid of peer process
+     */
+    fun check(application: Application, myUid: Int, callingUid: Int): Boolean
+}
+
+/** Verifies apk signatures as permission check. */
+class SignatureChecker : PermissionChecker {
+    private val cache = mutableIntIntMapOf()
+
+    override fun check(application: Application, myUid: Int, callingUid: Int): Boolean =
+        cache.getOrPut(callingUid) {
+            application.packageManager.checkSignatures(myUid, callingUid)
+        } == PackageManager.SIGNATURE_MATCH
+}