diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index d54f1c4c78..c8408c6e91 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -222,7 +222,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private fun updateLogFiles() { appLogsStore.deleteOldLogsFile() - appLogsStore.deleteLastLogFile() + + if (!BuildConfig.TESTER_MENU_ENABLED) { + appLogsStore.deleteLastLogFile() + } // Temporally logs are not saved // scope.launch { diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt index 1b3f599f3f..a2011ff5e7 100644 --- a/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt +++ b/app/src/main/java/com/tangem/tap/common/log/TangemAppLoggerInitializer.kt @@ -35,7 +35,10 @@ class TangemAppLoggerInitializer( } if (PERMITTED_PRIORITY.contains(priority)) { - appLogsStore.saveLogMessage(message) + appLogsStore.saveLogMessage( + tag = tag ?: "TangemAppLogger", + message = message, + ) } } } diff --git a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt index a85c3e4667..f197705464 100644 --- a/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt +++ b/app/src/main/java/com/tangem/tap/data/TangemBlockchainSDKLogger.kt @@ -15,6 +15,6 @@ internal class TangemBlockchainSDKLogger( ) : BlockchainSDKLogger { override fun log(level: BlockchainSDKLogger.Level, message: String) { - appLogsStore.saveLogMessage(message) + appLogsStore.saveLogMessage(tag = "BlockchainSDK_${level.name}", message) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt index bb1112d268..ac6d80ef03 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/logs/AppLogsStore.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.logs import android.content.Context +import com.tangem.datasource.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineExceptionHandler @@ -58,26 +59,30 @@ class AppLogsStore @Inject constructor( fun getFile(): File? = if (file.exists()) file else null /** Save log [message] */ - fun saveLogMessage(message: String) { - // Temporally logs are not saved - return + fun saveLogMessage(tag: String, message: String) { + // Temporally logs are not saved in prod environment + if (!BuildConfig.TESTER_MENU_ENABLED) { + return + } launchWithLock { createFileIfNotExist() - writeMessage(message) + writeMessage(tag = tag, message) } } /** Save log that consists from [messages] */ - fun saveLogMessage(vararg messages: String) { - // Temporally logs are not saved - return + fun saveLogMessage(tag: String, vararg messages: String) { + // Temporally logs are not saved in prod environment + if (!BuildConfig.TESTER_MENU_ENABLED) { + return + } launchWithLock { createFileIfNotExist() - writeMessage(*messages) + writeMessage(tag = tag, *messages) } } @@ -102,10 +107,10 @@ class AppLogsStore @Inject constructor( if (file.exists()) file.delete() } - private fun writeMessage(vararg messages: String) { + private fun writeMessage(tag: String, vararg messages: String) { BufferedWriter(FileWriter(file, true)).use { writer -> writer.append(formatter.print(DateTime.now())) - writer.append(": ") + writer.append(": $tag ") messages.forEach(writer::append) writer.newLine() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt index b593688bd9..26703087a9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/NetworkLogsSaveInterceptor.kt @@ -40,7 +40,7 @@ internal class NetworkLogsSaveInterceptor( try { response = chain.proceed(request) } catch (e: Exception) { - appLogsStore.saveLogMessage("<-- HTTP FAILED: $e") + saveLogMessage("<-- HTTP FAILED: $e") throw e } @@ -53,7 +53,7 @@ internal class NetworkLogsSaveInterceptor( val connection = chain.connection() val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" - appLogsStore.saveLogMessage( + saveLogMessage( "--> ${request.method} ${request.url}$connectionProtocol\n", createRequestEndMessage(request), ) @@ -136,7 +136,7 @@ internal class NetworkLogsSaveInterceptor( val spaceBeforeResponseMessage = if (response.message.isEmpty()) "" else ' ' + response.message - appLogsStore.saveLogMessage( + saveLogMessage( "<-- ${response.code}", spaceBeforeResponseMessage, response.message, @@ -198,4 +198,8 @@ internal class NetworkLogsSaveInterceptor( null } } + + private fun saveLogMessage(vararg messages: String) { + appLogsStore.saveLogMessage(tag = "NetworkLogs", *messages) + } } \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 86afbab205..5c994aba93 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) + implementation(projects.domain.feedback) /** Other libraries */ implementation(deps.arrow.core) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt index c814bf750b..9307de43a7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsContentState.kt @@ -1,21 +1,26 @@ package com.tangem.feature.tester.presentation.actions +import androidx.compose.runtime.Immutable import com.tangem.domain.apptheme.model.AppThemeMode +import java.io.File internal data class TesterActionsContentState( - val hideAllCurrenciesConfig: HideAllCurrenciesConfig, - val toggleAppThemeConfig: ToggleAppThemeConfig, + val hideAllCurrenciesUM: HideAllCurrenciesUM, + val toggleAppThemeUM: ToggleAppThemeUM, + val shareLogsUM: ShareLogsUM, val onBackClick: () -> Unit, - val onApplyChangesClick: () -> Unit, -) +) { + sealed class HideAllCurrenciesUM { + data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesUM() -internal sealed class HideAllCurrenciesConfig { - data class Clickable(val onClick: () -> Unit) : HideAllCurrenciesConfig() + data object Progress : HideAllCurrenciesUM() + } - data object Progress : HideAllCurrenciesConfig() -} + data class ToggleAppThemeUM( + val currentAppTheme: AppThemeMode, + val onClick: () -> Unit, + ) -internal data class ToggleAppThemeConfig( - val currentAppTheme: AppThemeMode, - val onClick: () -> Unit, -) \ No newline at end of file + @Immutable + data class ShareLogsUM(val file: File?) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt index adadf6886f..f19b69a825 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsScreen.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tester.presentation.actions +import android.app.Activity +import android.content.Intent import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background @@ -8,14 +10,24 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ShareCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.utils.findActivity import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.feature.tester.impl.R +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM +import timber.log.Timber +import java.io.File @OptIn(ExperimentalFoundationApi::class) @Composable @@ -24,6 +36,7 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod modifier = modifier .fillMaxSize() .background(TangemTheme.colors.background.secondary), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { stickyHeader { AppBarWithBackButton( @@ -31,42 +44,75 @@ internal fun TesterActionsScreen(state: TesterActionsContentState, modifier: Mod onBackClick = state.onBackClick, ) } + item { - val onClick = remember(state.hideAllCurrenciesConfig) { - { (state.hideAllCurrenciesConfig as? HideAllCurrenciesConfig.Clickable)?.onClick?.invoke() ?: Unit } + val onClick = remember(state.hideAllCurrenciesUM) { + { (state.hideAllCurrenciesUM as? HideAllCurrenciesUM.Clickable)?.onClick?.invoke() ?: Unit } } TesterActionItem( name = stringResourceSafe(R.string.hide_all_currencies), - progress = state.hideAllCurrenciesConfig is HideAllCurrenciesConfig.Progress, + progress = state.hideAllCurrenciesUM is HideAllCurrenciesUM.Progress, onClick = onClick, ) } + item { - val config = state.toggleAppThemeConfig + val config = state.toggleAppThemeUM TesterActionItem( name = stringResourceSafe(id = R.string.toggle_app_theme, config.currentAppTheme.name), onClick = config.onClick, ) } + + item { + val activity = LocalContext.current.findActivity() + + TesterActionItem( + name = stringResourceSafe(id = R.string.share_logs), + onClick = { activity.shareFile(file = state.shareLogsUM.file) }, + enabled = state.shareLogsUM.file != null, + ) + } } } @Composable -private fun TesterActionItem( - name: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, - progress: Boolean = false, -) { - Box(modifier = modifier.padding(all = TangemTheme.dimens.spacing16)) { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = name, - onClick = onClick, - showProgress = progress, +private fun TesterActionItem(name: String, onClick: () -> Unit, progress: Boolean = false, enabled: Boolean = true) { + PrimaryButton( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + text = name, + onClick = onClick, + showProgress = progress, + enabled = enabled, + ) +} + +private fun Activity.shareFile(file: File?) { + val originalIntent = createEmailShareIntent(activity = this, file = file) + + try { + val chooserIntent = Intent.createChooser(originalIntent, "Share logs...") + + ContextCompat.startActivity(this, chooserIntent, null) + } catch (ex: Exception) { + Timber.e("Failed to share file: $ex") + } +} + +private fun createEmailShareIntent(activity: Activity, file: File?): Intent { + val builder = ShareCompat.IntentBuilder(activity) + .setType("text/plain") + + file?.let { + builder.setStream( + FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it), ) } + + return builder.intent } // region Preview @@ -78,10 +124,10 @@ private fun TesterActionsScreenSample(modifier: Modifier = Modifier) { ) { TesterActionsScreen( state = TesterActionsContentState( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable {}, - toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT) {}, + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable {}, + toggleAppThemeUM = ToggleAppThemeUM(AppThemeMode.DEFAULT) {}, + shareLogsUM = TesterActionsContentState.ShareLogsUM(file = null), onBackClick = {}, - onApplyChangesClick = {}, ), ) } diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt index 514edb448d..1c351420c7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/actions/TesterActionsViewModel.kt @@ -9,6 +9,9 @@ import arrow.core.getOrElse import com.tangem.domain.apptheme.ChangeAppThemeModeUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.HideAllCurrenciesUM +import com.tangem.feature.tester.presentation.actions.TesterActionsContentState.ToggleAppThemeUM import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.lib.crypto.UserWalletManager import dagger.hilt.android.lifecycle.HiltViewModel @@ -24,6 +27,7 @@ internal class TesterActionsViewModel @Inject constructor( private val userWalletManager: UserWalletManager, private val changeAppThemeModeUseCase: ChangeAppThemeModeUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, + private val feedbackRepository: FeedbackRepository, ) : ViewModel() { var uiState: TesterActionsContentState by mutableStateOf(initialState) @@ -31,10 +35,13 @@ internal class TesterActionsViewModel @Inject constructor( private val initialState: TesterActionsContentState get() = TesterActionsContentState( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this::hideAllCurrencies), - toggleAppThemeConfig = ToggleAppThemeConfig(AppThemeMode.DEFAULT, this::toggleAppTheme), + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable(this::hideAllCurrencies), + toggleAppThemeUM = ToggleAppThemeUM( + currentAppTheme = AppThemeMode.DEFAULT, + onClick = this::toggleAppTheme, + ), + shareLogsUM = TesterActionsContentState.ShareLogsUM(file = feedbackRepository.getLogFile()), onBackClick = { /* no-op */ }, - onApplyChangesClick = { /* no-op */ }, ) init { @@ -47,17 +54,17 @@ internal class TesterActionsViewModel @Inject constructor( private fun hideAllCurrencies() = viewModelScope.launch { uiState = uiState.copy( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Progress, + hideAllCurrenciesUM = HideAllCurrenciesUM.Progress, ) userWalletManager.hideAllTokens() uiState = uiState.copy( - hideAllCurrenciesConfig = HideAllCurrenciesConfig.Clickable(this@TesterActionsViewModel::hideAllCurrencies), + hideAllCurrenciesUM = HideAllCurrenciesUM.Clickable(this@TesterActionsViewModel::hideAllCurrencies), ) } private fun toggleAppTheme() = viewModelScope.launch { - val currentAppThemeMode = uiState.toggleAppThemeConfig.currentAppTheme + val currentAppThemeMode = uiState.toggleAppThemeUM.currentAppTheme val newAppThemeMode = when (currentAppThemeMode) { AppThemeMode.FORCE_DARK -> AppThemeMode.FORCE_LIGHT AppThemeMode.FORCE_LIGHT -> AppThemeMode.FOLLOW_SYSTEM @@ -89,13 +96,13 @@ internal class TesterActionsViewModel @Inject constructor( Timber.d( """ Current app theme mode updated - |- Previous app theme mode: ${uiState.toggleAppThemeConfig.currentAppTheme} + |- Previous app theme mode: ${uiState.toggleAppThemeUM.currentAppTheme} |- New app theme mode: $maybeAppThemeMode """.trimIndent(), ) uiState = uiState.copy( - toggleAppThemeConfig = uiState.toggleAppThemeConfig.copy( + toggleAppThemeUM = uiState.toggleAppThemeUM.copy( currentAppTheme = maybeAppThemeMode.getOrElse { error -> Timber.e( """ diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 77b8a9c63d..12d59827a6 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -13,4 +13,5 @@ Excluded blockchains Filter by name or symbol Blockchain providers + Share logs