From 8ce86138ee92b0e37bc0cefbae5f63e9974f9fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9F=D0=B5=D1=80=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 13 Apr 2022 02:00:49 +0300 Subject: [PATCH 1/2] BinderHelper updates --- .../common/binderhelper/BinderHelper.kt | 49 +++++++++- .../common/binderhelper/BinderHelperExt.kt | 30 ++++++ .../binderhelper/BinderHelperFactory2.kt | 64 +++++++++++++ .../common/binderhelper/CachedBinderHelper.kt | 66 +++++++++++++ .../binderhelper/entities/BinderException.kt | 25 +++++ .../binderhelper/entities/BinderState.kt | 0 .../binderhelper/BinderHelperFactory.kt | 21 ++-- .../binderhelper/BinderHelperFactory2Impl.kt | 58 +++++++++++ .../common/binderhelper/BinderHelperImpl.kt | 96 +++++++++++++------ .../binderhelper/CachedBinderHelperImpl.kt} | 63 ++++++++---- .../common/binderhelper/BinderHelperTest.kt | 93 +++++++++++++++--- .../binderhelper/CachedBinderHelperTest.kt | 12 ++- 12 files changed, 497 insertions(+), 80 deletions(-) rename public/common/binderhelper/api/src/main/{java => kotlin}/ru/sberdevices/common/binderhelper/BinderHelper.kt (54%) create mode 100644 public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperExt.kt create mode 100644 public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2.kt create mode 100644 public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt create mode 100644 public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderException.kt rename public/common/binderhelper/api/src/main/{java => kotlin}/ru/sberdevices/common/binderhelper/entities/BinderState.kt (100%) rename public/common/binderhelper/impl/src/main/{java => kotlin}/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt (68%) create mode 100644 public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2Impl.kt rename public/common/binderhelper/impl/src/main/{java => kotlin}/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt (72%) rename public/common/binderhelper/impl/src/main/{java/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt => kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperImpl.kt} (61%) rename public/common/binderhelper/impl/src/test/{java => kotlin}/ru/sberdevices/common/binderhelper/BinderHelperTest.kt (70%) rename public/common/binderhelper/impl/src/test/{java => kotlin}/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt (96%) diff --git a/public/common/binderhelper/api/src/main/java/ru/sberdevices/common/binderhelper/BinderHelper.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelper.kt similarity index 54% rename from public/common/binderhelper/api/src/main/java/ru/sberdevices/common/binderhelper/BinderHelper.kt rename to public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelper.kt index 0e7dcdc..616e46e 100644 --- a/public/common/binderhelper/api/src/main/java/ru/sberdevices/common/binderhelper/BinderHelper.kt +++ b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelper.kt @@ -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. @@ -21,9 +22,15 @@ interface BinderHelper { */ val binderStateFlow: StateFlow + /** + * Проверяем наличие сервиса + */ + fun hasService(): Boolean + /** * Асинхронно подключаемся к сервису. Если сразу подключиться не удалось, но сервис есть на * девайсе, пытаемся сделать это бесконечно раз в секунду, пока корутину не отменят. + * @return true - если процесс старта сервиса был начат и были пройдены все проверки на пермишены */ fun connect(): Boolean @@ -40,8 +47,16 @@ interface BinderHelper { * * В случае если контекст, в котором выполняемся отменили - вернет null. */ - @BinderThread - suspend fun execute(method: (binder: BinderInterface) -> Result): Result? + suspend fun execute(method: (binder: BinderInterface) -> T?): T? + + /** + * Использовать аналогично [execute] методу, но только тогда, когда ожидаемый IPC ответ != null. + * + * @return [T], обернутый в [Result], где: + * - [Result.success] имеет non-null значение + * - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC + */ + suspend fun executeWithResult(method: (binder: BinderInterface) -> T): Result /** * Пытаемся выполнить aidl-метод, если есть активное соединение. @@ -49,8 +64,32 @@ interface BinderHelper { * Удобно использовать для очистки там, где нет suspend-контекста, * например в awaitClose {} в callbackFlow. */ - @BinderThread - fun tryExecute(method: (binder: BinderInterface) -> Result): Result? + @WorkerThread + fun tryExecute(method: (binder: BinderInterface) -> T?): T? + + /** + * Использовать аналогично [tryExecute] методу, но только тогда, когда ожидаемый IPC ответ != null. + * + * @return [T], обернутый в [Result], где: + * - [Result.success] имеет non-null значение + * - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC + */ + @WorkerThread + fun tryExecuteWithResult(method: (binder: BinderInterface) -> T?): Result + + /** + * Аналогично [execute] методу, но здесь [method] передается в виде suspend лямбды. + */ + suspend fun suspendExecute(method: suspend (binder: BinderInterface) -> T?): T? + + /** + * Использовать аналогично [suspendExecute] методу, но только тогда, когда ожидаемый IPC ответ != null. + * + * @return [T], обернутый в [Result], где: + * - [Result.success] имеет non-null значение + * - [Result.failure] содержит одно из [BinderException] исключений, в том числе - получение null значение через IPC + */ + suspend fun suspendExecuteWithResult(method: suspend (binder: BinderInterface) -> T?): Result companion object { /** diff --git a/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperExt.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperExt.kt new file mode 100644 index 0000000..4a97097 --- /dev/null +++ b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperExt.kt @@ -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 CoroutineScope.repeatOnState( + helper: BinderHelper, + binderState: BinderState, + block: suspend () -> Unit +) { + helper.binderStateFlow + .filter { it == binderState } + .onEach { block.invoke() } + .launchIn(this) +} diff --git a/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2.kt new file mode 100644 index 0000000..fab8992 --- /dev/null +++ b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2.kt @@ -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 create( + context: Context, + intent: Intent, + loggerTag: String? = null, + getBinding: (IBinder) -> BinderInterface, + ): BinderHelper + + /** + * Создать закешированную версию [BinderHelper]. Используется для непродолжительных и частых IPC вызовов. + * @param context Application context. + * @param intent Intent с компонентом сервиса, к которому будем подключаться. + * @param loggerTag тэг для логгира, если не передать, то будет создан логгер по умолчанию. + * @param disconnectDelay таймаут на отключение от удаленного сервиса. По умолчанию - 3 секунды. + * @param getBinding вызывается в коллбеке onServiceConnected() [android.content.ServiceConnection]. + * Дает биндеру интерфейс сервиса + */ + fun createCached( + context: Context, + intent: Intent, + loggerTag: String?, + disconnectDelay: Long = 3000L, + getBinding: (IBinder) -> BinderInterface, + ): CachedBinderHelper +} + +/** + * @see BinderHelperFactory2.create + */ +inline fun BinderHelperFactory2.create( + context: Context, + intent: Intent, + noinline getBinding: (IBinder) -> BinderInterface, +): BinderHelper = create(context, intent, BinderInterface::class.java.simpleName, getBinding) + +/** + * @see BinderHelperFactory2.createCached + */ +inline fun BinderHelperFactory2.createCached( + context: Context, + intent: Intent, + disconnectDelay: Long = 3000L, + noinline getBinding: (IBinder) -> BinderInterface, +): CachedBinderHelper = createCached( + context, intent, BinderInterface::class.java.simpleName, disconnectDelay, getBinding +) diff --git a/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt new file mode 100644 index 0000000..29c09a5 --- /dev/null +++ b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt @@ -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 : BinderHelper { + + /** + * Есть активное соединение с сервисом + */ + 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 execute(method: (binder: BinderInterface) -> Result?): Result? +} diff --git a/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderException.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderException.kt new file mode 100644 index 0000000..55979b2 --- /dev/null +++ b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderException.kt @@ -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" + ) +} diff --git a/public/common/binderhelper/api/src/main/java/ru/sberdevices/common/binderhelper/entities/BinderState.kt b/public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderState.kt similarity index 100% rename from public/common/binderhelper/api/src/main/java/ru/sberdevices/common/binderhelper/entities/BinderState.kt rename to public/common/binderhelper/api/src/main/kotlin/ru/sberdevices/common/binderhelper/entities/BinderState.kt diff --git a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt similarity index 68% rename from public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt rename to public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt index 0a4e878..a443a1e 100644 --- a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt +++ b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory.kt @@ -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( private val context: Context, private val intent: Intent, @@ -44,8 +47,8 @@ class BinderHelperFactory( * Реальный disconnect происходит с задержкой [disconnectDelay]. * Это позволяет последовательно вызывать несколько [BinderHelper.execute] в рамках одного физического соединения */ - fun createCached(disconnectDelay: Long = DISCONNECT_DELAY): BinderHelper = - CachedBinderHelper(create(), logger, disconnectDelay) + fun createCached(disconnectDelay: Long = DISCONNECT_DELAY): CachedBinderHelper = + CachedBinderHelperImpl(create(), logger, disconnectDelay) } private const val DISCONNECT_DELAY = 3000L diff --git a/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2Impl.kt b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2Impl.kt new file mode 100644 index 0000000..340f2d2 --- /dev/null +++ b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperFactory2Impl.kt @@ -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 create( + context: Context, + intent: Intent, + loggerTag: String?, + getBinding: (IBinder) -> BinderInterface, + ): BinderHelper = 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 createCached( + context: Context, + intent: Intent, + loggerTag: String?, + disconnectDelay: Long, + getBinding: (IBinder) -> BinderInterface, + ): CachedBinderHelper { + return CachedBinderHelperImpl( + create(context, intent, loggerTag, getBinding), + Logger.get(loggerTag ?: "BinderHelper"), + disconnectDelay + ) + } + +} diff --git a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt similarity index 72% rename from public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt rename to public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt index 6bfb4c5..c43979c 100644 --- a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt +++ b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/BinderHelperImpl.kt @@ -8,8 +8,8 @@ import android.content.pm.PackageManager import android.os.DeadObjectException import android.os.IBinder import android.os.IInterface -import androidx.annotation.BinderThread import androidx.annotation.MainThread +import androidx.annotation.WorkerThread import kotlinx.coroutines.CancellationException import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.MutableStateFlow @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.first import kotlinx.coroutines.isActive +import ru.sberdevices.common.binderhelper.entities.BinderException import ru.sberdevices.common.binderhelper.entities.BinderState import ru.sberdevices.common.logger.Logger @@ -79,13 +80,21 @@ internal class BinderHelperImpl( binderState.compareAndSet(binder, null) } + /** + * Проверяем наличие сервиса + */ + @Suppress("WrongConstant", "QueryPermissionsNeeded") + override fun hasService(): Boolean { + return context.packageManager.queryIntentServices(intent, PackageManager.MATCH_ALL).isNotEmpty() + } + /** * Асинхронно подключаемся к сервису и получаем aidl-интерфейс через [getBinding]. * Если сразу подключиться не удалось - пытаемся сделать это бесконечно раз в секунду, пока корутину не отменят. */ override fun connect(): Boolean { logger.verbose { "try to connect() intent=${intent.component?.className}" } - if (context.packageManager.queryIntentServices(intent, PackageManager.MATCH_ALL).isEmpty()) { + if (!hasService()) { logger.warn { "service (${intent.component}) is not present in the system, will not connect" } return false } @@ -107,6 +116,7 @@ internal class BinderHelperImpl( clearBinder() connectionState.value?.let { context.applicationContext.unbindService(it) } connectionState.value = null + mutableBinderStateFlow.value = BinderState.DISCONNECTED } /** @@ -117,25 +127,10 @@ internal class BinderHelperImpl( * * В случае если контекст, в котором выполняемся отменили - вернет null. */ - @BinderThread - override suspend fun execute(method: (binder: BinderInterface) -> Result): Result? { - while (currentCoroutineContext().isActive) { - val binder = binderState - .filterNotNull() - .first() - try { - return method(binder) - } catch (e: DeadObjectException) { - clearBinder(binder) - logger.warn { - "The object we are calling has died, because its hosting process no longer exists. Retrying..." - } - // We just want to wait for ServiceConnection#onServiceConnected(...) - } - } + override suspend fun execute(method: (binder: BinderInterface) -> T?): T? = suspendExecute { method(it) } - throw CancellationException("Connection is cancelled") - } + override suspend fun executeWithResult(method: (binder: BinderInterface) -> T): Result = + suspendExecuteWithResult { method(it) } /** * Пытаемся выполнить aidl-метод, если есть активное соединение. @@ -143,24 +138,65 @@ internal class BinderHelperImpl( * Удобно использовать для очистки там, где нет suspend-контекста, * например в awaitClose {} в callbackFlow. */ - @BinderThread - override fun tryExecute(method: (binder: BinderInterface) -> Result): Result? { + @WorkerThread + override fun tryExecute(method: (binder: BinderInterface) -> T?): T? = tryExecuteWithResult(method).fold( + onSuccess = { it }, + onFailure = { null } + ) + + @WorkerThread + override fun tryExecuteWithResult(method: (binder: BinderInterface) -> T?): Result { val binder = binderState.value - return if (binder != null) { - try { - method(binder) - } catch (e: DeadObjectException) { + + return runCatching { + if (binder != null) { + return@runCatching method(binder) ?: throw BinderException.ReceivedNullValue() + } else { + throw BinderException.ConnectionNotEstablished() + } + }.onFailure { + if (it is DeadObjectException) { logger.warn { "The object we are calling has died, because its hosting process no longer exists. Retrying..." } clearBinder(binder) + // We just want to wait for ServiceConnection#onServiceConnected(...) + } + } + } + + override suspend fun suspendExecute(method: suspend (binder: BinderInterface) -> T?): T? = + suspendExecuteWithResult(method).fold( + onSuccess = { it }, + onFailure = { + if (it is BinderException.CoroutineContextCancelled) { + throw CancellationException("Connection is cancelled") + } null } - } else { - logger.info { - "The object we are calling has died, because its hosting process no longer exists..." + ) + + override suspend fun suspendExecuteWithResult(method: suspend (binder: BinderInterface) -> T?): Result { + var binder: BinderInterface? = null + + return runCatching { + while (currentCoroutineContext().isActive) { + binder = binderState + .filterNotNull() + .first() + + return@runCatching method(binder!!) ?: throw BinderException.ReceivedNullValue() + } + + throw BinderException.CoroutineContextCancelled() + }.onFailure { + if (it is DeadObjectException) { + clearBinder(binder) + logger.warn { + "The object we are calling has died, because its hosting process no longer exists. Retrying..." + } + // We just want to wait for ServiceConnection#onServiceConnected(...) } - null } } } diff --git a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperImpl.kt similarity index 61% rename from public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt rename to public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperImpl.kt index 77a3d38..d9e6f53 100644 --- a/public/common/binderhelper/impl/src/main/java/ru/sberdevices/common/binderhelper/CachedBinderHelper.kt +++ b/public/common/binderhelper/impl/src/main/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperImpl.kt @@ -1,7 +1,6 @@ package ru.sberdevices.common.binderhelper import android.os.IInterface -import androidx.annotation.BinderThread import androidx.annotation.VisibleForTesting import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -9,7 +8,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import ru.sberdevices.common.binderhelper.entities.BinderException import ru.sberdevices.common.logger.Logger +import java.lang.IllegalStateException import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicReference @@ -18,19 +19,20 @@ import java.util.concurrent.atomic.AtomicReference * Позволяет делать независимые connect/disconnect для разных процессов, * которые в реальности работают через одно, закешированное соединение * - * Внутри хранит счетчик соединений, [connect] инкрементирует счетчик, [disconnect] декрементирует - * Реальное соединения и рассоединения происходит, когда счетчик = 0 + * Для выполнения aidl метода достаточно просто вызывать метод [execute] без вызова [connect]. + * Внутри автоматически выполнится [connect], если соединения с сервисом еще не было. + * Если нет других процессов, использующих соединение с сервисом, то по завершению метода [execute] автоматически + * выполнится [disconnect] с заданной задержкой * - * Метод [execute] внутри делает свой connect, выполняет тело метода и затем делает disconnect - * - * Реальный [disconnect] происходит с задержкой. - * Это позволяет последовательно вызывать несколько [execute] в рамках одного физического соединения + * В случае явного вызова [connect], соединение с сервисом будет поддерживаться до явного вызова [disconnect] + * Так как внутри используется счетчик соединений, то каждый явный вызов [connect] должен сопровождаться + * явным вызовом [disconnect] */ -internal class CachedBinderHelper( +internal class CachedBinderHelperImpl( private val helper: BinderHelper, private val logger: Logger, private val disconnectDelay: Long -) : BinderHelper by helper { +) : CachedBinderHelper, BinderHelper by helper { private val scope = CoroutineScope(SupervisorJob()) private val connectCounter = AtomicInteger(0) @@ -40,27 +42,32 @@ internal class CachedBinderHelper( * Признак, что есть активное соединение */ @Volatile - var hasConnection: Boolean = false + override var hasConnection: Boolean = false private set + override val connectionCount: Int + get() = connectCounter.get() + @VisibleForTesting - val connectionCount: Int get() = connectCounter.get() - @VisibleForTesting - val hasScheduleDisconnectTask: Boolean get() = disconnectJobRef.get()?.isActive ?: false + val hasScheduleDisconnectTask: Boolean + get() = disconnectJobRef.get()?.isActive ?: false override fun connect(): Boolean { + val connectionCount = connectCounter.incrementAndGet() + logger.debug { "cachedConnect(), connectionCount == $connectionCount" } + cancelDisconnectTask() if (!hasConnection) { binderConnect() } - val connectionCount = connectCounter.incrementAndGet() - logger.debug { "cachedConnect(), connectionCount == $connectionCount" } return hasConnection } override fun disconnect() { val connectionCount = connectCounter.decrementAndGet() + logger.debug { "cachedDisconnect(), connectionCount == $connectionCount" } + if (connectionCount < 0) { throw IllegalStateException("service already disconnected, connection counter value < 0") } @@ -68,14 +75,19 @@ internal class CachedBinderHelper( // Если соединений больше нет, то создаем задание на отложенный disconnect scheduleDisconnectTask() } - logger.debug { "cachedDisconnect(), connectionCount == $connectionCount" } } - @BinderThread - override suspend fun execute(method: (binder: BinderInterface) -> Result): Result? { + override suspend fun execute(method: (binder: BinderInterface) -> Result?): Result? = suspendExecute { + method(it) + } + + override suspend fun executeWithResult(method: (binder: BinderInterface) -> T): Result = + suspendExecuteWithResult { method(it) } + + override suspend fun suspendExecute(method: suspend (binder: BinderInterface) -> Result?): Result? { return if (connect()) { try { - helper.execute(method) + helper.suspendExecute(method) } finally { disconnect() } @@ -84,6 +96,19 @@ internal class CachedBinderHelper( } } + override suspend fun suspendExecuteWithResult(method: suspend (binder: BinderInterface) -> T?): Result = + runCatching { + if (connect()) { + try { + helper.suspendExecuteWithResult(method).getOrThrow() + } finally { + disconnect() + } + } else { + throw BinderException.ConnectionNotEstablished() + } + } + @Synchronized private fun binderConnect() { if (!hasConnection) { diff --git a/public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/BinderHelperTest.kt b/public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/BinderHelperTest.kt similarity index 70% rename from public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/BinderHelperTest.kt rename to public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/BinderHelperTest.kt index c06b6e7..d0b621f 100644 --- a/public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/BinderHelperTest.kt +++ b/public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/BinderHelperTest.kt @@ -11,22 +11,31 @@ import android.os.IInterface import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.slot +import io.mockk.unmockkObject import io.mockk.verify import io.mockk.verifySequence -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertNull -import junit.framework.Assert.assertTrue +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.TestCoroutineScope import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue import org.junit.Test import ru.sberdevices.common.binderhelper.entities.BinderState /** * Тест для [BinderHelper] + * + * @author Илья Богданович on 12.02.2021 */ class BinderHelperTest { private val intent = mockk() @@ -41,11 +50,10 @@ class BinderHelperTest { } private val binding = mockk() - private val helper = BinderHelperFactory( + private val helper = BinderHelperFactory2Impl().create( context = context, - intent = intent, - getBinding = { binding }, - ).create() + intent = intent + ) { binding } private val scope = TestCoroutineScope() @Test @@ -62,16 +70,32 @@ class BinderHelperTest { } @Test - fun `Неуспешный connect если сервис отсутствует`() = runBlocking { + fun `Сервис отсутствует`() = runBlocking { // Prepare every { pm.queryIntentServices(intent, PackageManager.MATCH_ALL) } returns mutableListOf() + // Do + val result = helper.hasService() + + // Check + assertFalse(result) + } + + @Test + fun `Неуспешный connect если сервис отсутствует`() = runBlocking { + // Prepare + mockkObject(helper) + every { helper.hasService() } returns false + // Do val result = helper.connect() // Check verify(inverse = true) { appContext.bindService(any(), any(), any()) } assertFalse(result) + + // Unmock + unmockkObject(helper) } @Test @@ -165,7 +189,7 @@ class BinderHelperTest { every { appContext.bindService(any(), any(), any()) } answers { arg(1).onBindingDied(mockk()) true - } andThen(true) + } andThen (true) // Do helper.connect() @@ -183,7 +207,7 @@ class BinderHelperTest { every { appContext.bindService(any(), any(), any()) } answers { arg(1).onNullBinding(mockk()) true - } andThen(true) + } andThen (true) // Do helper.connect() @@ -201,7 +225,7 @@ class BinderHelperTest { every { appContext.bindService(any(), any(), any()) } answers { arg(1).onServiceDisconnected(mockk()) true - } andThen(true) + } andThen (true) // Do helper.connect() @@ -213,4 +237,49 @@ class BinderHelperTest { assertThat(helper.binderStateFlow.value, equalTo(BinderState.DISCONNECTED)) } + @Test + fun `Все ивенты ServiceConnection отразятся в BinderStateFlow`() { + // Prepare + val serviceConnectionSlot = slot() + every { appContext.bindService(any(), capture(serviceConnectionSlot), any()) } returns true + + val testScope = TestCoroutineScope() + val captor = helper.binderStateFlow.collectToList(testScope) + + // Do + helper.connect() + + with(serviceConnectionSlot.captured) { + onServiceConnected(mockk(relaxed = true), mockk(relaxed = true)) + onBindingDied(mockk(relaxed = true)) + onNullBinding(mockk(relaxed = true)) + onServiceDisconnected(mockk(relaxed = true)) + } + + // Check + assertThat( + captor, + equalTo( + listOf( + BinderState.DISCONNECTED, // initial + BinderState.CONNECTED, + BinderState.BINDING_DIED, + BinderState.NULL_BINDING, + BinderState.DISCONNECTED + ) + ) + ) + } + + private companion object { + + /** + * Асинхронно собирает флоу в список. + */ + private fun Flow.collectToList(scope: CoroutineScope): List { + val collector = mutableListOf() + onEach { collector += it }.launchIn(scope) + return collector + } + } } diff --git a/public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt b/public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt similarity index 96% rename from public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt rename to public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt index cbbb9a3..ac55812 100644 --- a/public/common/binderhelper/impl/src/test/java/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt +++ b/public/common/binderhelper/impl/src/test/kotlin/ru/sberdevices/common/binderhelper/CachedBinderHelperTest.kt @@ -11,17 +11,19 @@ import android.os.IInterface import io.mockk.every import io.mockk.mockk import io.mockk.verify -import junit.framework.Assert.assertEquals -import junit.framework.Assert.assertFalse -import junit.framework.Assert.assertTrue import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import ru.sberdevices.common.logger.Logger /** - * Тест для [CachedBinderHelper] + * Тест для [CachedBinderHelperImpl] + * + * @author Сидоров Максим */ class CachedBinderHelperTest { private val intent = mockk() @@ -46,7 +48,7 @@ class CachedBinderHelperTest { getBinding = { binding }, ).create() - private val helper = CachedBinderHelper(internalHelper, logger, DISCONNECT_DELAY) + private val helper = CachedBinderHelperImpl(internalHelper, logger, DISCONNECT_DELAY) @Before fun prepare() { From 8260827610f68ac215006a5704b18688924f307a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9F=D0=B5=D1=80=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=BE=D0=B2?= Date: Wed, 13 Apr 2022 02:04:02 +0300 Subject: [PATCH 2/2] Paylib added --- public/services/paylib/aidl/build.gradle | 51 ++++++++++ .../paylib/aidl/src/main/AndroidManifest.xml | 1 + .../services/paylib/IPayLibService.aidl | 12 +++ .../services/paylib/IPayStatusListener.aidl | 7 ++ .../services/paylib/codes/PayResultCode.java | 13 +++ public/services/paylib/api/build.gradle | 64 ++++++++++++ .../paylib/api/src/main/AndroidManifest.xml | 1 + .../ru/sberdevices/services/paylib/PayLib.kt | 8 ++ .../services/paylib/entities/PayResultCode.kt | 27 ++++++ .../services/paylib/entities/PayStatus.kt | 13 +++ public/services/paylib/impl/build.gradle | 75 ++++++++++++++ .../paylib/impl/src/main/AndroidManifest.xml | 6 ++ .../services/paylib/PayLibFactory.kt | 36 +++++++ .../sberdevices/services/paylib/PayLibImpl.kt | 97 +++++++++++++++++++ .../services/paylib/PayResultCodeFactory.kt | 17 ++++ .../aidl/wrappers/PayStatusListenerWrapper.kt | 12 +++ .../wrappers/PayStatusListenerWrapperImpl.kt | 32 ++++++ settings.gradle | 5 +- 18 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 public/services/paylib/aidl/build.gradle create mode 100644 public/services/paylib/aidl/src/main/AndroidManifest.xml create mode 100644 public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayLibService.aidl create mode 100644 public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayStatusListener.aidl create mode 100644 public/services/paylib/aidl/src/main/java/ru/sberdevices/services/paylib/codes/PayResultCode.java create mode 100644 public/services/paylib/api/build.gradle create mode 100644 public/services/paylib/api/src/main/AndroidManifest.xml create mode 100644 public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/PayLib.kt create mode 100644 public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayResultCode.kt create mode 100644 public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayStatus.kt create mode 100644 public/services/paylib/impl/build.gradle create mode 100644 public/services/paylib/impl/src/main/AndroidManifest.xml create mode 100644 public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibFactory.kt create mode 100644 public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibImpl.kt create mode 100644 public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayResultCodeFactory.kt create mode 100644 public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapper.kt create mode 100644 public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapperImpl.kt diff --git a/public/services/paylib/aidl/build.gradle b/public/services/paylib/aidl/build.gradle new file mode 100644 index 0000000..f3caa66 --- /dev/null +++ b/public/services/paylib/aidl/build.gradle @@ -0,0 +1,51 @@ +plugins { + id 'com.android.library' + id 'maven-publish' +} + +//afterEvaluate { +// publishing { +// repositories.add(rootProject.repositories.getByName('OSSRH')) +// +// publications { +// mavenAppstate(MavenPublication) { +// from components.release +// +// groupId publication.pomGroupID + '.appstate' +// artifactId "aidl" +// version publication.pomAppStateVersion +// +// pom { +// name = "appstate:aidl" +// description = 'appstate is a library used to transmit native app state to smartapp backend' +// url = publication.githubUrl +// licenses { +// license { +// name = publication.licenseName +// url = publication.licenseUrl +// } +// } +// developers { +// developer { +// name = 'Igor Perminov' +// email = 'Perminov.I.Yurye@sberbank.ru' +// } +// developer { +// name = 'Nikolay Pahomov' +// email = 'NMPakhomov@sberbank.ru' +// } +// } +// scm { +// connection = publication.connectionUrl +// developerConnection = publication.connectionUrl +// url = publication.githubUrl +// } +// } +// } +// } +// } +// +// signing { +// sign publishing.publications.mavenAppstate +// } +//} \ No newline at end of file diff --git a/public/services/paylib/aidl/src/main/AndroidManifest.xml b/public/services/paylib/aidl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ac8317d --- /dev/null +++ b/public/services/paylib/aidl/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayLibService.aidl b/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayLibService.aidl new file mode 100644 index 0000000..6c5d861 --- /dev/null +++ b/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayLibService.aidl @@ -0,0 +1,12 @@ +package ru.sberdevices.services.paylib; + +import ru.sberdevices.services.paylib.IPayStatusListener; + +interface IPayLibService { + const String PLATFORM_VERSION = "1.79.0"; + + boolean launchPayDialog(in String invoiceId) = 10; + + void addPayStatusListener(in IPayStatusListener listener) = 120; + void removePayStatusListener(in IPayStatusListener listener) = 121; +} diff --git a/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayStatusListener.aidl b/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayStatusListener.aidl new file mode 100644 index 0000000..98420d9 --- /dev/null +++ b/public/services/paylib/aidl/src/main/aidl/ru/sberdevices/services/paylib/IPayStatusListener.aidl @@ -0,0 +1,7 @@ +package ru.sberdevices.services.paylib; + +//import ru.sberdevices.services.paylib.PayStatus; + +interface IPayStatusListener { + oneway void onPayStatusUpdated(in String invoiceId, in int resultCode) = 10; +} diff --git a/public/services/paylib/aidl/src/main/java/ru/sberdevices/services/paylib/codes/PayResultCode.java b/public/services/paylib/aidl/src/main/java/ru/sberdevices/services/paylib/codes/PayResultCode.java new file mode 100644 index 0000000..75a8df3 --- /dev/null +++ b/public/services/paylib/aidl/src/main/java/ru/sberdevices/services/paylib/codes/PayResultCode.java @@ -0,0 +1,13 @@ +package ru.sberdevices.services.paylib.codes; + +public enum PayResultCode { + SUCCESS(0), + ERROR(1), + CANCELLED(2); + + public final int rawCode; + + private PayResultCode(int rawCode) { + this.rawCode = rawCode; + } +} diff --git a/public/services/paylib/api/build.gradle b/public/services/paylib/api/build.gradle new file mode 100644 index 0000000..e30c802 --- /dev/null +++ b/public/services/paylib/api/build.gradle @@ -0,0 +1,64 @@ +plugins { + id 'com.android.library' + id 'maven-publish' +} + +dependencies { + implementation "androidx.core:core-ktx:$versions.androidx.core" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.kotlinxCoroutines" +} + +//task androidSourcesJar(type: Jar) { +// archiveClassifier.set('sources') +// from android.sourceSets.main.java.srcDirs +// from android.sourceSets.main.kotlin.srcDirs +//} +// +//afterEvaluate { +// publishing { +// repositories.add(rootProject.repositories.getByName('OSSRH')) +// +// publications { +// mavenAppstateApi(MavenPublication) { +// from components.release +// +// artifact androidSourcesJar +// +// groupId publication.pomGroupID + '.appstate' +// artifactId "api" +// version publication.pomAppStateVersion +// +// pom { +// name = "appstate:api" +// description = 'appstate is a library used to transmit native app state to smartapp backend' +// url = publication.githubUrl +// licenses { +// license { +// name = publication.licenseName +// url = publication.licenseUrl +// } +// } +// developers { +// developer { +// name = 'Igor Perminov' +// email = 'Perminov.I.Yurye@sberbank.ru' +// } +// developer { +// name = 'Nikolay Pahomov' +// email = 'NMPakhomov@sberbank.ru' +// } +// } +// scm { +// connection = publication.connectionUrl +// developerConnection = publication.connectionUrl +// url = publication.githubUrl +// } +// } +// } +// } +// } +// +// signing { +// sign publishing.publications.mavenAppstateApi +// } +//} \ No newline at end of file diff --git a/public/services/paylib/api/src/main/AndroidManifest.xml b/public/services/paylib/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0e485c1 --- /dev/null +++ b/public/services/paylib/api/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/PayLib.kt b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/PayLib.kt new file mode 100644 index 0000000..41b792f --- /dev/null +++ b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/PayLib.kt @@ -0,0 +1,8 @@ +package ru.sberdevices.services.paylib + +import ru.sberdevices.services.paylib.entities.PayStatus + +interface PayLib { + + suspend fun launchPayDialog(invoiceId: String): Result +} diff --git a/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayResultCode.kt b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayResultCode.kt new file mode 100644 index 0000000..44020da --- /dev/null +++ b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayResultCode.kt @@ -0,0 +1,27 @@ +package ru.sberdevices.services.paylib.entities + +/** + * Результат завершения оплаты + */ +enum class PayResultCode { + + /** + * Оплата завершилась успешно + */ + SUCCESS, + + /** + * Ошибка + */ + ERROR, + + /** + * Закрыто пользователем + */ + CANCELLED, + + /** + * Неподдерживаемый код + */ + UNKNOWN, +} diff --git a/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayStatus.kt b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayStatus.kt new file mode 100644 index 0000000..31ea0a0 --- /dev/null +++ b/public/services/paylib/api/src/main/kotlin/ru/sberdevices/services/paylib/entities/PayStatus.kt @@ -0,0 +1,13 @@ +package ru.sberdevices.services.paylib.entities + +/** + * Результат оплаты + * @param invoiceId Идентификатор счета + * @param resultCode Результат завершения оплаты + * + * @author Николай Пахомов on 23.02.2022 + */ +data class PayStatus( + val invoiceId: String, + val resultCode: PayResultCode, +) diff --git a/public/services/paylib/impl/build.gradle b/public/services/paylib/impl/build.gradle new file mode 100644 index 0000000..6f006a8 --- /dev/null +++ b/public/services/paylib/impl/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'com.android.library' + id 'kotlin-android' + id 'maven-publish' +} + +apply from: "$rootProject.projectDir/android_subproject.gradle" + +dependencies { + api project(':public:services:paylib:api') + implementation project(':public:services:paylib:aidl') + + implementation project(':public:common:asserts') + implementation project(':public:common:binderhelper:impl') + implementation project(':public:common:coroutines') + implementation project(':public:common:logger') + + implementation "androidx.annotation:annotation:$versions.annotation" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.kotlinxCoroutines" +} + +//task androidSourcesJar(type: Jar) { +// archiveClassifier.set('sources') +// from android.sourceSets.main.java.srcDirs +// from android.sourceSets.main.kotlin.srcDirs +//} +// +//afterEvaluate { +// publishing { +// repositories.add(rootProject.repositories.getByName('OSSRH')) +// +// publications { +// mavenAppstateImpl(MavenPublication) { +// from components.release +// +// artifact androidSourcesJar +// +// groupId publication.pomGroupID +// artifactId "appstate" +// version publication.pomAppStateVersion +// +// pom { +// name = "appstate" +// description = 'appstate is a library used to transmit native app state to smartapp backend' +// url = publication.githubUrl +// licenses { +// license { +// name = publication.licenseName +// url = publication.licenseUrl +// } +// } +// developers { +// developer { +// name = 'Igor Perminov' +// email = 'Perminov.I.Yurye@sberbank.ru' +// } +// developer { +// name = 'Nikolay Pahomov' +// email = 'NMPakhomov@sberbank.ru' +// } +// } +// scm { +// connection = publication.connectionUrl +// developerConnection = publication.connectionUrl +// url = publication.githubUrl +// } +// } +// } +// } +// } +// +// signing { +// sign publishing.publications.mavenAppstateImpl +// } +//} \ No newline at end of file diff --git a/public/services/paylib/impl/src/main/AndroidManifest.xml b/public/services/paylib/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..db940c0 --- /dev/null +++ b/public/services/paylib/impl/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibFactory.kt b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibFactory.kt new file mode 100644 index 0000000..8c20771 --- /dev/null +++ b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibFactory.kt @@ -0,0 +1,36 @@ +package ru.sberdevices.services.paylib + +import android.content.Context +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import ru.sberdevices.common.binderhelper.BinderHelper +import ru.sberdevices.common.binderhelper.BinderHelperFactory2 +import ru.sberdevices.common.binderhelper.CachedBinderHelper +import ru.sberdevices.common.binderhelper.createCached +import ru.sberdevices.common.coroutines.CoroutineDispatchers +import ru.sberdevices.services.paylib.aidl.wrappers.PayStatusListenerWrapperImpl + +class PayLibFactory( + private val context: Context, + private val coroutineDispatchers: CoroutineDispatchers, + private val binderHelperFactory2: BinderHelperFactory2, +) { + + fun create(): PayLib = PayLibImpl( + helper = getHelper(), + dispatchers = coroutineDispatchers, + payStatusListenerWrapper = PayStatusListenerWrapperImpl(), + callbackScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.default) + ) + + private fun getHelper(): CachedBinderHelper { + val bindIntent = BinderHelper.createBindIntent( + packageName = "ru.sberdevices.services", + className = "ru.sberdevices.services.pay.PayLibService" + ) + + return binderHelperFactory2.createCached(context, bindIntent) { + IPayLibService.Stub.asInterface(it) + } + } +} diff --git a/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibImpl.kt b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibImpl.kt new file mode 100644 index 0000000..df4f223 --- /dev/null +++ b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayLibImpl.kt @@ -0,0 +1,97 @@ +package ru.sberdevices.services.paylib + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.shareIn +import ru.sberdevices.common.binderhelper.CachedBinderHelper +import ru.sberdevices.common.binderhelper.entities.BinderState +import ru.sberdevices.common.binderhelper.repeatOnState +import ru.sberdevices.common.coroutines.CoroutineDispatchers +import ru.sberdevices.common.logger.Logger +import ru.sberdevices.services.paylib.aidl.wrappers.PayStatusListenerWrapper +import ru.sberdevices.services.paylib.entities.PayStatus + +internal class PayLibImpl( + private val helper: CachedBinderHelper, + private val dispatchers: CoroutineDispatchers, + private val payStatusListenerWrapper: PayStatusListenerWrapper, + callbackScope: CoroutineScope, +) : PayLib { + + private val logger = Logger.get("PayLibImpl") + + private val payStatusFlow: SharedFlow + + init { + logger.debug { "init" } + + payStatusFlow = callbackFlow { + logger.debug { "onStart()" } + helper.connect() + + repeatOnState(helper, BinderState.CONNECTED) { + logger.debug { "helper connected event received" } + helper.execute { it.addPayStatusListener(payStatusListenerWrapper) } + } + + payStatusListenerWrapper.payStatusFlow + .onEach { + logger.debug { "received new payStatus: $it" } + trySend(it) + } + .flowOn(dispatchers.io) + .launchIn(this) + + awaitClose { + logger.debug { "awaitClose()" } + // tryExecute, потому что scope может быть отменен из-за ошибки + helper.tryExecuteWithResult { it.removePayStatusListener(payStatusListenerWrapper) } + .onSuccess { logger.debug { "Successfully removed listener" } } + .onFailure { logger.warn(it) { "Couldn't remove listener" } } + + helper.disconnect() + } + }.shareIn( + callbackScope, + started = SharingStarted.WhileSubscribed( + stopTimeoutMillis = 3_000, + replayExpirationMillis = 0 + ), + replay = 0, + ) + } + + override suspend fun launchPayDialog(invoiceId: String): Result { + logger.debug { "launchPayDialog, invoiceId: $invoiceId" } + val launched = helper.executeWithResult { it.launchPayDialog(invoiceId) } + return if (launched.isSuccess) { + try { + val first = payStatusFlow + .filter { it.invoiceId == invoiceId } + .first() + Result.success(first) + } catch (ex: NoSuchElementException) { + Result.failure(ex) + } + } else { + val exception = launched.exceptionOrNull() + if (exception != null) { + Result.failure(exception) + } else { + Result.failure(IllegalStateException("Unknown examples")) + } + }.also { result -> + logger.debug { "launchPayDialog, invoiceId: $invoiceId result: $result" } + } + } +} diff --git a/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayResultCodeFactory.kt b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayResultCodeFactory.kt new file mode 100644 index 0000000..c742f1b --- /dev/null +++ b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/PayResultCodeFactory.kt @@ -0,0 +1,17 @@ +package ru.sberdevices.services.paylib + +import ru.sberdevices.services.paylib.entities.PayResultCode +import ru.sberdevices.services.paylib.codes.PayResultCode as AidlPayResultCode + +/** + * @author Николай Пахомов on 23.02.2022 + */ +internal object PayResultCodeFactory { + + fun fromInt(resultCode: Int): PayResultCode = when (resultCode) { + AidlPayResultCode.SUCCESS.rawCode -> PayResultCode.SUCCESS + AidlPayResultCode.ERROR.rawCode -> PayResultCode.ERROR + AidlPayResultCode.CANCELLED.rawCode -> PayResultCode.CANCELLED + else -> PayResultCode.UNKNOWN + } +} diff --git a/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapper.kt b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapper.kt new file mode 100644 index 0000000..d7abf74 --- /dev/null +++ b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapper.kt @@ -0,0 +1,12 @@ +package ru.sberdevices.services.paylib.aidl.wrappers + +import kotlinx.coroutines.flow.SharedFlow +import ru.sberdevices.services.paylib.IPayStatusListener +import ru.sberdevices.services.paylib.entities.PayStatus + +/** + * @author Николай Пахомов on 23.02.2022 + */ +internal abstract class PayStatusListenerWrapper : IPayStatusListener.Stub() { + abstract val payStatusFlow: SharedFlow +} diff --git a/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapperImpl.kt b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapperImpl.kt new file mode 100644 index 0000000..4730473 --- /dev/null +++ b/public/services/paylib/impl/src/main/kotlin/ru/sberdevices/services/paylib/aidl/wrappers/PayStatusListenerWrapperImpl.kt @@ -0,0 +1,32 @@ +package ru.sberdevices.services.paylib.aidl.wrappers + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import ru.sberdevices.common.logger.Logger +import ru.sberdevices.services.paylib.PayResultCodeFactory +import ru.sberdevices.services.paylib.entities.PayStatus + +/** + * @author Николай Пахомов on 23.02.2022 + */ +internal class PayStatusListenerWrapperImpl : PayStatusListenerWrapper() { + + private val logger = Logger.get("PayStatusListenerWrapperImpl") + + private val mutableEventsFlow = MutableSharedFlow( + onBufferOverflow = BufferOverflow.DROP_OLDEST, // Для tryEmit + extraBufferCapacity = 16 // Для BufferOverflow.DROP_OLDEST + ) + + override val payStatusFlow: SharedFlow = mutableEventsFlow + + override fun onPayStatusUpdated(invoiceId: String, resultCode: Int) { + val payStatus = PayStatus( + invoiceId = invoiceId, + resultCode = PayResultCodeFactory.fromInt(resultCode), + ) + val result = mutableEventsFlow.tryEmit(payStatus) + logger.debug { "onPayStatusUpdated, emit result: $result" } + } +} diff --git a/settings.gradle b/settings.gradle index 16e172d..278c473 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,4 +25,7 @@ include ':public:common:asserts', ':public:services:messaging:impl', ':public:services:mic_camera_state:aidl', ':public:services:mic_camera_state:api', - ':public:services:mic_camera_state:impl' + ':public:services:mic_camera_state:impl', + ':public:services:paylib:aidl', + ':public:services:paylib:api', + ':public:services:paylib:impl'