diff options
author | 2024-09-13 12:35:03 +0800 | |
---|---|---|
committer | 2024-09-13 12:49:10 +0800 | |
commit | 05d91d730a03f354b3024b990491cfdeb59d3e3e (patch) | |
tree | b4cd5d0a4025a6c56c09f00b2dd4fa5055e55faa | |
parent | d2ed3a9ce62c069b33306de0035df1cad437fe6c (diff) |
Import SettingsLib/Ipc library
Bug: 365886533
Flag: EXEMPT Import library
Test: Local build
Change-Id: I467c97d5f6e0f40204d9d379b8865e3f9913d82d
10 files changed, 1170 insertions, 0 deletions
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp new file mode 100644 index 000000000000..61adb46b44e3 --- /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 000000000000..fc48a7da8044 --- /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 000000000000..ea2c3a1b52db --- /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 000000000000..42772a4b5002 --- /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 000000000000..802141dae7ec --- /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 000000000000..4b7572b90b3f --- /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 000000000000..0bdae38a0a24 --- /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 000000000000..7ffefed239a4 --- /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 000000000000..795a920ba575 --- /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 000000000000..da9c955d5069 --- /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 +} |