Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import android.content.Intent
import android.content.ServiceConnection
import android.os.DeadObjectException
import android.os.IInterface
import androidx.annotation.BinderThread
import androidx.annotation.WorkerThread
import kotlinx.coroutines.flow.StateFlow
import ru.sberdevices.common.binderhelper.entities.BinderState
import ru.sberdevices.common.binderhelper.entities.BinderException

/**
* Интерфейс для подключения к aidl сервисам. Имплементацию нужно получать в BinderHelperFactory.
Expand All @@ -21,9 +22,15 @@ interface BinderHelper<BinderInterface : IInterface> {
*/
val binderStateFlow: StateFlow<BinderState>

/**
* Проверяем наличие сервиса
*/
fun hasService(): Boolean

/**
* Асинхронно подключаемся к сервису. Если сразу подключиться не удалось, но сервис есть на
* девайсе, пытаемся сделать это бесконечно раз в секунду, пока корутину не отменят.
* @return true - если процесс старта сервиса был начат и были пройдены все проверки на пермишены
*/
fun connect(): Boolean

Expand All @@ -40,17 +47,49 @@ interface BinderHelper<BinderInterface : IInterface> {
*
* В случае если контекст, в котором выполняемся отменили - вернет null.
*/
@BinderThread
suspend fun <Result> execute(method: (binder: BinderInterface) -> Result): Result?
suspend fun <T> execute(method: (binder: BinderInterface) -> T?): T?

/**
* Использовать аналогично [execute] методу, но только тогда, когда ожидаемый IPC ответ != null.
*
* @return [T], обернутый в [Result], где:
* - [Result.success] имеет non-null значение
* - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC
*/
suspend fun <T> executeWithResult(method: (binder: BinderInterface) -> T): Result<T>

/**
* Пытаемся выполнить aidl-метод, если есть активное соединение.
* Если соединения нет, то просто чистим биндер и возвращаем null.
* Удобно использовать для очистки там, где нет suspend-контекста,
* например в awaitClose {} в callbackFlow.
*/
@BinderThread
fun <Result> tryExecute(method: (binder: BinderInterface) -> Result): Result?
@WorkerThread
fun <T> tryExecute(method: (binder: BinderInterface) -> T?): T?

/**
* Использовать аналогично [tryExecute] методу, но только тогда, когда ожидаемый IPC ответ != null.
*
* @return [T], обернутый в [Result], где:
* - [Result.success] имеет non-null значение
* - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC
*/
@WorkerThread
fun <T> tryExecuteWithResult(method: (binder: BinderInterface) -> T?): Result<T>

/**
* Аналогично [execute] методу, но здесь [method] передается в виде suspend лямбды.
*/
suspend fun <T> suspendExecute(method: suspend (binder: BinderInterface) -> T?): T?

/**
* Использовать аналогично [suspendExecute] методу, но только тогда, когда ожидаемый IPC ответ != null.
*
* @return [T], обернутый в [Result], где:
* - [Result.success] имеет non-null значение
* - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC
*/
suspend fun <T> suspendExecuteWithResult(method: suspend (binder: BinderInterface) -> T?): Result<T>

companion object {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.sberdevices.common.binderhelper

import android.os.IInterface
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import ru.sberdevices.common.binderhelper.entities.BinderState

/**
* @author Николай Пахомов on 03.02.2022
*/

/**
* Метод, позволяющий повесить повторяющиеся операции,
* которые будут выполняться при достижении того или иного [BinderState]
* @param helper хелпер, на чей [BinderHelper.binderStateFlow] осуществится подписка
* @param binderState стейт, при котором [block] будет выполнен
* @param block саспенд лямбда, которая будет вызвана при достижении определенного [BinderState].
*/
fun <T : IInterface> CoroutineScope.repeatOnState(
helper: BinderHelper<T>,
binderState: BinderState,
block: suspend () -> Unit
) {
helper.binderStateFlow
.filter { it == binderState }
.onEach { block.invoke() }
.launchIn(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package ru.sberdevices.common.binderhelper

import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.IInterface

/**
* Фабрика, изолирующая имплементацию от потребителей [BinderHelper]
* @author Николай Пахомов on 24.11.2021
*/
interface BinderHelperFactory2 {
/**
* @param context Application context.
* @param intent Intent с компонентом сервиса, к которому будем подключаться.
* @param loggerTag тэг для логгира, если не передать, то будет создан логгер по умолчанию.
* @param getBinding вызывается в коллбеке onServiceConnected() [android.content.ServiceConnection].
* Дает биндеру интерфейс сервиса
*/
fun <BinderInterface : IInterface> create(
context: Context,
intent: Intent,
loggerTag: String? = null,
getBinding: (IBinder) -> BinderInterface,
): BinderHelper<BinderInterface>

/**
* Создать закешированную версию [BinderHelper]. Используется для непродолжительных и частых IPC вызовов.
* @param context Application context.
* @param intent Intent с компонентом сервиса, к которому будем подключаться.
* @param loggerTag тэг для логгира, если не передать, то будет создан логгер по умолчанию.
* @param disconnectDelay таймаут на отключение от удаленного сервиса. По умолчанию - 3 секунды.
* @param getBinding вызывается в коллбеке onServiceConnected() [android.content.ServiceConnection].
* Дает биндеру интерфейс сервиса
*/
fun <BinderInterface : IInterface> createCached(
context: Context,
intent: Intent,
loggerTag: String?,
disconnectDelay: Long = 3000L,
getBinding: (IBinder) -> BinderInterface,
): CachedBinderHelper<BinderInterface>
}

/**
* @see BinderHelperFactory2.create
*/
inline fun <reified BinderInterface : IInterface> BinderHelperFactory2.create(
context: Context,
intent: Intent,
noinline getBinding: (IBinder) -> BinderInterface,
): BinderHelper<BinderInterface> = create(context, intent, BinderInterface::class.java.simpleName, getBinding)

/**
* @see BinderHelperFactory2.createCached
*/
inline fun <reified BinderInterface : IInterface> BinderHelperFactory2.createCached(
context: Context,
intent: Intent,
disconnectDelay: Long = 3000L,
noinline getBinding: (IBinder) -> BinderInterface,
): CachedBinderHelper<BinderInterface> = createCached(
context, intent, BinderInterface::class.java.simpleName, disconnectDelay, getBinding
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package ru.sberdevices.common.binderhelper

import android.os.IInterface
import java.lang.IllegalStateException

/**
* Интерфейс для подключения к aidl сервисам. Имплементацию нужно получать в [BinderHelperFactory2].
* Является оберткой над [BinderHelper] с поддержкой кеширования соединения
*
* Кеширует соединение с сервисом и сам управляет им. Не требует явного соединения с сервисом [connect].
* Позволяет использовать одно физическое соединение для независимого обращения к сервису из разных потоков.
*
* Для выполнения aidl метода достаточно просто вызывать метод [execute].
* Внутри автоматически выполнится [connect], если соединения с сервисом еще не было.
* Если нет других процессов, использующих соединение с сервисом, то по завершению метода [execute] автоматически
* выполнится [disconnect] с заданной задержкой
*
* В случае явного вызова [connect], соединение с сервисом будет поддерживаться до явного вызова [disconnect]
* Так как внутри используется счетчик соединений, то каждый явный вызов [connect] должен сопровождаться
* явным вызовом [disconnect]
*
* В случае рассинхронизации вызовов [connect], [disconnect] может вызвать исключение [IllegalStateException],
* если внутренний счетчик соединений станет меньше 0
*
*/
interface CachedBinderHelper<BinderInterface : IInterface> : BinderHelper<BinderInterface> {

/**
* Есть активное соединение с сервисом
*/
val hasConnection: Boolean

/**
* Количество виртуальных соединений
*/
val connectionCount: Int

/**
* Устанавливает соединение с сервисом и поддерживает его до явного вызова [disconnect].
* Если физическое соединение с сервисом уже существует, то просто увеличивает счетчик соединений [connectionCount]
* Каждый явный вызов [connect] требует явного вызова [disconnect]
*/
override fun connect(): Boolean

/**
* Уменьшает внутренний счетчик соединений [connectionCount].
* Физическое рассоединение с сервисом произойдет, когда внутренний счетчик станет равен 0
*
* В целях оптимизации, физическое рассоединение всегда происходит с заданной задержкой.
* Это позволяет переиспользовать соединения с сервсиом в случае частых обращений к сервису
*
* В случае рассинхронизации вызовов [connect], [disconnect] может вызвать исключение [IllegalStateException],
* если внутренний счетчик соединений [connectionCount] станет меньше 0
*/
@Throws(IllegalStateException::class)
override fun disconnect()

/**
* Вызывает aidl метод сервиса и не требует явного соединения с сервисом.
*
* Внутри автоматически выполнится [connect], если соединения с сервисом еще не было.
* Если нет других процессов, использующих соединение с сервисом, то по завершению метода [execute] автоматически
* выполнится [disconnect] с заданной задержкой
*/
override suspend fun <Result> execute(method: (binder: BinderInterface) -> Result?): Result?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.sberdevices.common.binderhelper.entities

/**
* Исключения, которые могут кинуться во время вызовов Remote методов.
* @author Николай Пахомов on 02.02.2022
*/
sealed class BinderException(override val message: String) : Exception(message) {
/**
* IPC соединение не было установлено.
*/
class ConnectionNotEstablished : BinderException("Binder connection hasn't been established")

/**
* Корутина, в которой ожидалось выполнение Remote метода была отменена
*/
class CoroutineContextCancelled : BinderException("Coroutine with connection was cancelled")

/**
* Remote метод вернул null значение, хотя этого не ожидалось.
* Скорей всего текущая версия SDK обращается к старому SPS, который не умеет обрабатывать эти методы.
*/
class ReceivedNullValue : BinderException(
"Received null value from IPC call, expected non-null. Current SDK version might not be yet supported"
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import ru.sberdevices.common.logger.Logger

/**
* Фабрика, изолирующая имплементацию от потребителей [BinderHelper]
* [context] application context
* [intent] Intent с компонентом сервиса, к которому будем подключаться
* [logger] внешний [Logger], если не передать, то будет создан логгер по умолчанию
* [onDisconnect] вызывается в коллбеке onServiceDisconnected() [android.content.ServiceConnection]
* [onBindingDied] вызывается в коллбеке onBindingDied() [android.content.ServiceConnection]
* [onNullBinding] вызывается в коллбеке onNullBinding() [android.content.ServiceConnection]
* [getBinding] вызывается в коллбеке onServiceConnected() [android.content.ServiceConnection]. Дает биндеру интерфейс сервиса
* @param context application context
* @param intent Intent с компонентом сервиса, к которому будем подключаться
* @param logger внешний [Logger], если не передать, то будет создан логгер по умолчанию
* @param getBinding вызывается в коллбеке onServiceConnected() [android.content.ServiceConnection].
* Дает биндеру интерфейс сервиса
*/
@Deprecated(
message = "Используйте BinderHelperFactory2 для создания инстанса BinderHelper. Он не требует impl модуля.",
level = DeprecationLevel.WARNING,
replaceWith = ReplaceWith("BinderHelperFactory2")
)
class BinderHelperFactory<BinderInterface : IInterface>(
private val context: Context,
private val intent: Intent,
Expand Down Expand Up @@ -44,8 +47,8 @@ class BinderHelperFactory<BinderInterface : IInterface>(
* Реальный disconnect происходит с задержкой [disconnectDelay].
* Это позволяет последовательно вызывать несколько [BinderHelper.execute] в рамках одного физического соединения
*/
fun createCached(disconnectDelay: Long = DISCONNECT_DELAY): BinderHelper<BinderInterface> =
CachedBinderHelper(create(), logger, disconnectDelay)
fun createCached(disconnectDelay: Long = DISCONNECT_DELAY): CachedBinderHelper<BinderInterface> =
CachedBinderHelperImpl(create(), logger, disconnectDelay)
}

private const val DISCONNECT_DELAY = 3000L
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package ru.sberdevices.common.binderhelper

import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.IInterface
import ru.sberdevices.common.logger.Logger

/**
* Фабрика, изолирующая имплементацию от потребителей [BinderHelper]
*/
class BinderHelperFactory2Impl : BinderHelperFactory2 {

/**
* @param context application context
* @param intent Intent с компонентом сервиса, к которому будем подключаться
* @param loggerTag тэг для логгирования, если не передать, то будет использоваться `BinderHelper`
* @param getBinding лямбда, которая должна вернуть тип сервиса.
*/
override fun <BinderInterface : IInterface> create(
context: Context,
intent: Intent,
loggerTag: String?,
getBinding: (IBinder) -> BinderInterface,
): BinderHelper<BinderInterface> = BinderHelperImpl(
context = context,
intent = intent,
logger = Logger.get(loggerTag ?: "BinderHelper"),
getBinding = getBinding
)

/**
* Создает [CachedBinderHelper] поддерживающий кеширование соединения
*
* Внутри хранит счетчик соединений, connect инкрементирует счетчик, disconnect декрементирует
* Реальное соединения и рассоединение происходит, когда счетчик = 0
*
* Каждый вызов [BinderHelper.execute] внутри себя вызывает connect/disconnect
* При этом если реальное соединение уже существует, то физического connect/disconnect не будет
*
* Реальный disconnect происходит с задержкой [disconnectDelay].
* Это позволяет последовательно вызывать несколько [BinderHelper.execute] в рамках одного физического соединения
*/
override fun <BinderInterface : IInterface> createCached(
context: Context,
intent: Intent,
loggerTag: String?,
disconnectDelay: Long,
getBinding: (IBinder) -> BinderInterface,
): CachedBinderHelper<BinderInterface> {
return CachedBinderHelperImpl(
create(context, intent, loggerTag, getBinding),
Logger.get(loggerTag ?: "BinderHelper"),
disconnectDelay
)
}

}
Loading