Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-22 14:57:15 +04:00
parent 842b3e494a
commit e01127186c
14 changed files with 128 additions and 131 deletions

View file

@ -3,19 +3,19 @@ package com.tangem.tap.common.log
import android.util.Log import android.util.Log
import com.orhanobut.logger.AndroidLogAdapter import com.orhanobut.logger.AndroidLogAdapter
import com.orhanobut.logger.Logger import com.orhanobut.logger.Logger
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import timber.log.Timber import timber.log.Timber
/** /**
* Tangem app logger * Tangem app logger
* *
* @property settingsRepository repository for saving logs * @property appLogsStore app logs store
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
class TangemAppLoggerInitializer( class TangemAppLoggerInitializer(
private val settingsRepository: SettingsRepository, private val appLogsStore: AppLogsStore,
) { ) {
/** Initialize */ /** Initialize */
@ -35,7 +35,7 @@ class TangemAppLoggerInitializer(
} }
if (PERMITTED_PRIORITY.contains(priority)) { if (PERMITTED_PRIORITY.contains(priority)) {
settingsRepository.saveLogMessage(message) appLogsStore.saveLogMessage(message)
} }
} }
} }

View file

@ -3,26 +3,26 @@ package com.tangem.tap.common.log
import com.tangem.Log import com.tangem.Log
import com.tangem.LogFormat import com.tangem.LogFormat
import com.tangem.TangemSdkLogger import com.tangem.TangemSdkLogger
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.datasource.local.logs.AppLogsStore
/** /**
* CardSDK logger implementation * CardSDK logger implementation
* *
* @property levels logging levels * @property levels logging levels
* @property messageFormatter message formatter * @property messageFormatter message formatter
* @property settingsRepository settings repository * @property appLogsStore app logs store
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class TangemCardSDKLogger( internal class TangemCardSDKLogger(
private val levels: List<Log.Level>, private val levels: List<Log.Level>,
private val messageFormatter: LogFormat, private val messageFormatter: LogFormat,
private val settingsRepository: SettingsRepository, private val appLogsStore: AppLogsStore,
) : TangemSdkLogger { ) : TangemSdkLogger {
override fun log(message: () -> String, level: Log.Level) { override fun log(message: () -> String, level: Log.Level) {
if (!levels.contains(level)) return if (!levels.contains(level)) return
settingsRepository.saveLogMessage(message = messageFormatter.format(message, level)) appLogsStore.saveLogMessage(message = messageFormatter.format(message, level))
} }
} }

View file

@ -9,7 +9,7 @@ import timber.log.Timber
val logMiddleware: Middleware<AppState> = { _, _ -> val logMiddleware: Middleware<AppState> = { _, _ ->
{ nextDispatch -> { nextDispatch ->
{ action -> { action ->
Timber.i("Dispatch action: $action") Timber.i("Dispatch action: ${action::class.java.simpleName}")
nextDispatch(action) nextDispatch(action)
} }
} }

View file

@ -1,20 +1,20 @@
package com.tangem.tap.data package com.tangem.tap.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.datasource.local.logs.AppLogsStore
/** /**
* BlockchainSDK logger implementation * BlockchainSDK logger implementation
* *
* @property settingsRepository settings repository * @property appLogsStore app logs store
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class TangemBlockchainSDKLogger( internal class TangemBlockchainSDKLogger(
private val settingsRepository: SettingsRepository, private val appLogsStore: AppLogsStore,
) : BlockchainSDKLogger { ) : BlockchainSDKLogger {
override fun log(level: BlockchainSDKLogger.Level, message: String) { override fun log(level: BlockchainSDKLogger.Level, message: String) {
settingsRepository.saveLogMessage(message) appLogsStore.saveLogMessage(message)
} }
} }

View file

@ -4,7 +4,7 @@ import com.tangem.Log
import com.tangem.LogFormat import com.tangem.LogFormat
import com.tangem.TangemSdkLogger import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.log.TangemAppLoggerInitializer
import com.tangem.tap.common.log.TangemCardSDKLogger import com.tangem.tap.common.log.TangemCardSDKLogger
import com.tangem.tap.data.TangemBlockchainSDKLogger import com.tangem.tap.data.TangemBlockchainSDKLogger
@ -20,13 +20,13 @@ internal object TangemLoggingModule {
@Provides @Provides
@Singleton @Singleton
fun provideAppLoggerInitializer(settingsRepository: SettingsRepository): TangemAppLoggerInitializer { fun provideAppLoggerInitializer(appLogsStore: AppLogsStore): TangemAppLoggerInitializer {
return TangemAppLoggerInitializer(settingsRepository) return TangemAppLoggerInitializer(appLogsStore)
} }
@Provides @Provides
@Singleton @Singleton
fun provideCardSDKLogger(settingsRepository: SettingsRepository): TangemSdkLogger { fun provideCardSDKLogger(appLogsStore: AppLogsStore): TangemSdkLogger {
val logLevels = listOf( val logLevels = listOf(
Log.Level.ApduCommand, Log.Level.ApduCommand,
Log.Level.Apdu, Log.Level.Apdu,
@ -44,13 +44,13 @@ internal object TangemLoggingModule {
return TangemCardSDKLogger( return TangemCardSDKLogger(
levels = logLevels, levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(), messageFormatter = LogFormat.StairsFormatter(),
settingsRepository = settingsRepository, appLogsStore = appLogsStore,
) )
} }
@Provides @Provides
@Singleton @Singleton
fun provideBlockchainSDKLogger(settingsRepository: SettingsRepository): BlockchainSDKLogger { fun provideBlockchainSDKLogger(appLogsStore: AppLogsStore): BlockchainSDKLogger {
return TangemBlockchainSDKLogger(settingsRepository) return TangemBlockchainSDKLogger(appLogsStore)
} }
} }

View file

@ -1,69 +1,109 @@
package com.tangem.datasource.local.logs package com.tangem.datasource.local.logs
import androidx.datastore.preferences.core.MutablePreferences import android.content.Context
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.joda.time.DateTime import org.joda.time.DateTime
import org.joda.time.format.DateTimeFormatterBuilder
import timber.log.Timber
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
/** /**
* Store for saving app logs * Store for saving app logs
* *
* @property appPreferencesStore app preferences store * @property applicationContext application context
* @param dispatchers coroutine dispatcher provider * @param dispatchers coroutine dispatcher provider
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
@Singleton
class AppLogsStore @Inject constructor( class AppLogsStore @Inject constructor(
private val appPreferencesStore: AppPreferencesStore, @ApplicationContext private val applicationContext: Context,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
) { ) {
private val scope = CoroutineScope(dispatchers.io) private val scope = CoroutineScope(dispatchers.io)
private val mutex = Mutex() private val mutex = Mutex()
private val file = File(applicationContext.filesDir, LOG_FILE_NAME)
private val formatter = DateTimeFormatterBuilder()
.appendDayOfMonth(2)
.appendLiteral('.')
.appendMonthOfYear(2)
.appendLiteral(' ')
.appendHourOfDay(1)
.appendLiteral(':')
.appendMinuteOfHour(2)
.appendLiteral(':')
.appendSecondOfMinute(2)
.appendLiteral('.')
.appendMillisOfSecond(3)
.toFormatter()
/** Get log file */
fun getFile(): File? = if (file.exists()) file else null
/** Save log [message] */ /** Save log [message] */
fun saveLogMessage(message: String) { fun saveLogMessage(message: String) {
val newLogs = DateTime.now().millis.toString() to message launchWithLock {
createFileIfNotExist()
appPreferencesStore.editDataWithLock { preferences -> writeMessage(message)
val savedLogs = preferences.getObjectMap<String>(PreferencesKeys.APP_LOGS_KEY) }
}
preferences.setObjectMap(key = PreferencesKeys.APP_LOGS_KEY, value = savedLogs + newLogs) /** Save log that consists from [messages] */
fun saveLogMessage(vararg messages: String) {
launchWithLock {
createFileIfNotExist()
writeMessage(*messages)
} }
} }
/** Delete deprecated logs if file size exceeds [maxSize] */ /** Delete deprecated logs if file size exceeds [maxSize] */
fun deleteDeprecatedLogs(maxSize: Int) { fun deleteDeprecatedLogs(maxSize: Int) {
appPreferencesStore.editDataWithLock { preferences -> launchWithLock {
val savedLogs = preferences.getObjectMap<String>(PreferencesKeys.APP_LOGS_KEY) if (file.exists() && file.length() > maxSize) {
file.delete()
var sum = 0
preferences.setObjectMap(
key = PreferencesKeys.APP_LOGS_KEY,
value = savedLogs.entries
.sortedBy(Map.Entry<String, String>::key)
.takeLastWhile {
sum += it.value.length
sum < maxSize
}
.associate { it.key to it.value },
)
}
}
private fun AppPreferencesStore.editDataWithLock(
transform: suspend AppPreferencesStore.(MutablePreferences) -> Unit,
) {
scope.launch {
mutex.withLock {
editData(transform)
} }
} }
} }
private fun writeMessage(vararg messages: String) {
BufferedWriter(FileWriter(file, true)).use { writer ->
writer.append(formatter.print(DateTime.now()))
writer.append(": ")
messages.forEach(writer::append)
writer.newLine()
}
}
private fun createFileIfNotExist() {
if (!file.exists()) {
runCatching { file.createNewFile() }
.onFailure(Timber::e)
}
}
private fun launchWithLock(callback: () -> Unit) {
scope.launch {
mutex.withLock {
callback()
}
}
}
private companion object {
const val LOG_FILE_NAME = "logs.txt"
}
} }

View file

@ -54,8 +54,8 @@ internal class NetworkLogsSaveInterceptor(
val connectionProtocol = if (connection != null) " ${connection.protocol()}" else "" val connectionProtocol = if (connection != null) " ${connection.protocol()}" else ""
appLogsStore.saveLogMessage( appLogsStore.saveLogMessage(
"--> ${request.method} ${request.url}$connectionProtocol\n" + "--> ${request.method} ${request.url}$connectionProtocol\n",
createRequestEndMessage(request), createRequestEndMessage(request),
) )
} }
@ -88,12 +88,6 @@ internal class NetworkLogsSaveInterceptor(
} }
private fun logResponseMessage(response: Response, startNs: Long) { private fun logResponseMessage(response: Response, startNs: Long) {
val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val responseMessage = if (response.message.isEmpty()) "" else ' ' + response.message
val startMessage = "<-- ${response.code}$responseMessage ${response.request.url} " +
"(${tookMs}ms)"
val responseHeaders = response.headers val responseHeaders = response.headers
val responseBody = response.body!! val responseBody = response.body!!
val contentLength = responseBody.contentLength() val contentLength = responseBody.contentLength()
@ -138,7 +132,17 @@ internal class NetworkLogsSaveInterceptor(
} }
} }
appLogsStore.saveLogMessage(startMessage + "\n" + message) val tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs)
val spaceBeforeResponseMessage = if (response.message.isEmpty()) "" else ' ' + response.message
appLogsStore.saveLogMessage(
"<-- ${response.code}",
spaceBeforeResponseMessage,
response.message,
" ${response.request.url} (${tookMs}ms)\n",
message,
)
} }
private fun bodyHasUnknownEncoding(headers: Headers): Boolean { private fun bodyHasUnknownEncoding(headers: Headers): Boolean {

View file

@ -5,7 +5,7 @@
}, },
{ {
"name": "LOCAL_USER_LOGS_ENABLED", "name": "LOCAL_USER_LOGS_ENABLED",
"version": "undefined" "version": "5.14.0"
}, },
{ {
"name": "GENERATE_XPUB_ENABLED", "name": "GENERATE_XPUB_ENABLED",

View file

@ -6,48 +6,43 @@ import android.os.Build
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.data.feedback.converters.BlockchainInfoConverter import com.tangem.data.feedback.converters.BlockchainInfoConverter
import com.tangem.data.feedback.converters.CardInfoConverter import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.models.BlockchainErrorInfo
import com.tangem.domain.feedback.models.BlockchainInfo
import com.tangem.domain.feedback.models.PhoneInfo
import com.tangem.domain.feedback.models.UserWalletsInfo
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/** /**
* Implementation of [FeedbackRepository] * Implementation of [FeedbackRepository]
* *
* @property appPreferencesStore application preferences store * @property appLogsStore app logs store
* @property userWalletsListManager user wallets list manager * @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store * @property walletManagersStore wallet managers store
* @property context context for getting app version * @property context context for getting app version
* @property dispatchers coroutine dispatchers provider
* *
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
*/ */
internal class DefaultFeedbackRepository( internal class DefaultFeedbackRepository(
private val appPreferencesStore: AppPreferencesStore, private val appLogsStore: AppLogsStore,
private val userWalletsListManager: UserWalletsListManager, private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore, private val walletManagersStore: WalletManagersStore,
private val context: Context, private val context: Context,
private val dispatchers: CoroutineDispatcherProvider,
) : FeedbackRepository { ) : FeedbackRepository {
private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap()) private val blockchainsErrors = MutableStateFlow<Map<UserWalletId, BlockchainErrorInfo>>(emptyMap())
override suspend fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse) override fun getCardInfo(scanResponse: ScanResponse) = CardInfoConverter.convert(value = scanResponse)
override suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo { override fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo {
return UserWalletsInfo( return UserWalletsInfo(
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated", selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
totalUserWallets = userWalletsListManager.walletsCount, totalUserWallets = userWalletsListManager.walletsCount,
@ -92,38 +87,13 @@ internal class DefaultFeedbackRepository(
} }
} }
override suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? { override fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? {
return blockchainsErrors.value[userWalletId].also { return blockchainsErrors.value[userWalletId].also {
if (it == null) Timber.e("Blockchain error info is null for $userWalletId") if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
} }
} }
override suspend fun getAppLogs(): List<AppLogModel> { override fun getLogFile(): File? = appLogsStore.getFile()
return appPreferencesStore.getObjectMapSync<String>(key = PreferencesKeys.APP_LOGS_KEY)
.map { AppLogModel(timestamp = it.key.toLong(), message = it.value) }
.sortedBy(AppLogModel::timestamp)
}
override suspend fun createLogFile(logs: String): File? {
return runCatching(dispatchers.io) {
val file = File(context.filesDir, LOGS_FILE)
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
stringWriter.append(logs)
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
file
}.getOrElse {
Timber.e(it, "Logs file isn't created")
null
}
}
private fun getAppVersion(): String { private fun getAppVersion(): String {
return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) } return runCatching { context.packageManager.getPackageInfo(context.packageName, 0) }
@ -135,8 +105,4 @@ internal class DefaultFeedbackRepository(
}, },
) )
} }
private companion object {
const val LOGS_FILE = "logs.txt"
}
} }

View file

@ -2,11 +2,10 @@ package com.tangem.data.feedback.di
import android.content.Context import android.content.Context
import com.tangem.data.feedback.DefaultFeedbackRepository import com.tangem.data.feedback.DefaultFeedbackRepository
import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.logs.AppLogsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.repository.FeedbackRepository import com.tangem.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -21,18 +20,16 @@ internal object FeedbackRepositoryModule {
@Provides @Provides
@Singleton @Singleton
fun provideFeedbackRepository( fun provideFeedbackRepository(
appPreferencesStore: AppPreferencesStore, appLogsStore: AppLogsStore,
userWalletsListManager: UserWalletsListManager, userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore, walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context, @ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): FeedbackRepository { ): FeedbackRepository {
return DefaultFeedbackRepository( return DefaultFeedbackRepository(
appPreferencesStore = appPreferencesStore, appLogsStore = appLogsStore,
userWalletsListManager = userWalletsListManager, userWalletsListManager = userWalletsListManager,
walletManagersStore = walletManagersStore, walletManagersStore = walletManagersStore,
context = context, context = context,
dispatchers = dispatchers,
) )
} }
} }

View file

@ -37,10 +37,6 @@ internal class DefaultSettingsRepository(
) )
} }
override fun saveLogMessage(message: String) {
appLogsStore.saveLogMessage(message)
}
override fun deleteDeprecatedLogs(maxSize: Int) { override fun deleteDeprecatedLogs(maxSize: Int) {
appLogsStore.deleteDeprecatedLogs(maxSize) appLogsStore.deleteDeprecatedLogs(maxSize)
} }

View file

@ -25,13 +25,11 @@ class GetFeedbackEmailUseCase(
private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository) private val emailMessageBodyResolver = EmailMessageBodyResolver(feedbackRepository)
suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail { suspend operator fun invoke(type: FeedbackEmailType): FeedbackEmail {
val formattedLogs = AppLogsFormatter().format(appLogs = feedbackRepository.getAppLogs())
return FeedbackEmail( return FeedbackEmail(
address = getAddress(type.cardInfo), address = getAddress(type.cardInfo),
subject = emailSubjectResolver.resolve(type), subject = emailSubjectResolver.resolve(type),
message = createMessage(type), message = createMessage(type),
file = feedbackRepository.createLogFile(logs = formattedLogs), file = feedbackRepository.getLogFile(),
) )
} }

View file

@ -7,9 +7,9 @@ import java.io.File
interface FeedbackRepository { interface FeedbackRepository {
suspend fun getCardInfo(scanResponse: ScanResponse): CardInfo fun getCardInfo(scanResponse: ScanResponse): CardInfo
suspend fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo fun getUserWalletsInfo(userWalletId: UserWalletId?): UserWalletsInfo
suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo> suspend fun getBlockchainInfoList(userWalletId: UserWalletId): List<BlockchainInfo>
@ -23,9 +23,7 @@ interface FeedbackRepository {
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo? fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo?
suspend fun getAppLogs(): List<AppLogModel> fun getLogFile(): File?
suspend fun createLogFile(logs: String): File?
} }

View file

@ -10,8 +10,6 @@ interface SettingsRepository {
suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean) suspend fun setWalletScrollPreviewAvailability(isEnabled: Boolean)
fun saveLogMessage(message: String)
fun deleteDeprecatedLogs(maxSize: Int) fun deleteDeprecatedLogs(maxSize: Int)
suspend fun isSendTapHelpPreviewEnabled(): Boolean suspend fun isSendTapHelpPreviewEnabled(): Boolean