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