Skip to content

Commit

Permalink
Merge pull request #8 from tberchanov/highlight_details_stacktrace
Browse files Browse the repository at this point in the history
Highlight stacktrace items on detail screen.
  • Loading branch information
tberchanov authored Dec 18, 2024
2 parents af542bf + 355c8b0 commit d06bd1a
Show file tree
Hide file tree
Showing 10 changed files with 192 additions and 131 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Setup CI/CD with GitHub Actions.
- Run `check-changelog` only on `pull_request`.
- Created `strictpro-stubs`, `strictpro-ui-stubs`.
- Highlight stacktrace items on detail screen.

### Fixed

Expand Down
9 changes: 5 additions & 4 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ dependencies {

debugImplementation(project(":strictpro"))
releaseImplementation(project(":strictpro-stubs"))

debugImplementation(project(":strictpro-ui"))
releaseImplementation(project(":strictpro-ui-stubs"))

// val libVersion = "0.1.0-preview.1"
// implementation("com.github.tberchanov.StrictPro:strictpro:$libVersion")
// implementation("com.github.tberchanov.StrictPro:strictpro.ui:$libVersion")
// val libVersion = "0.1.0-preview.2"
// debugImplementation("com.github.tberchanov.StrictPro:strictpro:$libVersion")
// debugImplementation("com.github.tberchanov.StrictPro:strictpro.ui:$libVersion")
// releaseImplementation("com.github.tberchanov.StrictPro:strictpro.stubs:$libVersion")
// releaseImplementation("com.github.tberchanov.StrictPro:strictpro.ui.stubs:$libVersion")

testImplementation(libs.junit)
androidTestImplementation(libs.mockk)
Expand Down
4 changes: 3 additions & 1 deletion strictpro-ui/src/main/java/com/strictpro/ui/di/Modules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.datastore.preferences.preferencesDataStore
import com.strictpro.ui.data.LocalViolationDataSource
import com.strictpro.ui.data.ViolationRepositoryImpl
import com.strictpro.ui.domain.ViolationRepository
import com.strictpro.ui.domain.usecase.FilterStackTraceUseCase
import com.strictpro.ui.domain.usecase.GetAppPackageNameUseCase
import com.strictpro.ui.domain.usecase.GetViolationUseCase
import com.strictpro.ui.domain.usecase.GetViolationsQuantityUseCase
Expand All @@ -32,12 +33,13 @@ internal val domainModule = module {
factory { GetViolationsUseCase(get()) }
factory { GetAppPackageNameUseCase(androidContext()) }
factory { GetViolationUseCase(get()) }
factory { FilterStackTraceUseCase(get()) }
}

internal val viewModelModule = module {
viewModel { ViolationsViewModel(get(), get()) }
viewModel { ViolationsHistoryViewModel(get(), get(), get()) }
viewModel { ViolationDetailsViewModel(get()) }
viewModel { ViolationDetailsViewModel(get(), get()) }
}

internal val presentationModule = module {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.strictpro.ui.domain.usecase

internal class FilterStackTraceUseCase(
private val getAppPackageNameUseCase: GetAppPackageNameUseCase,
) {

private val packageName by lazy { getAppPackageNameUseCase.execute() }

fun execute(stackTrace: Array<StackTraceElement>): List<StackTraceElement> {
return stackTrace.filter {
it.className.startsWith(packageName)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.strictpro.ui.presentation.ui.common

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.FontWeight.Companion
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

@Composable
internal fun StackTraceItems(
stackTraceItems: List<String>,
modifier: Modifier = Modifier,
cutLast: Boolean = false,
highlightPredicate: (String) -> Boolean = { false },
) {
val localDensity = LocalDensity.current
Column(modifier = modifier) {
stackTraceItems.forEach { item ->
var columnHeightDp by remember {
mutableStateOf(0.dp)
}
Row(
modifier = Modifier
.onGloballyPositioned { coordinates ->
columnHeightDp = with(localDensity) { coordinates.size.height.toDp() }
}
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
StackTraceItemMarker(
cutBottom = cutLast && stackTraceItems.last() == item,
modifier = Modifier
.size(width = 30.dp, height = columnHeightDp)
.padding(end = 8.dp),
)
val isHighlighted = remember(item) { highlightPredicate(item) }
Text(
modifier = Modifier.padding(vertical = 4.dp),
fontWeight = if (isHighlighted) FontWeight.Bold else Companion.Normal,
text = item,
color = Color.White,
fontSize = if (isHighlighted) 14.sp else 12.sp,
lineHeight = 14.sp
)
}
}
}
}

@Preview
@Composable
private fun StackTraceItemsPreview() {
StackTraceItems(
cutLast = true,
stackTraceItems = listOf(
"violation1",
"violation2",
"violation3",
"violation4",
),
highlightPredicate = { it == "violation2" },
)
}

@Preview
@Composable
private fun StackTraceItemMarkerPreview() {
StackTraceItemMarker(modifier = Modifier.size(240.dp), cutBottom = true)
}

@Composable
internal fun StackTraceItemMarker(
cutBottom: Boolean = false,
modifier: Modifier = Modifier,
color: Color = Color.Red,
thickness: Dp = 2.dp,
) {
val cutBottomCoefficient = if (cutBottom) 2 else 1
Canvas(modifier = modifier) {
drawLine(
color = color,
start = Offset(thickness.toPx(), 0F),
end = Offset(thickness.toPx(), size.height / cutBottomCoefficient),
strokeWidth = thickness.toPx(),
)
drawLine(
color = color,
start = Offset(thickness.toPx(), size.height / 2),
end = Offset(size.width, size.height / 2),
strokeWidth = thickness.toPx(),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.strictpro.ui.presentation.violations.details.ui

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
Expand All @@ -18,10 +19,10 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.strictpro.ui.R
import com.strictpro.ui.presentation.ui.common.BackButton
import com.strictpro.ui.presentation.ui.common.StackTraceItems
import com.strictpro.ui.presentation.ui.theme.DarkGray
import com.strictpro.ui.presentation.violations.details.viewmodel.ViolationDetailsState
import com.strictpro.ui.presentation.violations.details.viewmodel.ViolationDetailsViewModel
import com.strictpro.ui.presentation.violations.history.ui.StackTraceItems
import org.koin.androidx.compose.koinViewModel

@Composable
Expand Down Expand Up @@ -73,9 +74,11 @@ internal fun ViolationDetailsScreenContent(
StackTraceItems(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 4.dp),
.padding(horizontal = 4.dp)
.padding(bottom = 18.dp),
stackTraceItems = violationDetailsState.trace,
cutLast = true,
highlightPredicate = { violationDetailsState.highlightedTrace.contains(it) },
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.strictpro.ui.domain.model.StrictProViolation
import com.strictpro.ui.domain.model.getViolationName
import com.strictpro.ui.domain.usecase.FilterStackTraceUseCase
import com.strictpro.ui.domain.usecase.GetViolationUseCase
import com.strictpro.ui.presentation.util.snackbar.snackbarCoroutineExceptionHandler
import com.strictpro.utils.Base64Util
Expand All @@ -17,6 +18,7 @@ import kotlinx.coroutines.launch

internal class ViolationDetailsViewModel(
private val getViolationUseCase: GetViolationUseCase,
private val filterStackTraceUseCase: FilterStackTraceUseCase,
) : ViewModel() {

private val _state = MutableStateFlow(ViolationDetailsState())
Expand All @@ -31,6 +33,7 @@ internal class ViolationDetailsViewModel(
_state.value = ViolationDetailsState(
title = violation.getViolationName(),
trace = violation.getTraceString(),
highlightedTrace = defineHighlightedTrace(violation),
strictProViolation = violation,
)
}
Expand All @@ -54,18 +57,25 @@ internal class ViolationDetailsViewModel(
}
}

private fun StrictProViolation.getTraceString(): List<String> {
return if (VERSION.SDK_INT >= VERSION_CODES.P) {
violation.stackTrace
.map { it.toString() }
} else {
emptyList()
}
private fun defineHighlightedTrace(violation: StrictProViolation): Set<String> {
return filterStackTraceUseCase.execute(violation.violation.stackTrace)
.map { it.toString() }
.toSet()
}
}

private fun StrictProViolation.getTraceString(): List<String> {
return if (VERSION.SDK_INT >= VERSION_CODES.P) {
violation.stackTrace
.map { it.toString() }
} else {
emptyList()
}
}

internal data class ViolationDetailsState(
val title: String = "",
val trace: List<String> = emptyList(),
val highlightedTrace: Set<String> = emptySet(),
val strictProViolation: StrictProViolation? = null,
)
Original file line number Diff line number Diff line change
@@ -1,38 +1,9 @@
package com.strictpro.ui.presentation.violations.history.model

import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.strictmode.Violation
import com.strictpro.ui.domain.model.StrictProViolation
import com.strictpro.ui.domain.model.getViolationName
import com.strictpro.ui.presentation.violations.history.util.formatViolationDate

data class ViolationHistoryItemUI(
val dateMillis: Long,
val formattedDate: String,
val violationName: String,
val filteredStackTraceItems: List<String>,
val violationId: String,
)

fun StrictProViolation.toViolationHistoryItemUI(packageName: String): ViolationHistoryItemUI {
val formattedDate = formatViolationDate(dateMillis)
val filteredStackTraceItems = if (VERSION.SDK_INT >= VERSION_CODES.P) {
violation.stackTrace
.filter {
it.className.startsWith(packageName)
}
.take(3)
.map { it.toString() }
} else {
emptyList()
}

return ViolationHistoryItemUI(
dateMillis = dateMillis,
formattedDate = formattedDate,
violationName = getViolationName(),
filteredStackTraceItems = filteredStackTraceItems,
violationId = id,
)
}
Loading

0 comments on commit d06bd1a

Please sign in to comment.