Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 00cf61a1 authored by Jacky Wang's avatar Jacky Wang Committed by Android (Google) Code Review
Browse files

Merge "Import SettingsLib/Ipc library" into main

parents dcff4e80 05d91d73
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
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"],
}
+6 −0
Original line number Diff line number Diff line
<?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>
+116 −0
Original line number Diff line number Diff line
# 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
}
```
+91 −0
Original line number Diff line number Diff line
/*
 * 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()
+92 −0
Original line number Diff line number Diff line
/*
 * 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
}
Loading