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 com.orhanobut.logger.AndroidLogAdapter
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 timber.log.Timber
/**
* Tangem app logger
*
* @property settingsRepository repository for saving logs
* @property appLogsStore app logs store
*
[REDACTED_AUTHOR]
*/
class TangemAppLoggerInitializer(
private val settingsRepository: SettingsRepository,
private val appLogsStore: AppLogsStore,
) {
/** Initialize */
@ -35,7 +35,7 @@ class TangemAppLoggerInitializer(
}
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.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.datasource.local.logs.AppLogsStore
/**
* CardSDK logger implementation
*
* @property levels logging levels
* @property messageFormatter message formatter
* @property settingsRepository settings repository
* @property levels logging levels
* @property messageFormatter message formatter
* @property appLogsStore app logs store
*
[REDACTED_AUTHOR]
*/
internal class TangemCardSDKLogger(
private val levels: List<Log.Level>,
private val messageFormatter: LogFormat,
private val settingsRepository: SettingsRepository,
private val appLogsStore: AppLogsStore,
) : TangemSdkLogger {
override fun log(message: () -> String, level: Log.Level) {
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> = { _, _ ->
{ nextDispatch ->
{ action ->
Timber.i("Dispatch action: $action")
Timber.i("Dispatch action: ${action::class.java.simpleName}")
nextDispatch(action)
}
}

View file

@ -1,20 +1,20 @@
package com.tangem.tap.data
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.datasource.local.logs.AppLogsStore
/**
* BlockchainSDK logger implementation
*
* @property settingsRepository settings repository
* @property appLogsStore app logs store
*
[REDACTED_AUTHOR]
*/
internal class TangemBlockchainSDKLogger(
private val settingsRepository: SettingsRepository,
private val appLogsStore: AppLogsStore,
) : BlockchainSDKLogger {
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.TangemSdkLogger
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.TangemCardSDKLogger
import com.tangem.tap.data.TangemBlockchainSDKLogger
@ -20,13 +20,13 @@ internal object TangemLoggingModule {
@Provides
@Singleton
fun provideAppLoggerInitializer(settingsRepository: SettingsRepository): TangemAppLoggerInitializer {
return TangemAppLoggerInitializer(settingsRepository)
fun provideAppLoggerInitializer(appLogsStore: AppLogsStore): TangemAppLoggerInitializer {
return TangemAppLoggerInitializer(appLogsStore)
}
@Provides
@Singleton
fun provideCardSDKLogger(settingsRepository: SettingsRepository): TangemSdkLogger {
fun provideCardSDKLogger(appLogsStore: AppLogsStore): TangemSdkLogger {
val logLevels = listOf(
Log.Level.ApduCommand,
Log.Level.Apdu,
@ -44,13 +44,13 @@ internal object TangemLoggingModule {
return TangemCardSDKLogger(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter(),
settingsRepository = settingsRepository,
appLogsStore = appLogsStore,
)
}
@Provides
@Singleton
fun provideBlockchainSDKLogger(settingsRepository: SettingsRepository): BlockchainSDKLogger {
return TangemBlockchainSDKLogger(settingsRepository)
fun provideBlockchainSDKLogger(appLogsStore: AppLogsStore): BlockchainSDKLogger {
return TangemBlockchainSDKLogger(appLogsStore)
}
}

View file

@ -1,69 +1,109 @@
package com.tangem.datasource.local.logs
import androidx.datastore.preferences.core.MutablePreferences
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import android.content.Context
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
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.Singleton
/**
* Store for saving app logs
*
* @property appPreferencesStore app preferences store
* @param dispatchers coroutine dispatcher provider
* @property applicationContext application context
* @param dispatchers coroutine dispatcher provider
*
[REDACTED_AUTHOR]
*/
@Singleton
class AppLogsStore @Inject constructor(
private val appPreferencesStore: AppPreferencesStore,
@ApplicationContext private val applicationContext: Context,
dispatchers: CoroutineDispatcherProvider,
) {
private val scope = CoroutineScope(dispatchers.io)
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] */
fun saveLogMessage(message: String) {
val newLogs = DateTime.now().millis.toString() to message
launchWithLock {
createFileIfNotExist()
appPreferencesStore.editDataWithLock { preferences ->
val savedLogs = preferences.getObjectMap<String>(PreferencesKeys.APP_LOGS_KEY)
writeMessage(message)
}
}
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] */
fun deleteDeprecatedLogs(maxSize: Int) {
appPreferencesStore.editDataWithLock { preferences ->
val savedLogs = preferences.getObjectMap<String>(PreferencesKeys.APP_LOGS_KEY)
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)
launchWithLock {
if (file.exists() && file.length() > maxSize) {
file.delete()
}
}
}
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 ""
appLogsStore.saveLogMessage(
"--> ${request.method} ${request.url}$connectionProtocol\n" +
createRequestEndMessage(request),
"--> ${request.method} ${request.url}$connectionProtocol\n",
createRequestEndMessage(request),
)
}
@ -88,12 +88,6 @@ internal class NetworkLogsSaveInterceptor(
}
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 responseBody = response.body!!
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 {

View file

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

View file

@ -6,48 +6,43 @@ import android.os.Build
import com.tangem.blockchain.common.Blockchain
import com.tangem.data.feedback.converters.BlockchainInfoConverter
import com.tangem.data.feedback.converters.CardInfoConverter
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
import com.tangem.datasource.local.logs.AppLogsStore
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.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
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.update
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
/**
* Implementation of [FeedbackRepository]
*
* @property appPreferencesStore application preferences store
* @property appLogsStore app logs store
* @property userWalletsListManager user wallets list manager
* @property walletManagersStore wallet managers store
* @property context context for getting app version
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultFeedbackRepository(
private val appPreferencesStore: AppPreferencesStore,
private val appLogsStore: AppLogsStore,
private val userWalletsListManager: UserWalletsListManager,
private val walletManagersStore: WalletManagersStore,
private val context: Context,
private val dispatchers: CoroutineDispatcherProvider,
) : FeedbackRepository {
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(
selectedUserWalletId = userWalletId?.stringValue ?: "card isn't activated",
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 {
if (it == null) Timber.e("Blockchain error info is null for $userWalletId")
}
}
override suspend fun getAppLogs(): List<AppLogModel> {
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
}
}
override fun getLogFile(): File? = appLogsStore.getFile()
private fun getAppVersion(): String {
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 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.domain.feedback.repository.FeedbackRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -21,18 +20,16 @@ internal object FeedbackRepositoryModule {
@Provides
@Singleton
fun provideFeedbackRepository(
appPreferencesStore: AppPreferencesStore,
appLogsStore: AppLogsStore,
userWalletsListManager: UserWalletsListManager,
walletManagersStore: WalletManagersStore,
@ApplicationContext context: Context,
dispatchers: CoroutineDispatcherProvider,
): FeedbackRepository {
return DefaultFeedbackRepository(
appPreferencesStore = appPreferencesStore,
appLogsStore = appLogsStore,
userWalletsListManager = userWalletsListManager,
walletManagersStore = walletManagersStore,
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) {
appLogsStore.deleteDeprecatedLogs(maxSize)
}

View file

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

View file

@ -7,9 +7,9 @@ import java.io.File
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>
@ -23,9 +23,7 @@ interface FeedbackRepository {
fun saveBlockchainErrorInfo(error: BlockchainErrorInfo)
suspend fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo?
fun getBlockchainErrorInfo(userWalletId: UserWalletId): BlockchainErrorInfo?
suspend fun getAppLogs(): List<AppLogModel>
suspend fun createLogFile(logs: String): File?
fun getLogFile(): File?
}

View file

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