Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-31 23:19:07 +04:00
parent 552da5be18
commit 35d7b71361
10 changed files with 130 additions and 55 deletions

View file

@ -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 {

View file

@ -35,7 +35,10 @@ class TangemAppLoggerInitializer(
}
if (PERMITTED_PRIORITY.contains(priority)) {
appLogsStore.saveLogMessage(message)
appLogsStore.saveLogMessage(
tag = tag ?: "TangemAppLogger",
message = message,
)
}
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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,
)
@Immutable
data class ShareLogsUM(val file: File?)
}

View file

@ -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 = {},
),
)
}

View file

@ -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(
"""

View file

@ -13,4 +13,5 @@
<string name="excluded_blockchains" translatable="false">Excluded blockchains</string>
<string name="excluded_blockchains_search_placeholder" translatable="false">Filter by name or symbol</string>
<string name="blockchain_providers" translatable="false">Blockchain providers</string>
<string name="share_logs" translatable="false">Share logs</string>
</resources>