diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae6aec3..8a8597c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,19 +12,29 @@ android { applicationId = "com.example.mypos" minSdk = 23 targetSdk = 36 - versionCode = 1 + versionCode = 2 versionName = "1.0" buildConfigField ("String", "ACTIVATION_CODE", "\"\"") buildConfigField ("String", "APPLICATION_TOKEN", "\"\"") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + signingConfigs { + create("release") { + storeFile = file("mypos.jks") + storePassword = "app644223" + keyAlias = "pos" + keyPassword = "app644223" + } + } + buildTypes { debug { buildConfigField ("String", "ACTIVATION_CODE", "\"436596309\"") buildConfigField ("String", "APPLICATION_TOKEN", "\"mk_MbQ92RRHEOcGFRIf9/R1A\"") } release { + signingConfig = signingConfigs.getByName("release") isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), @@ -47,7 +57,8 @@ android { } dependencies { - implementation(files("libs/AditumSdkIntegration-2.3.4.860743-release.aar")) +// implementation(files("libs/AditumSdkIntegration-2.3.4.860743-release.aar")) + implementation(files("libs/AditumSdkIntegration-2.3.7-App2App.763915-release.aar")) implementation("com.google.code.gson:gson:2.13.1") implementation("androidx.datastore:datastore-preferences:1.1.7") implementation(libs.androidx.core.ktx) diff --git a/app/libs/AditumSdkIntegration-2.3.7-App2App.763915-release.aar b/app/libs/AditumSdkIntegration-2.3.7-App2App.763915-release.aar new file mode 100644 index 0000000..b1d350e Binary files /dev/null and b/app/libs/AditumSdkIntegration-2.3.7-App2App.763915-release.aar differ diff --git a/app/mypos.jks b/app/mypos.jks new file mode 100644 index 0000000..d5e8fa9 Binary files /dev/null and b/app/mypos.jks differ diff --git a/app/src/main/java/com/example/mypos/models/PaymentViewModel.kt b/app/src/main/java/com/example/mypos/models/PaymentViewModel.kt index 688790a..de705a2 100644 --- a/app/src/main/java/com/example/mypos/models/PaymentViewModel.kt +++ b/app/src/main/java/com/example/mypos/models/PaymentViewModel.kt @@ -1,8 +1,8 @@ package com.example.mypos.models import android.content.Context -import android.nfc.Tag import android.util.Log +import androidx.datastore.preferences.core.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -33,13 +33,28 @@ import br.com.aditum.data.v2.model.transactions.ConfirmTransactionCallback import br.com.aditum.data.v2.model.transactions.PendingTransactionsCallback import com.example.mypos.BuildConfig import com.example.mypos.data.AditumError -import com.example.mypos.data.PaymentRegister +import com.example.mypos.preferences.PrefKeys +import com.example.mypos.preferences.dataStore import com.example.mypos.services.PaymentApplication import com.google.gson.Gson import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +enum class SdkState { + NOT_INITIALIZED, + INITIALIZING, + IDLE, + PROCESSING, + PAYING, + CANCELING +} class PaymentViewModel( private val application: PaymentApplication, @@ -48,65 +63,265 @@ class PaymentViewModel( private val paymentApplication = application private val gson = Gson() private val TAG = "PaymentViewModel" + + // Estado do SDK + private val _sdkState = MutableStateFlow(SdkState.NOT_INITIALIZED) + val sdkState: StateFlow = _sdkState.asStateFlow() + + // Estado de conexão do serviço private val _isServiceConnected = MutableStateFlow(false) val isServiceConnected: StateFlow = _isServiceConnected.asStateFlow() + + // Mensagens e dados do PIN pad private val _messageFlow = MutableStateFlow("") val messageFlow: StateFlow = _messageFlow.asStateFlow() + private val _displayFlow = MutableStateFlow("") + val displayFlow: StateFlow = _displayFlow.asStateFlow() private val _pinLengthFlow = MutableStateFlow(0) val pinLengthFlow: StateFlow = _pinLengthFlow.asStateFlow() + // Estado de erro + private val _errorFlow = MutableStateFlow?>(null) + val errorFlow: StateFlow?> = _errorFlow.asStateFlow() + init { viewModelScope.launch { paymentApplication.isServiceConnectedFlow.collect { isConnected -> _isServiceConnected.value = isConnected if (isConnected) { try { - paymentApplication.communicationService?.registerPaymentCallback(paymentCallback) + paymentApplication.communicationService?.registerPaymentCallback(notificationCallback) ?: Log.e(TAG, "Failed to register payment callback: communicationService is null") + // Tentar inicializar automaticamente quando o serviço se conectar + autoInitialize() } catch (e: Exception) { Log.e(TAG, "Error registering payment callback: ${e.message}") + setError(AditumError.SERVICE_NULL, e.message) + } + } else { + // Quando o serviço desconecta, voltar para NOT_INITIALIZED + if (_sdkState.value != SdkState.NOT_INITIALIZED) { + _sdkState.value = SdkState.NOT_INITIALIZED } } } } } - fun startService() { - paymentApplication.startAditumSdkService() - } - - val paymentCallback = object : IPaymentCallback.Stub() { + private val notificationCallback = object : IPaymentCallback.Stub() { override fun notification( - message: String?, - transactionStatus: TransactionStatus?, - command: AbecsCommands? + message: String, + transactionStatus: TransactionStatus, + command: AbecsCommands ) { - Log.i(TAG, "\nnotification - ${ message ?: "" }") - if (message != null) { - _messageFlow.value = message.replace("\\s+".toRegex(), " ").trim() + val filteredMessage = message.replace("\\s+".toRegex(), " ").trim() + viewModelScope.launch { + if (command == AbecsCommands.Display && filteredMessage != "Callback Added") { + _displayFlow.emit(filteredMessage) + } + _messageFlow.emit(filteredMessage) } } - override fun pinNotification(message: String?, length: Int) { - _pinLengthFlow.value = length + override fun pinNotification(message: String, length: Int) { + viewModelScope.launch { + _pinLengthFlow.emit(length) + _messageFlow.emit("PIN entrada: $length dígitos") + } } override fun startGetClearData( clearDataRequest: GetClearDataRequest?, finished: GetClearDataFinishedCallback? ) { - TODO("Not yet implemented") + if (clearDataRequest != null) { + Log.d(TAG, "clearDataRequest: ${clearDataRequest.title} ${clearDataRequest.alphaNumeric} ${clearDataRequest.timeout}") + } } override fun startGetMenuSelection( - menuSelectionRequest: GetMenuSelectionRequest?, + clearDataRequest: GetMenuSelectionRequest?, finished: GetMenuSelectionFinishedCallback? ) { - TODO("Not yet implemented") + // Implementação customizada se necessário } - override fun qrCodeGenerated(qrCode: String?, expirationTime: Int) { - Log.d(TAG, "\nqrCode - ${ qrCode ?: "" }\nexpirationTime - $expirationTime") + override fun qrCodeGenerated(qrCode: String, expirationTime: Int) { + viewModelScope.launch { + _messageFlow.emit("QR Code gerado - Expira em: $expirationTime segundos") + } + } + } + + private fun setError(errorCode: String, errorMessage: String?) { + _errorFlow.value = Pair(errorCode, errorMessage) + } + + private fun clearError() { + _errorFlow.value = null + } + + private suspend fun getActivationCode(): String? { + return context.dataStore.data.map { preferences -> + preferences[PrefKeys.ACTIVATION_CODE] + }.firstOrNull() + } + + private suspend fun getNewActivationCode(): String? { + return context.dataStore.data.map { preferences -> + preferences[PrefKeys.NEW_ACTIVATION_CODE] + }.firstOrNull() + } + + fun saveActivationCode(code: String?) { + viewModelScope.launch { + context.dataStore.edit { preferences -> + if (code != null) { + preferences[PrefKeys.ACTIVATION_CODE] = code + } else { + preferences.remove(PrefKeys.ACTIVATION_CODE) + } + } + } + } + + fun saveNewActivationCode(code: String?) { + viewModelScope.launch { + context.dataStore.edit { preferences -> + if (code != null) { + preferences[PrefKeys.NEW_ACTIVATION_CODE] = code + } else { + preferences.remove(PrefKeys.NEW_ACTIVATION_CODE) + } + } + } + } + + private suspend fun clearNewActivationCode() { + context.dataStore.edit { preferences -> + preferences.remove(PrefKeys.NEW_ACTIVATION_CODE) + } + } + + private suspend fun autoInitialize() { + Log.d(TAG, "autoInitialize - Iniciando inicialização automática") + + try { + val activationCode = getActivationCode() + val newActivationCode = getNewActivationCode() + + Log.d(TAG, "autoInitialize - activation_code: $activationCode, new_activation_code: $newActivationCode") + + if (activationCode == null && newActivationCode == null) { + Log.d(TAG, "autoInitialize - Nenhum código de ativação encontrado") + return + } + + // Se os códigos são iguais, não precisa fazer nada + if (activationCode != null && newActivationCode != null && activationCode == newActivationCode) { + Log.d(TAG, "autoInitialize - Códigos são iguais, limpando new_activation_code") + clearNewActivationCode() + return + } + + // Se existe um novo código diferente do atual, fazer a troca + if (newActivationCode != null && activationCode != null && activationCode != newActivationCode) { + Log.d(TAG, "autoInitialize - Desativando código antigo") + val deactivationSuccess = suspendCancellableCoroutine { continuation -> + deactivateInternal( + resolve = { success -> continuation.resume(success) }, + reject = { error, message -> + Log.e(TAG, "Erro na desativação: $error - $message") + continuation.resume(false) + } + ) + } + + if (!deactivationSuccess) { + Log.e(TAG, "autoInitialize - Falha ao desativar código antigo") + setError(AditumError.DEACTIVATE_ERROR, "Falha ao desativar para usar novo código") + return + } + } + + // Usar o código apropriado para inicialização + val codeToUse = newActivationCode ?: activationCode + Log.d(TAG, "autoInitialize - Inicializando com código: $codeToUse") + + initAditumSdkInternal( + applicationName = "MyPOS", + applicationVersion = BuildConfig.VERSION_NAME, + activationCode = codeToUse, + resolve = { success -> + if (success && newActivationCode != null) { + // Se usou o novo código com sucesso, salvar como atual e limpar o novo + viewModelScope.launch { + saveActivationCode(newActivationCode) + clearNewActivationCode() + Log.d(TAG, "autoInitialize - Novo código salvo como atual") + } + } + }, + reject = { error, message -> + Log.e(TAG, "autoInitialize - Erro na inicialização: $error - $message") + setError(error, message) + } + ) + } catch (e: Exception) { + Log.e(TAG, "autoInitialize - Exceção: ${e.message}") + setError(AditumError.INIT_ERROR, e.message) + } + } + + private suspend fun ensureServiceReady(): Boolean { + if (!isServiceConnected.value) { + Log.d(TAG, "ensureServiceReady - Serviço não conectado, tentando conectar") + val connected = paymentApplication.startAditumSdkService(context) + if (!connected) { + setError(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + return false + } + } + + // Verificar se o SDK está em estado apropriado + when (_sdkState.value) { + SdkState.NOT_INITIALIZED -> { + Log.d(TAG, "ensureServiceReady - SDK não inicializado, tentando inicializar") + return initializeIfNeeded() + } + SdkState.INITIALIZING, SdkState.PROCESSING, SdkState.PAYING, SdkState.CANCELING -> { + setError(AditumError.SERVICE_NULL, "SDK ocupado com outra operação") + return false + } + SdkState.IDLE -> { + return true + } + } + return false + } + + private suspend fun initializeIfNeeded(): Boolean = suspendCancellableCoroutine { continuation -> + val activationCode = viewModelScope.launch { + val currentCode = getActivationCode() + val newCode = getNewActivationCode() + val codeToUse = newCode ?: currentCode + + if (codeToUse == null) { + setError(AditumError.INIT_ERROR, "Nenhum código de ativação disponível") + continuation.resume(false) + return@launch + } + + initAditumSdkInternal( + applicationName = "MyPOS", + applicationVersion = BuildConfig.VERSION_NAME, + activationCode = codeToUse, + resolve = { success -> continuation.resume(success) }, + reject = { error, message -> + setError(error, message) + continuation.resume(false) + } + ) } } @@ -116,12 +331,34 @@ class PaymentViewModel( activationCode: String?, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> } + ) { + if (_sdkState.value == SdkState.INITIALIZING) { + reject(AditumError.INIT_ERROR, "SDK já está inicializando") + return + } + + initAditumSdkInternal(applicationName, applicationVersion, activationCode, resolve, reject) + } + + private fun initAditumSdkInternal( + applicationName: String, + applicationVersion: String, + activationCode: String?, + resolve: (Boolean) -> Unit = {}, + reject: (String, String?) -> Unit = { _, _ -> } ) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) - return@launch + _sdkState.value = SdkState.INITIALIZING + clearError() + + if (!isServiceConnected.value) { + val connected = paymentApplication.startAditumSdkService(context) + if (!connected) { + _sdkState.value = SdkState.NOT_INITIALIZED + reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + return@launch + } } val pinpadMessages = PinpadMessages().apply { @@ -139,43 +376,37 @@ class PaymentViewModel( val callback = object : InitResponseCallback.Stub() { override fun onResponse(initResponse: InitResponse?) { - if (initResponse != null) { - resolve(initResponse.initialized) - } else { - Log.e(TAG, "onResponse - initResponse is null") - reject(AditumError.INIT_RESPONSE_NULL, AditumError.INIT_RESPONSE_NULL_MESSAGE) + viewModelScope.launch { + if (initResponse != null && initResponse.initialized) { + _sdkState.value = SdkState.IDLE + if (activationCode != null) { + saveActivationCode(activationCode) + clearNewActivationCode() + } + resolve(true) + } else { + _sdkState.value = SdkState.NOT_INITIALIZED + val errorMsg = "Falha na inicialização do SDK" + setError(AditumError.INIT_ERROR, errorMsg) + reject(AditumError.INIT_ERROR, errorMsg) + } } } } paymentApplication.communicationService?.init(initRequest, callback) - ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + ?: run { + _sdkState.value = SdkState.NOT_INITIALIZED + reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + } } catch (e: Exception) { + _sdkState.value = SdkState.NOT_INITIALIZED + setError(AditumError.INIT_ERROR, "${AditumError.INIT_ERROR_MESSAGE}: ${e.message}") reject(AditumError.INIT_ERROR, "${AditumError.INIT_ERROR_MESSAGE}: ${e.message}") } } } - fun getMerchantData(resolve: (MerchantData) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { - viewModelScope.launch { - try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) - return@launch - } - - val merchantData: MerchantData? = paymentApplication.communicationService?.merchantData - if (merchantData != null) { - resolve(merchantData) - } else { - reject(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE) - } - } catch (e: Exception) { - reject(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}") - } - } - } - fun pay( amount: Long, installments: Int? = null, @@ -187,13 +418,17 @@ class PaymentViewModel( ) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + if (!ensureServiceReady()) { return@launch } + _sdkState.value = SdkState.PAYING + clearError() + val adjustedAmount = amountSeasoning?.invoke(amount) ?: amount if (adjustedAmount <= 0) { + _sdkState.value = SdkState.IDLE + setError(AditumError.INVALID_AMOUNT, AditumError.INVALID_AMOUNT_MESSAGE) reject(AditumError.INVALID_AMOUNT, AditumError.INVALID_AMOUNT_MESSAGE) return@launch } @@ -216,20 +451,30 @@ class PaymentViewModel( val callback = object : PaymentResponseCallback.Stub() { override fun onResponse(paymentResponse: PaymentResponse?) { - if (paymentResponse != null) { - val json = gson.toJson(paymentResponse) - Log.d(TAG, "onResponse - $json") - resolve(paymentResponse) - } else { - Log.e(TAG, "onResponse - paymentResponse is null") - reject(AditumError.PAYMENT_RESPONSE_NULL, AditumError.PAYMENT_RESPONSE_NULL_MESSAGE) + viewModelScope.launch { + _sdkState.value = SdkState.IDLE + if (paymentResponse != null) { + val json = gson.toJson(paymentResponse) + Log.d(TAG, "Payment response: $json") + resolve(paymentResponse) + } else { + val errorMsg = "Resposta de pagamento nula" + setError(AditumError.PAYMENT_RESPONSE_NULL, errorMsg) + reject(AditumError.PAYMENT_RESPONSE_NULL, errorMsg) + } } } } paymentApplication.communicationService?.pay(paymentRequest, callback) - ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + ?: run { + _sdkState.value = SdkState.IDLE + setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + } } catch (e: Exception) { + _sdkState.value = SdkState.IDLE + setError(AditumError.PAYMENT_ERROR, "${AditumError.PAYMENT_ERROR_MESSAGE}: ${e.message}") reject(AditumError.PAYMENT_ERROR, "${AditumError.PAYMENT_ERROR_MESSAGE}: ${e.message}") } } @@ -238,20 +483,31 @@ class PaymentViewModel( fun confirm(nsu: String, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + if (!ensureServiceReady()) { return@launch } + _sdkState.value = SdkState.PROCESSING + clearError() + val callback = object : ConfirmTransactionCallback.Stub() { override fun onResponse(confirmed: Boolean) { - resolve(confirmed) + viewModelScope.launch { + _sdkState.value = SdkState.IDLE + resolve(confirmed) + } } } paymentApplication.communicationService?.confirmTransaction(nsu, callback) - ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + ?: run { + _sdkState.value = SdkState.IDLE + setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + } } catch (e: Exception) { + _sdkState.value = SdkState.IDLE + setError(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}") reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}") } } @@ -260,91 +516,152 @@ class PaymentViewModel( fun cancel(nsu: String, isReversal: Boolean, resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + if (!ensureServiceReady()) { return@launch } + _sdkState.value = SdkState.CANCELING + clearError() + val cancelationRequest = CancelationRequest(nsu, isReversal) val callback = object : CancelationResponseCallback.Stub() { override fun onResponse(cancelationResponse: CancelationResponse?) { - if (cancelationResponse != null) { - resolve(cancelationResponse.canceled) - } else { - Log.e(TAG, "onResponse - cancelationResponse is null") - reject(AditumError.CANCEL_RESPONSE_NULL, AditumError.CANCEL_RESPONSE_NULL_MESSAGE) + viewModelScope.launch { + _sdkState.value = SdkState.IDLE + if (cancelationResponse != null) { + resolve(cancelationResponse.canceled) + } else { + val errorMsg = "Resposta de cancelamento nula" + setError(AditumError.CANCEL_RESPONSE_NULL, errorMsg) + reject(AditumError.CANCEL_RESPONSE_NULL, errorMsg) + } } } } paymentApplication.communicationService?.cancel(cancelationRequest, callback) - ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + ?: run { + _sdkState.value = SdkState.IDLE + setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + } } catch (e: Exception) { + _sdkState.value = SdkState.IDLE + setError(AditumError.CANCEL_ERROR, "${AditumError.CANCEL_ERROR_MESSAGE}: ${e.message}") reject(AditumError.CANCEL_ERROR, "${AditumError.CANCEL_ERROR_MESSAGE}: ${e.message}") } } } - fun aboutOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { - viewModelScope.launch { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) - return@launch - } - - paymentApplication.communicationService?.abortOperation() - - } - } - - fun pendingTransactions(resolve: (List?) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ ->}) { + fun abortOperation(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { - reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) + if (!ensureServiceReady()) { return@launch } + paymentApplication.communicationService?.abortOperation() + _sdkState.value = SdkState.IDLE + resolve(true) + } catch (e: Exception) { + setError(AditumError.SERVICE_NULL, "${AditumError.SERVICE_NULL_MESSAGE}: ${e.message}") + reject(AditumError.SERVICE_NULL, "${AditumError.SERVICE_NULL_MESSAGE}: ${e.message}") + } + } + } + + fun pendingTransactions(resolve: (List?) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { + viewModelScope.launch { + try { + if (!ensureServiceReady()) { + return@launch + } + + _sdkState.value = SdkState.PROCESSING + val callback = object : PendingTransactionsCallback.Stub() { override fun onResponse(results: List?) { - val json = gson.toJson(results) - Log.e(TAG, "onResponse - $json") - resolve(results) + viewModelScope.launch { + _sdkState.value = SdkState.IDLE + val json = gson.toJson(results) + Log.d(TAG, "Pending transactions: $json") + resolve(results) + } } } paymentApplication.communicationService?.pendingTransactions(callback) - ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + ?: run { + _sdkState.value = SdkState.IDLE + setError(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) + } } catch (e: Exception) { + _sdkState.value = SdkState.IDLE + setError(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}") reject(AditumError.CONFIRM_ERROR, "${AditumError.CONFIRM_ERROR_MESSAGE}: ${e.message}") } } } fun deactivate(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { + deactivateInternal(resolve, reject) + } + + private fun deactivateInternal(resolve: (Boolean) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { viewModelScope.launch { try { - if (!paymentApplication.ensureServiceConnected()) { + if (!isServiceConnected.value) { reject(AditumError.SERVICE_NOT_AVAILABLE, AditumError.SERVICE_NOT_AVAILABLE_MESSAGE) return@launch } val callback = object : DeactivationResponseCallback.Stub() { override fun onResponse(status: Boolean) { - Log.d(TAG, "onDeactivationResponse - deactivationResponse: $status") - resolve(status) + viewModelScope.launch { + _sdkState.value = SdkState.NOT_INITIALIZED + Log.d(TAG, "Deactivation response: $status") + if (status) { + // Limpar os códigos de ativação quando desativar + saveActivationCode(null) + clearNewActivationCode() + } + resolve(status) + } } } paymentApplication.communicationService?.deactivate(callback) ?: reject(AditumError.SERVICE_NULL, AditumError.SERVICE_NULL_MESSAGE) } catch (e: Exception) { + setError(AditumError.DEACTIVATE_ERROR, "${AditumError.DEACTIVATE_ERROR_MESSAGE}: ${e.message}") reject(AditumError.DEACTIVATE_ERROR, "${AditumError.DEACTIVATE_ERROR_MESSAGE}: ${e.message}") } } } + fun getMerchantData(resolve: (MerchantData) -> Unit = {}, reject: (String, String?) -> Unit = { _, _ -> }) { + viewModelScope.launch { + try { + if (!ensureServiceReady()) { + return@launch + } + + val merchantData: MerchantData? = paymentApplication.communicationService?.merchantData + if (merchantData != null) { + resolve(merchantData) + } else { + setError(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE) + reject(AditumError.MERCHANT_DATA_NULL, AditumError.MERCHANT_DATA_NULL_MESSAGE) + } + } catch (e: Exception) { + setError(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}") + reject(AditumError.MERCHANT_DATA_ERROR, "${AditumError.MERCHANT_DATA_ERROR_MESSAGE}: ${e.message}") + } + } + } + companion object { fun provideFactory( paymentApplication: PaymentApplication, diff --git a/app/src/main/java/com/example/mypos/preferences/PrefKeys.kt b/app/src/main/java/com/example/mypos/preferences/PrefKeys.kt index 1916c34..660fc5c 100644 --- a/app/src/main/java/com/example/mypos/preferences/PrefKeys.kt +++ b/app/src/main/java/com/example/mypos/preferences/PrefKeys.kt @@ -11,4 +11,5 @@ val Context.dataStore: DataStore by preferencesDataStore(name = "my object PrefKeys { val SERIAL_NUMBER = stringPreferencesKey("session_serial_number") val ACTIVATION_CODE = stringPreferencesKey("session_activation_code") + val NEW_ACTIVATION_CODE = stringPreferencesKey("new_session_activation_code") } \ No newline at end of file diff --git a/app/src/main/java/com/example/mypos/screen/PaymentScreen.kt b/app/src/main/java/com/example/mypos/screen/PaymentScreen.kt index 29d709c..349ce1b 100644 --- a/app/src/main/java/com/example/mypos/screen/PaymentScreen.kt +++ b/app/src/main/java/com/example/mypos/screen/PaymentScreen.kt @@ -1,21 +1,26 @@ package com.example.mypos.screen + import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import br.com.aditum.data.v2.enums.PaymentType import com.example.mypos.BuildConfig import com.example.mypos.models.PaymentViewModel -import kotlinx.coroutines.flow.collectLatest +import com.example.mypos.models.SdkState @Composable fun PaymentScreen(viewModel: PaymentViewModel) { @@ -23,202 +28,409 @@ fun PaymentScreen(viewModel: PaymentViewModel) { var amount by remember { mutableStateOf("") } var installments by remember { mutableStateOf("1") } var paymentType by remember { mutableStateOf(PaymentType.Credit) } + var activationCode by remember { mutableStateOf(if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else "") } + var newActivationCode by remember { mutableStateOf(if (BuildConfig.DEBUG) BuildConfig.ACTIVATION_CODE else "") } + val isServiceConnected by viewModel.isServiceConnected.collectAsState() + val sdkState by viewModel.sdkState.collectAsState() val message by viewModel.messageFlow.collectAsState() val pinLength by viewModel.pinLengthFlow.collectAsState() - var isInitializing by remember { mutableStateOf(false) } - var isPaying by remember { mutableStateOf(false) } - var showLoading by remember { mutableStateOf(false) } + val error by viewModel.errorFlow.collectAsState() + val scrollState = rememberScrollState() val handler = remember { Handler(Looper.getMainLooper()) } - if (showLoading) { - LoadingScreen( - message = message, - onDismiss = { showLoading = false } - ) + + LaunchedEffect(error) { + error?.let { (errorCode, errorMessage) -> + handler.post { + Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show() + } + } } Column( modifier = Modifier .fillMaxSize() - .padding(16.dp), + .padding(16.dp) + .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp) ) { Text( - text = "Pagamento", + text = "Sistema de Pagamento POS", style = MaterialTheme.typography.headlineMedium ) - Text( - text = if (isServiceConnected) "Serviço Conectado" else "Serviço Desconectado", - color = if (isServiceConnected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error - ) + + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = when { + !isServiceConnected -> MaterialTheme.colorScheme.errorContainer + sdkState == SdkState.IDLE -> MaterialTheme.colorScheme.primaryContainer + sdkState == SdkState.NOT_INITIALIZED -> MaterialTheme.colorScheme.secondaryContainer + else -> MaterialTheme.colorScheme.tertiaryContainer + } + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Status do Sistema", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = if (isServiceConnected) "Serviço: Conectado" else "Serviço: Desconectado", + style = MaterialTheme.typography.bodyMedium + ) + + Text( + text = "SDK: ${getSdkStateText(sdkState)}", + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + + if (message.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (pinLength > 0) { + Text( + text = "PIN: ${"*".repeat(pinLength)}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + } + } + } + + + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Códigos de Ativação", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = activationCode, + onValueChange = { activationCode = it }, + label = { Text("Código de Ativação Atual") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + modifier = Modifier.fillMaxWidth(), + enabled = sdkState != SdkState.INITIALIZING + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + if (activationCode.isBlank()) { + handler.post { + Toast.makeText(context, "Código de ativação não pode estar vazio", Toast.LENGTH_SHORT).show() + } + return@Button + } + viewModel.saveActivationCode(activationCode) + handler.post { + Toast.makeText(context, "Código de ativação salvo com sucesso", Toast.LENGTH_SHORT).show() + } + }, + enabled = sdkState != SdkState.INITIALIZING, + modifier = Modifier.fillMaxWidth() + ) { + Text("Salvar Código Atual") + } + + Spacer(modifier = Modifier.height(12.dp)) + + OutlinedTextField( + value = newActivationCode, + onValueChange = { newActivationCode = it }, + label = { Text("Novo Código de Ativação") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + modifier = Modifier.fillMaxWidth(), + enabled = sdkState != SdkState.INITIALIZING + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { + if (newActivationCode.isBlank()) { + handler.post { + Toast.makeText(context, "Novo código de ativação não pode estar vazio", Toast.LENGTH_SHORT).show() + } + return@Button + } + viewModel.saveNewActivationCode(newActivationCode) + handler.post { + Toast.makeText(context, "Novo código de ativação salvo com sucesso", Toast.LENGTH_SHORT).show() + } + }, + enabled = sdkState != SdkState.INITIALIZING, + modifier = Modifier.fillMaxWidth() + ) { + Text("Salvar Novo Código") + } + } + } + Button( onClick = { - if (!isServiceConnected) { - viewModel.startService() + val codeToUse = if (newActivationCode.isNotBlank()) newActivationCode else activationCode + if (codeToUse.isBlank()) { handler.post { - Toast.makeText(context, "Tentando conectar ao serviço...", Toast.LENGTH_SHORT).show() + Toast.makeText(context, "Insira um código de ativação", Toast.LENGTH_SHORT).show() } return@Button } - if (!isInitializing) { - isInitializing = true - showLoading = true - viewModel.initAditumSdk( - applicationName = "MyPOS", - applicationVersion = "1.0.0", - activationCode = BuildConfig.ACTIVATION_CODE, - resolve = { success -> - isInitializing = false - showLoading = false + + viewModel.initAditumSdk( + applicationName = "MyPOS", + applicationVersion = BuildConfig.VERSION_NAME, + activationCode = codeToUse, + resolve = { success -> + handler.post { + Toast.makeText( + context, + if (success) "SDK inicializado com sucesso!" else "Falha na inicialização do SDK", + Toast.LENGTH_LONG + ).show() + } + }, + reject = { errorCode, errorMessage -> + handler.post { + Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show() + } + } + ) + }, + enabled = sdkState !in listOf(SdkState.INITIALIZING, SdkState.PROCESSING, SdkState.PAYING, SdkState.CANCELING), + modifier = Modifier.fillMaxWidth() + ) { + Text( + when (sdkState) { + SdkState.INITIALIZING -> "Inicializando SDK..." + SdkState.IDLE -> "Reinicializar SDK" + else -> "Inicializar SDK" + } + ) + } + + + Card(modifier = Modifier.fillMaxWidth()) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Realizar Pagamento", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = amount, + onValueChange = { amount = it.filter { char -> char.isDigit() || char == '.' } }, + label = { Text("Valor (R$)") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), + modifier = Modifier.fillMaxWidth(), + enabled = sdkState == SdkState.IDLE + ) + + Spacer(modifier = Modifier.height(8.dp)) + + OutlinedTextField( + value = installments, + onValueChange = { installments = it.filter { char -> char.isDigit() } }, + label = { Text("Quantidade de Parcelas") }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.fillMaxWidth(), + enabled = paymentType == PaymentType.Credit && sdkState == SdkState.IDLE + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = paymentType == PaymentType.Credit, + onClick = { if (sdkState == SdkState.IDLE) paymentType = PaymentType.Credit }, + enabled = sdkState == SdkState.IDLE + ) + Text("Crédito") + } + Row(verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = paymentType == PaymentType.Debit, + onClick = { if (sdkState == SdkState.IDLE) paymentType = PaymentType.Debit }, + enabled = sdkState == SdkState.IDLE + ) + Text("Débito") + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Button( + onClick = { + val amountValue = amount.toDoubleOrNull()?.times(100)?.toLong() + val installmentCount = installments.toIntOrNull() ?: 1 + + if (amountValue == null || amountValue <= 0) { handler.post { - Toast.makeText( - context, - if (success) "SDK inicializado com sucesso!" else "Falha na inicialização.", - Toast.LENGTH_LONG - ).show() + Toast.makeText(context, "Insira um valor válido", Toast.LENGTH_SHORT).show() } - }, - reject = { errorCode, errorMessage -> - isInitializing = false - showLoading = false - handler.post { - Toast.makeText(context, "Erro: $errorMessage", Toast.LENGTH_LONG).show() + return@Button + } + + viewModel.pay( + amount = amountValue, + installments = if (paymentType == PaymentType.Credit) installmentCount else null, + paymentType = paymentType, + allowContactless = true, + resolve = { response -> + handler.post { + Toast.makeText(context, "Pagamento realizado com sucesso!", Toast.LENGTH_LONG).show() + } + }, + reject = { errorCode, errorMessage -> + handler.post { + Toast.makeText(context, "Erro no pagamento: $errorMessage", Toast.LENGTH_LONG).show() + } } + ) + }, + enabled = sdkState == SdkState.IDLE, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = if (sdkState == SdkState.PAYING) + MaterialTheme.colorScheme.secondary + else + MaterialTheme.colorScheme.primary + ) + ) { + Text( + when (sdkState) { + SdkState.PAYING -> "Processando Pagamento..." + SdkState.IDLE -> "Realizar Pagamento" + else -> "Aguarde..." } ) } - }, - enabled = !isInitializing, - modifier = Modifier.fillMaxWidth() - ) { - Text(if (isInitializing) "Inicializando..." else "Inicializar SDK") + } } - OutlinedTextField( - value = amount, - onValueChange = { amount = it.filter { char -> char.isDigit() || char == '.' } }, - label = { Text("Valor (R$)") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), - modifier = Modifier.fillMaxWidth() - ) - - OutlinedTextField( - value = installments, - onValueChange = { installments = it.filter { char -> char.isDigit() } }, - label = { Text("Quantidade de Parcelas") }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier.fillMaxWidth(), - enabled = paymentType == PaymentType.Credit - ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = paymentType == PaymentType.Credit, - onClick = { paymentType = PaymentType.Credit } - ) - Text("Crédito") - } - Row(verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = paymentType == PaymentType.Debit, - onClick = { paymentType = PaymentType.Debit } - ) - Text("Débito") - } - } - - Button( - onClick = { - val amountValue = amount.toDoubleOrNull()?.times(100)?.toLong() - val installmentCount = installments.toIntOrNull() ?: 1 - if (amountValue == null || amountValue <= 0) { - handler.post { - Toast.makeText(context, "Insira um valor válido", Toast.LENGTH_SHORT).show() - } - return@Button - } - if (!isServiceConnected) { - handler.post { - Toast.makeText(context, "Serviço não conectado", Toast.LENGTH_SHORT).show() - } - return@Button - } - if (!isPaying) { - isPaying = true - showLoading = true - viewModel.pay( - amount = amountValue, - installments = if (paymentType == PaymentType.Credit) installmentCount else null, - paymentType = paymentType, - allowContactless = true, - resolve = { response -> - isPaying = false - showLoading = false + Button( + onClick = { + viewModel.abortOperation( + resolve = { handler.post { - Toast.makeText(context, "Pagamento realizado com sucesso!", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Operação cancelada", Toast.LENGTH_SHORT).show() } }, reject = { errorCode, errorMessage -> - isPaying = false - showLoading = false handler.post { - Toast.makeText(context, "Erro no pagamento: $errorMessage", Toast.LENGTH_LONG).show() + Toast.makeText(context, "Erro ao cancelar: $errorMessage", Toast.LENGTH_SHORT).show() } } ) + }, + enabled = sdkState in listOf(SdkState.PAYING, SdkState.PROCESSING, SdkState.CANCELING), + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error) + ) { + Text("Cancelar Operação") + } + + Button( + onClick = { + viewModel.deactivate( + resolve = { success -> + handler.post { + Toast.makeText( + context, + if (success) "SDK desativado com sucesso" else "Falha ao desativar SDK", + Toast.LENGTH_SHORT + ).show() + } + }, + reject = { errorCode, errorMessage -> + handler.post { + Toast.makeText(context, "Erro na desativação: $errorMessage", Toast.LENGTH_SHORT).show() + } + } + ) + }, + enabled = sdkState == SdkState.IDLE, + colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondary) + ) { + Text("Desativar SDK") + } + } + + + if (sdkState in listOf(SdkState.INITIALIZING, SdkState.PAYING, SdkState.PROCESSING, SdkState.CANCELING)) { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = when (sdkState) { + SdkState.INITIALIZING -> "Inicializando SDK..." + SdkState.PAYING -> "Processando pagamento..." + SdkState.PROCESSING -> "Processando operação..." + SdkState.CANCELING -> "Cancelando operação..." + else -> "Aguarde..." + }, + style = MaterialTheme.typography.bodyMedium + ) + if (message.isNotEmpty()) { + Text( + text = message, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - }, - enabled = !isPaying, - modifier = Modifier.fillMaxWidth() - ) { - Text(if (isPaying) "Processando Pagamento..." else "Realizar Pagamento") - } - - if (message.isNotEmpty() && !showLoading) { - Text( - text = "Mensagem do PIN pad: $message", - style = MaterialTheme.typography.bodyMedium - ) - } - - if (pinLength > 0) { - Text( - text = "Comprimento do PIN: $pinLength", - style = MaterialTheme.typography.bodyMedium - ) + } } } } -@Composable -fun LoadingScreen(message: String, onDismiss: () -> Unit) { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.8f)), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .padding(16.dp) - ) { - CircularProgressIndicator() - Text( - text = message.ifEmpty { "Processando..." }, - style = MaterialTheme.typography.bodyLarge - ) - } +private fun getSdkStateText(state: SdkState): String { + return when (state) { + SdkState.NOT_INITIALIZED -> "Não Inicializado" + SdkState.INITIALIZING -> "Inicializando" + SdkState.IDLE -> "Pronto" + SdkState.PROCESSING -> "Processando" + SdkState.PAYING -> "Pagando" + SdkState.CANCELING -> "Cancelando" } } \ No newline at end of file diff --git a/app/src/main/java/com/example/mypos/services/PaymentApplication.kt b/app/src/main/java/com/example/mypos/services/PaymentApplication.kt index 063bb88..5c4fd0d 100644 --- a/app/src/main/java/com/example/mypos/services/PaymentApplication.kt +++ b/app/src/main/java/com/example/mypos/services/PaymentApplication.kt @@ -10,12 +10,12 @@ import android.os.IBinder import android.util.Log import br.com.aditum.IAditumSdkService import br.com.aditum.data.v2.model.MerchantData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.withContext +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException class PaymentApplication : Application() { companion object { @@ -55,81 +55,14 @@ class PaymentApplication : Application() { } } - private var mServiceConnectionListener: OnServiceConnectionListener? = null - var serviceConnectionListener: OnServiceConnectionListener? - get() = mServiceConnectionListener - set(listener) { - mServiceConnectionListener = listener - } - interface OnServiceConnectionListener { fun onServiceConnection(serviceConnected: Boolean) } - override fun onCreate() { - super.onCreate() - Log.d(TAG, "onCreate") - startAditumSdkService() - } + private var serviceConnectionListener: OnServiceConnectionListener? = null - override fun onTerminate() { - super.onTerminate() - Log.d(TAG, "onTerminate") - } - - fun startAditumSdkService() { - Log.d(TAG, "startAditumSdkService") - val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply { - setPackage(PACKAGE_NAME) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Log.d(TAG, "Android Oreo or higher: ${Build.VERSION.SDK_INT}") - startForegroundService(intent) - } else { - Log.d(TAG, "Android Nougat or lower: ${Build.VERSION.SDK_INT}") - startService(intent) - } - - bindService(intent, mServiceConnection, BIND_AUTO_CREATE or CONTEXT_IGNORE_SECURITY) - Log.d(TAG, "startAditumSdkService - bindService") - } - - suspend fun ensureServiceConnected(): Boolean = withContext(Dispatchers.IO) { - if (isServiceConnected && mAditumSdkService?.asBinder()?.isBinderAlive == true) { - Log.d(TAG, "Service already connected") - return@withContext true - } - - Log.d(TAG, "Service not connected, attempting to reconnect...") - val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply { - setPackage(PACKAGE_NAME) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - - val bound = bindService( - intent, - mServiceConnection, - BIND_AUTO_CREATE or CONTEXT_IGNORE_SECURITY - ) - - if (bound) { - repeat(30) { - delay(100) - if (isServiceConnected && mAditumSdkService?.asBinder()?.isBinderAlive == true) { - Log.d(TAG, "Service reconnected successfully") - return@withContext true - } - } - } - - Log.e(TAG, "Failed to reconnect service") - return@withContext false + fun setServiceConnectionListener(listener: OnServiceConnectionListener) { + this.serviceConnectionListener = listener } private fun setServiceConnected(isConnected: Boolean) { @@ -137,7 +70,53 @@ class PaymentApplication : Application() { synchronized(this) { mIsServiceConnected = isConnected _isServiceConnectedFlow.value = isConnected - mServiceConnectionListener?.onServiceConnection(mIsServiceConnected) + serviceConnectionListener?.onServiceConnection(isConnected) } } + override fun onCreate() { + super.onCreate() + Log.d(TAG, "onCreate") + } + + override fun onTerminate() { + super.onTerminate() + Log.d(TAG, "onTerminate") + } + + fun startAditumSdkService(context: Context): Boolean { + Log.d(TAG, "startAditumSdkService") + val intent = Intent(ACTION_COMMUNICATION_SERVICE).apply { + setPackage(PACKAGE_NAME) + } + + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent) + } else { + context.startService(intent) + } + context.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE) + } catch (e: Exception) { + Log.e(TAG, "Failed to start or bind service: ${e.message}") + false + } + } + + fun retryServiceConnection(context: Context, maxAttempts: Int = 3, delayMs: Long = 1000) { + var attempts = 0 + while (attempts < maxAttempts && !isServiceConnected) { + if (startAditumSdkService(context)) { + return + } + Thread.sleep(delayMs) + attempts++ + } + if (!isServiceConnected) { + Log.e(TAG, "Failed to connect to service after $maxAttempts attempts") + } + } + + fun ensureServiceConnected(): Boolean { + return isServiceConnected + } } \ No newline at end of file