diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c447dae..70a5305 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,6 +53,8 @@ jobs: needs: lint steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up JDK 21 uses: actions/setup-java@v4.2.2 @@ -78,6 +80,9 @@ jobs: KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} run: mkdir -p keystore_details && echo "$KEYSTORE_BASE64" | base64 -d > keystore_details/sensor.jks + - name: Fetch tags + run: git fetch --tags origin + - name: Bump version id: bump run: | @@ -102,6 +107,21 @@ jobs: ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} run: ./gradlew bundleRelease + - name: Generate release notes + id: notes + run: | + prev_tag=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$prev_tag" ]; then + notes=$(git log "$prev_tag"..HEAD --oneline --no-merges --format="- %s" 2>/dev/null || true) + else + notes=$(git log --oneline --no-merges --format="- %s" -20 2>/dev/null || true) + fi + if [ -z "$notes" ]; then + notes="- Bug fixes and improvements" + fi + echo "$notes" > /tmp/release_notes.txt + echo "notes_file=/tmp/release_notes.txt" >> "$GITHUB_OUTPUT" + - name: Upload to Google Play uses: r0adkll/upload-google-play@v1.1.5 with: @@ -111,6 +131,16 @@ jobs: tracks: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.track || 'alpha' }} status: completed inAppUpdatePriority: 2 + releaseNotesFile: /tmp/release_notes.txt + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "v${{ steps.bump.outputs.new_version_name }}" \ + --title "v${{ steps.bump.outputs.new_version_name }}" \ + --notes-file /tmp/release_notes.txt \ + app/build/outputs/bundle/release/app-release.aab - name: Commit version bump run: | diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/compass/CompassScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/compass/CompassScreen.kt index 654ca26..f47d1dd 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/compass/CompassScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/compass/CompassScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.CenterAlignedTopAppBar @@ -76,6 +77,7 @@ fun CompassScreen( Scaffold( topBar = { CenterAlignedTopAppBar( + modifier = Modifier.statusBarsPadding(), title = { Text("Compass") }, navigationIcon = { IconButton(onClick = onBack) { diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt index 44c58cf..2d02fea 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/dashboard/DashboardScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan @@ -50,15 +51,20 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import com.shreyash.sensorapp.R import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shreyash.sensorapp.domain.model.SensorAvailability import com.shreyash.sensorapp.domain.model.SensorCategory import com.shreyash.sensorapp.domain.model.SensorState import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.detail.sensorCategoryName +import com.shreyash.sensorapp.presentation.detail.sensorDescription +import com.shreyash.sensorapp.presentation.detail.sensorDisplayName import com.shreyash.sensorapp.presentation.permission.PermissionDialog import com.shreyash.sensorapp.presentation.permission.rememberPermissionHandler import com.shreyash.sensorapp.presentation.sensorIcon @@ -80,6 +86,7 @@ fun DashboardScreen( Scaffold( topBar = { CenterAlignedTopAppBar( + modifier = Modifier.statusBarsPadding(), title = { Text( "Sensors", @@ -189,7 +196,7 @@ private fun CategoryHeader(category: SensorCategory) { ) Spacer(Modifier.height(12.dp)) Text( - text = category.displayName.uppercase(), + text = sensorCategoryName(category).uppercase(), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), @@ -246,9 +253,9 @@ fun SensorRowItem( ) } Spacer(Modifier.width(14.dp)) - Column(Modifier.weight(1f)) { + Column(Modifier.weight(1f)) { Text( - text = state.type.displayName, + text = sensorDisplayName(state.type), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = if (isAvailable) MaterialTheme.colorScheme.onSurface @@ -257,7 +264,7 @@ fun SensorRowItem( if (!isAvailable) { Spacer(Modifier.height(2.dp)) Text( - text = "Unavailable", + text = stringResource(R.string.unavailable), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) ) @@ -349,7 +356,7 @@ fun SensorGridItem( } Spacer(Modifier.height(10.dp)) Text( - text = state.type.displayName, + text = sensorDisplayName(state.type), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, @@ -366,7 +373,7 @@ fun SensorGridItem( .padding(horizontal = 8.dp, vertical = 2.dp) ) { Text( - text = "Unavailable", + text = stringResource(R.string.unavailable), style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) ) @@ -392,14 +399,13 @@ fun SensorGridItem( ) Spacer(Modifier.height(16.dp)) Text( - text = state.type.displayName, + text = sensorDisplayName(state.type), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) Spacer(Modifier.height(12.dp)) Text( - text = "Your device does not have a ${state.type.displayName}." + - "\n\n${state.type.description}", + text = stringResource(R.string.sensor_missing, sensorDisplayName(state.type), sensorDescription(state.type)), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/AccelerometerScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/AccelerometerScreen.kt index f0ba780..66427b6 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/AccelerometerScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/AccelerometerScreen.kt @@ -1,22 +1,39 @@ package com.shreyash.sensorapp.presentation.detail +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme +import com.shreyash.sensorapp.R + +private val AxisRed = Color(0xFFE24B4A) +private val AxisGreen = Color(0xFF639922) +private val AxisBlue = Color(0xFF378ADD) @Composable fun AccelerometerScreen( @@ -37,41 +54,154 @@ private fun AccelerometerContent( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(16.dp) ) { - LiveIndicator() + Text( + text = sensorCategoryName(SensorType.ACCELEROMETER.category), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = sensorDisplayName(SensorType.ACCELEROMETER), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) - LiveValueDisplay( - reading = currentReading, - sensorType = SensorType.ACCELEROMETER - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + AxisValueCard( + label = "X", + value = currentReading?.values?.getOrNull(0), + unit = if (SensorType.ACCELEROMETER.unitX.isNotEmpty()) sensorUnitText(SensorType.ACCELEROMETER.unitX) else "", + labelColor = AxisRed, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Y", + value = currentReading?.values?.getOrNull(1), + unit = if (SensorType.ACCELEROMETER.unitY.isNotEmpty()) sensorUnitText(SensorType.ACCELEROMETER.unitY) else "", + labelColor = AxisGreen, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Z", + value = currentReading?.values?.getOrNull(2), + unit = if (SensorType.ACCELEROMETER.unitZ.isNotEmpty()) sensorUnitText(SensorType.ACCELEROMETER.unitZ) else "", + labelColor = AxisBlue, + modifier = Modifier.weight(1f) + ) + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(20.dp)) - LiveLineChart( - readings = chartReadings, - sensorType = SensorType.ACCELEROMETER, - modifier = Modifier.fillMaxWidth().height(260.dp) - ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Live waveform", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "50 Hz", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + MultiAxisLineChart( + readings = chartReadings, + modifier = Modifier.fillMaxWidth().height(200.dp) + ) + } + } Spacer(Modifier.height(12.dp)) - Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.ACCELEROMETER.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.ACCELEROMETER.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.ACCELEROMETER.unitZ}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + SensorUsageHint(sensorType = SensorType.ACCELEROMETER) Spacer(Modifier.height(16.dp)) + } +} - SensorUsageHint(sensorType = SensorType.ACCELEROMETER) +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewAccelerometerScreen() { + SensorAppTheme { + AccelerometerContent( + currentReading = SensorReading( + sensorType = SensorType.ACCELEROMETER, + values = listOf(0.02f, 0.15f, 9.81f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} - Spacer(Modifier.height(16.dp)) +@Composable +private fun AxisValueCard( + label: String, + value: Float?, + unit: String, + labelColor: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(labelColor.copy(alpha = 0.15f)) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = labelColor + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = formatDetailValue(value), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + if (unit.isNotEmpty()) { + Text( + text = unit, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } } } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GravityScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GravityScreen.kt index 395eed5..cb96afb 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GravityScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GravityScreen.kt @@ -1,22 +1,47 @@ package com.shreyash.sensorapp.presentation.detail +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme +import kotlin.math.abs +import kotlin.math.asin +import kotlin.math.roundToInt +import kotlin.math.sqrt +import kotlin.math.PI + +private val AxisRed = Color(0xFFE24B4A) +private val AxisGreen = Color(0xFF639922) +private val AxisBlue = Color(0xFF378ADD) @Composable fun GravityScreen( @@ -37,41 +62,281 @@ private fun GravityContent( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(16.dp) ) { - LiveIndicator() + Text( + text = "Tilt", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = sensorDisplayName(SensorType.GRAVITY), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) Spacer(Modifier.height(16.dp)) - LiveValueDisplay( - reading = currentReading, - sensorType = SensorType.GRAVITY - ) + val gx = currentReading?.values?.getOrNull(0) ?: 0f + val gy = currentReading?.values?.getOrNull(1) ?: 0f + val gz = currentReading?.values?.getOrNull(2) ?: 9.81f + val magnitude = sqrt(gx * gx + gy * gy + gz * gz) - Spacer(Modifier.height(24.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BubbleLevel( + gx = gx, + gy = gy, + magnitude = magnitude, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(8.dp) + ) - LiveLineChart( - readings = chartReadings, - sensorType = SensorType.GRAVITY, - modifier = Modifier.fillMaxWidth().height(260.dp) - ) + Spacer(Modifier.height(8.dp)) - Spacer(Modifier.height(12.dp)) + val tiltRight = asin((gx / magnitude).coerceIn(-1f, 1f)) * 180f / PI + val tiltForward = asin((gy / magnitude).coerceIn(-1f, 1f)) * 180f / PI - Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.GRAVITY.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.GRAVITY.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.GRAVITY.unitZ}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + val dirX = when { + abs(tiltRight) < 2f -> "" + tiltRight > 0 -> "right" + else -> "left" + } + val dirY = when { + abs(tiltForward) < 2f -> "" + tiltForward > 0 -> "forward" + else -> "backward" + } + val tiltText = buildString { + val parts = mutableListOf() + if (dirX.isNotEmpty()) parts.add("${abs(tiltRight).roundToInt()}° $dirX") + if (dirY.isNotEmpty()) parts.add("${abs(tiltForward).roundToInt()}° $dirY") + if (parts.isEmpty()) { + append("Level") + } else { + append("Tilted ${parts.joinToString(", ")}") + } + } + Text( + text = tiltText, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } + } Spacer(Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + AxisValueCard( + label = "X", + value = currentReading?.values?.getOrNull(0), + unit = if (SensorType.GRAVITY.unitX.isNotEmpty()) sensorUnitText(SensorType.GRAVITY.unitX) else "", + labelColor = AxisRed, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Y", + value = currentReading?.values?.getOrNull(1), + unit = if (SensorType.GRAVITY.unitY.isNotEmpty()) sensorUnitText(SensorType.GRAVITY.unitY) else "", + labelColor = AxisGreen, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Z", + value = currentReading?.values?.getOrNull(2), + unit = if (SensorType.GRAVITY.unitZ.isNotEmpty()) sensorUnitText(SensorType.GRAVITY.unitZ) else "", + labelColor = AxisBlue, + modifier = Modifier.weight(1f) + ) + } + + Spacer(Modifier.height(20.dp)) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Live waveform", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "50 Hz", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + LiveLineChart( + readings = chartReadings, + sensorType = SensorType.GRAVITY, + modifier = Modifier.fillMaxWidth().height(200.dp) + ) + } + } + + Spacer(Modifier.height(12.dp)) + SensorUsageHint(sensorType = SensorType.GRAVITY) Spacer(Modifier.height(16.dp)) } } + +@Composable +private fun BubbleLevel( + gx: Float, + gy: Float, + magnitude: Float, + modifier: Modifier = Modifier +) { + val bubbleColor = Color(0xFF1D9E75) + val bubbleDotColor = Color(0xFF04342C) + val outerRingColor = Color(0xFFB4B2A9) + val innerRingColor = Color(0xFFD3D1C7) + + Canvas(modifier = modifier) { + val center = Offset(size.width / 2f, size.height / 2f) + val outerRadius = size.minDimension / 2f - 4.dp.toPx() + val innerRadius = outerRadius * 0.63f + val maxBubbleRadius = outerRadius * 0.75f + + drawCircle( + color = outerRingColor, + radius = outerRadius, + center = center, + style = Stroke(width = 1.5.dp.toPx()) + ) + drawCircle( + color = innerRingColor, + radius = innerRadius, + center = center, + style = Stroke(width = 1.dp.toPx()) + ) + + drawLine( + color = innerRingColor, + start = Offset(center.x - outerRadius, center.y), + end = Offset(center.x + outerRadius, center.y), + strokeWidth = 0.8.dp.toPx() + ) + drawLine( + color = innerRingColor, + start = Offset(center.x, center.y - outerRadius), + end = Offset(center.x, center.y + outerRadius), + strokeWidth = 0.8.dp.toPx() + ) + + val normX = (gx / magnitude).coerceIn(-1f, 1f) + val normY = (gy / magnitude).coerceIn(-1f, 1f) + val bubbleOffsetX = normX * maxBubbleRadius + val bubbleOffsetY = -normY * maxBubbleRadius + val bubblePos = Offset(center.x + bubbleOffsetX, center.y + bubbleOffsetY) + val bubbleRadius = outerRadius * 0.19f + + drawCircle( + color = bubbleColor.copy(alpha = 0.85f), + radius = bubbleRadius, + center = bubblePos + ) + drawCircle( + color = bubbleDotColor, + radius = bubbleRadius * 0.17f, + center = bubblePos + ) + } +} + +@Composable +private fun AxisValueCard( + label: String, + value: Float?, + unit: String, + labelColor: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(labelColor.copy(alpha = 0.15f)) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = labelColor + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = formatDetailValue(value), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + if (unit.isNotEmpty()) { + Text( + text = unit, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewGravityScreen() { + SensorAppTheme { + GravityContent( + currentReading = SensorReading( + sensorType = SensorType.GRAVITY, + values = listOf(1.36f, 1.03f, 9.65f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt index 645c14c..44f07f9 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/GyroscopeScreen.kt @@ -42,6 +42,7 @@ import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType import com.shreyash.sensorapp.presentation.theme.SensorAppTheme import com.shreyash.sensorapp.presentation.theme.SensorGreen +import com.shreyash.sensorapp.presentation.detail.sensorUnitText import kotlin.math.sqrt @Composable @@ -252,19 +253,19 @@ private fun AxisValuesCard(reading: SensorReading) { AxisValueCard( label = "X", value = reading.values.getOrNull(0) ?: 0f, - unit = SensorType.GYROSCOPE.unitX, + unit = sensorUnitText(SensorType.GYROSCOPE.unitX), color = Color(0xFF42A5F5) ) AxisValueCard( label = "Y", value = reading.values.getOrNull(1) ?: 0f, - unit = SensorType.GYROSCOPE.unitY, + unit = sensorUnitText(SensorType.GYROSCOPE.unitY), color = SensorGreen ) AxisValueCard( label = "Z", value = reading.values.getOrNull(2) ?: 0f, - unit = SensorType.GYROSCOPE.unitZ, + unit = sensorUnitText(SensorType.GYROSCOPE.unitZ), color = Color(0xFFFF7043) ) } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LightScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LightScreen.kt index 81def46..d1a496b 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LightScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LightScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme @Composable fun LightScreen( @@ -61,3 +63,19 @@ private fun LightContent( Spacer(Modifier.height(16.dp)) } } + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewLightScreen() { + SensorAppTheme { + LightContent( + currentReading = SensorReading( + sensorType = SensorType.LIGHT, + values = listOf(450f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LinearAccelerationScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LinearAccelerationScreen.kt index a3edd9d..f1aac72 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LinearAccelerationScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/LinearAccelerationScreen.kt @@ -1,22 +1,39 @@ package com.shreyash.sensorapp.presentation.detail +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.stringResource +import com.shreyash.sensorapp.R import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme + +private val AxisRed = Color(0xFFE24B4A) +private val AxisGreen = Color(0xFF639922) +private val AxisBlue = Color(0xFF378ADD) @Composable fun LinearAccelerationScreen( @@ -37,41 +54,154 @@ private fun LinearAccelerationContent( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(16.dp) ) { - LiveIndicator() + Text( + text = sensorCategoryName(SensorType.LINEAR_ACCELERATION.category), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = sensorDisplayName(SensorType.LINEAR_ACCELERATION), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) - LiveValueDisplay( - reading = currentReading, - sensorType = SensorType.LINEAR_ACCELERATION - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + AxisValueCard( + label = "X", + value = currentReading?.values?.getOrNull(0), + unit = if (SensorType.LINEAR_ACCELERATION.unitX.isNotEmpty()) sensorUnitText(SensorType.LINEAR_ACCELERATION.unitX) else "", + labelColor = AxisRed, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Y", + value = currentReading?.values?.getOrNull(1), + unit = if (SensorType.LINEAR_ACCELERATION.unitY.isNotEmpty()) sensorUnitText(SensorType.LINEAR_ACCELERATION.unitY) else "", + labelColor = AxisGreen, + modifier = Modifier.weight(1f) + ) + AxisValueCard( + label = "Z", + value = currentReading?.values?.getOrNull(2), + unit = if (SensorType.LINEAR_ACCELERATION.unitZ.isNotEmpty()) sensorUnitText(SensorType.LINEAR_ACCELERATION.unitZ) else "", + labelColor = AxisBlue, + modifier = Modifier.weight(1f) + ) + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(20.dp)) - LiveLineChart( - readings = chartReadings, - sensorType = SensorType.LINEAR_ACCELERATION, - modifier = Modifier.fillMaxWidth().height(260.dp) - ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Live waveform", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "50 Hz", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + MultiAxisLineChart( + readings = chartReadings, + modifier = Modifier.fillMaxWidth().height(200.dp) + ) + } + } Spacer(Modifier.height(12.dp)) - Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.LINEAR_ACCELERATION.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.LINEAR_ACCELERATION.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.LINEAR_ACCELERATION.unitZ}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + SensorUsageHint(sensorType = SensorType.LINEAR_ACCELERATION) Spacer(Modifier.height(16.dp)) + } +} - SensorUsageHint(sensorType = SensorType.LINEAR_ACCELERATION) +@Composable +private fun AxisValueCard( + label: String, + value: Float?, + unit: String, + labelColor: Color, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .background(labelColor.copy(alpha = 0.15f)) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) { + Text( + text = label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = labelColor + ) + } + Spacer(Modifier.height(8.dp)) + Text( + text = formatDetailValue(value), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + if (unit.isNotEmpty()) { + Text( + text = unit, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } +} - Spacer(Modifier.height(16.dp)) +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewLinearAccelerationScreen() { + SensorAppTheme { + LinearAccelerationContent( + currentReading = SensorReading( + sensorType = SensorType.LINEAR_ACCELERATION, + values = listOf(0.05f, -0.03f, 0.12f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) } } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/MagnetometerScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/MagnetometerScreen.kt index 9893744..deb9bd7 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/MagnetometerScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/MagnetometerScreen.kt @@ -14,9 +14,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme +import com.shreyash.sensorapp.presentation.detail.sensorUnitText @Composable fun MagnetometerScreen( @@ -60,9 +63,9 @@ private fun MagnetometerContent( Spacer(Modifier.height(12.dp)) Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.MAGNETOMETER.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.MAGNETOMETER.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.MAGNETOMETER.unitZ}", + text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${sensorUnitText(SensorType.MAGNETOMETER.unitX)}" + + " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${sensorUnitText(SensorType.MAGNETOMETER.unitY)}" + + " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${sensorUnitText(SensorType.MAGNETOMETER.unitZ)}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center @@ -75,3 +78,19 @@ private fun MagnetometerContent( Spacer(Modifier.height(16.dp)) } } + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewMagnetometerScreen() { + SensorAppTheme { + MagnetometerContent( + currentReading = SensorReading( + sensorType = SensorType.MAGNETOMETER, + values = listOf(18.5f, -5.2f, 42.1f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/PressureScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/PressureScreen.kt index 9ed201c..c761a2e 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/PressureScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/PressureScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme @Composable fun PressureScreen( @@ -61,3 +63,19 @@ private fun PressureContent( Spacer(Modifier.height(16.dp)) } } + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewPressureScreen() { + SensorAppTheme { + PressureContent( + currentReading = SensorReading( + sensorType = SensorType.PRESSURE, + values = listOf(1012.3f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/ProximityScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/ProximityScreen.kt index 18a0ab6..4fa89ed 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/ProximityScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/ProximityScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme @Composable fun ProximityScreen( @@ -61,3 +63,19 @@ private fun ProximityContent( Spacer(Modifier.height(16.dp)) } } + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewProximityScreen() { + SensorAppTheme { + ProximityContent( + currentReading = SensorReading( + sensorType = SensorType.PROXIMITY, + values = listOf(0.5f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/RotationVectorScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/RotationVectorScreen.kt index 872ad54..9051143 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/RotationVectorScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/RotationVectorScreen.kt @@ -1,22 +1,45 @@ package com.shreyash.sensorapp.presentation.detail +import android.hardware.SensorManager +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.withTransform +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme +import kotlin.math.abs +import kotlin.math.roundToInt + +private val CardBg = Color(0xFFCECBF6) +private val CardStroke = Color(0xFF534AB7) +private val CardHomeBtn = Color(0xFF3C3489) +private val CardScreen = Color(0xFFF1EFE8) @Composable fun RotationVectorScreen( @@ -33,45 +56,277 @@ private fun RotationVectorContent( currentReading: SensorReading?, chartReadings: List ) { + val values = currentReading?.values + val azimuth = values?.let { computeAzimuth(it) } ?: 0f + val pitch = values?.let { computePitch(it) } ?: 0f + val roll = values?.let { computeRoll(it) } ?: 0f + Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally + .padding(16.dp) ) { - LiveIndicator() + Text( + text = "Orientation", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = sensorDisplayName(SensorType.ROTATION_VECTOR), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) Spacer(Modifier.height(16.dp)) - LiveValueDisplay( - reading = currentReading, - sensorType = SensorType.ROTATION_VECTOR - ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + PhoneOrientationIndicator( + azimuth = azimuth, + pitch = pitch, + roll = roll, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f) + .padding(8.dp) + ) + } + } - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(16.dp)) - LiveLineChart( - readings = chartReadings, - sensorType = SensorType.ROTATION_VECTOR, - modifier = Modifier.fillMaxWidth().height(260.dp) - ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + EulerCard( + label = "Azimuth", + value = azimuth.roundToInt().toString() + "°", + modifier = Modifier.weight(1f) + ) + EulerCard( + label = "Pitch", + value = pitch.roundToInt().toString() + "°", + modifier = Modifier.weight(1f) + ) + EulerCard( + label = "Roll", + value = roll.roundToInt().toString() + "°", + modifier = Modifier.weight(1f) + ) + } - Spacer(Modifier.height(12.dp)) + Spacer(Modifier.height(16.dp)) - Text( - text = "X: ${formatDetailValue(currentReading?.values?.getOrNull(0))} ${SensorType.ROTATION_VECTOR.unitX}" + - " Y: ${formatDetailValue(currentReading?.values?.getOrNull(1))} ${SensorType.ROTATION_VECTOR.unitY}" + - " Z: ${formatDetailValue(currentReading?.values?.getOrNull(2))} ${SensorType.ROTATION_VECTOR.unitZ}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Text( + text = "Quaternion (x, y, z, w)", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + Spacer(Modifier.height(4.dp)) + val qx = formatDetailValue(values?.getOrNull(0)) + val qy = formatDetailValue(values?.getOrNull(1)) + val qz = formatDetailValue(values?.getOrNull(2)) + val qw = formatDetailValue(values?.getOrNull(3)) { v -> String.format("%.2f", v) } + Text( + text = "$qx, $qy, $qz, $qw", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(20.dp)) + + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ) + ) { + Column(modifier = Modifier.padding(14.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Live waveform", + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "50 Hz", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + LiveLineChart( + readings = chartReadings, + sensorType = SensorType.ROTATION_VECTOR, + modifier = Modifier.fillMaxWidth().height(200.dp) + ) + } + } + + Spacer(Modifier.height(12.dp)) SensorUsageHint(sensorType = SensorType.ROTATION_VECTOR) Spacer(Modifier.height(16.dp)) } } + +@Composable +private fun PhoneOrientationIndicator( + azimuth: Float, + pitch: Float, + roll: Float, + modifier: Modifier = Modifier +) { + Canvas(modifier = modifier) { + val c = Offset(size.width / 2f, size.height / 2f) + val outerR = size.minDimension / 2f - 4.dp.toPx() + val outerColor = Color(0xFFB4B2A9) + val innerColor = Color(0xFFD3D1C7) + + drawCircle(color = outerColor, radius = outerR, center = c, style = Stroke(1.5.dp.toPx())) + drawCircle(color = innerColor, radius = outerR * 0.63f, center = c, style = Stroke(1.dp.toPx())) + drawLine(color = innerColor, start = Offset(c.x - outerR, c.y), end = Offset(c.x + outerR, c.y), strokeWidth = 0.8.dp.toPx()) + drawLine(color = innerColor, start = Offset(c.x, c.y - outerR), end = Offset(c.x, c.y + outerR), strokeWidth = 0.8.dp.toPx()) + + val pw = outerR * 0.53f + val ph = outerR * 0.95f + val cr = pw * 0.18f + val scaleY = (1f - abs(pitch) * 0.003f).coerceIn(0.7f, 1f) + + withTransform({ + translate(left = c.x, top = c.y) + rotate(degrees = azimuth + roll * 0.3f) + if (scaleY < 1f) { + scale(scaleX = 1f, scaleY = scaleY) + } + }) { + val path = Path().apply { + addRoundRect(roundRect = androidx.compose.ui.geometry.RoundRect( + left = -pw / 2f, top = -ph / 2f, right = pw / 2f, bottom = ph / 2f, + cornerRadius = CornerRadius(cr, cr) + )) + } + drawPath(path, color = CardBg, style = Stroke(2.dp.toPx())) + drawPath(path, color = CardStroke, style = Stroke(1.5.dp.toPx())) + drawCircle(color = CardHomeBtn, radius = pw * 0.07f, center = Offset(0f, ph * 0.31f)) + val screenPath = Path().apply { + addRoundRect(roundRect = androidx.compose.ui.geometry.RoundRect( + left = -pw * 0.28f, top = -ph * 0.38f, right = pw * 0.28f, bottom = ph * 0.1f, + cornerRadius = CornerRadius(cr * 0.5f, cr * 0.5f) + )) + } + drawPath(screenPath, color = CardScreen) + drawPath(screenPath, color = CardStroke.copy(alpha = 0.3f), style = Stroke(0.5.dp.toPx())) + } + } +} + +@Composable +private fun EulerCard( + label: String, + value: String, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + Spacer(Modifier.height(6.dp)) + Text( + text = value, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } + } +} + +private fun computeAzimuth(values: List): Float { + if (values.size < 4) return 0f + val r = FloatArray(9) + SensorManager.getRotationMatrixFromVector(r, values.toFloatArray()) + val orientation = FloatArray(3) + SensorManager.getOrientation(r, orientation) + return orientation[0] * 180f / Math.PI.toFloat() +} + +private fun computePitch(values: List): Float { + if (values.size < 4) return 0f + val r = FloatArray(9) + SensorManager.getRotationMatrixFromVector(r, values.toFloatArray()) + val orientation = FloatArray(3) + SensorManager.getOrientation(r, orientation) + return orientation[1] * 180f / Math.PI.toFloat() +} + +private fun computeRoll(values: List): Float { + if (values.size < 4) return 0f + val r = FloatArray(9) + SensorManager.getRotationMatrixFromVector(r, values.toFloatArray()) + val orientation = FloatArray(3) + SensorManager.getOrientation(r, orientation) + return orientation[2] * 180f / Math.PI.toFloat() +} + +private fun formatDetailValue(value: Float?, formatter: (Float) -> String = { v -> String.format("%.2f", v) }): String { + if (value == null) return "--" + return formatter(value) +} + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewRotationVectorScreen() { + SensorAppTheme { + RotationVectorContent( + currentReading = SensorReading( + sensorType = SensorType.ROTATION_VECTOR, + values = listOf(0.08f, 0.16f, 0.36f, 0.92f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorChart.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorChart.kt index 3e34ebe..2b09f30 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorChart.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorChart.kt @@ -1,10 +1,18 @@ package com.shreyash.sensorapp.presentation.detail import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.ui.draw.clip import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -198,6 +206,210 @@ fun LiveLineChart( } } +@Composable +fun MultiAxisLineChart( + readings: List, + modifier: Modifier = Modifier +) { + if (readings.isEmpty()) { + Box(modifier = modifier, contentAlignment = Alignment.Center) { + Text( + text = "No data yet", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + return + } + + val axisColors = listOf( + Color(0xFFE24B4A), + Color(0xFF639922), + Color(0xFF378ADD) + ) + val axisLabels = listOf("X", "Y", "Z") + + val axisValues = (0..2).map { axis -> + readings.map { it.values.getOrElse(axis) { 0f } } + } + + val allValues = axisValues.flatten() + val minVal = allValues.min() + val maxVal = allValues.max() + val range = if (maxVal - minVal == 0f) 1f else maxVal - minVal + val paddingV = 0.1f + val adjustedMin = minVal - range * paddingV + val adjustedMax = maxVal + range * paddingV + val adjustedRange = adjustedMax - adjustedMin + + val gridColor = MaterialTheme.colorScheme.outlineVariant + + var touchIndex by remember { mutableStateOf(-1) } + + Card( + modifier = modifier, + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) + ) { + Canvas( + modifier = Modifier + .fillMaxSize() + .padding(start = 8.dp, top = 24.dp, end = 8.dp, bottom = 24.dp) + .pointerInput(axisValues) { + detectTapGestures { offset -> + if (axisValues[0].size < 2) return@detectTapGestures + val stepX = size.width.toFloat() / (axisValues[0].size - 1) + val index = ((offset.x / stepX) + 0.5f).toInt() + .coerceIn(0, axisValues[0].size - 1) + touchIndex = if (touchIndex == index) -1 else index + } + } + ) { + if (axisValues[0].size < 2) return@Canvas + + val stepX = size.width / (axisValues[0].size - 1).coerceAtLeast(1) + + fun yForValue(value: Float): Float { + val normY = ((value - adjustedMin) / adjustedRange).coerceIn(0f, 1f) + return size.height - normY * size.height + } + + for (i in 0 until axisValues[0].size) { + val x = i * stepX + drawLine( + color = gridColor, + start = Offset(x, 0f), + end = Offset(x, size.height), + strokeWidth = 0.5f + ) + } + + axisValues.forEachIndexed { axisIdx, values -> + val linePath = Path() + values.forEachIndexed { index, value -> + val x = index * stepX + val y = yForValue(value) + if (index == 0) { + linePath.moveTo(x, y) + } else { + linePath.lineTo(x, y) + } + } + drawPath( + path = linePath, + color = axisColors[axisIdx], + style = Stroke( + width = 2.dp.toPx(), + cap = StrokeCap.Round, + join = StrokeJoin.Round + ) + ) + } + + val labelPaint = android.graphics.Paint().apply { + color = android.graphics.Color.GRAY + textSize = 22f + textAlign = android.graphics.Paint.Align.RIGHT + isAntiAlias = true + } + + drawContext.canvas.nativeCanvas.drawText( + formatLargeValue(adjustedMax), + -4.dp.toPx(), + 0f, + labelPaint + ) + + drawContext.canvas.nativeCanvas.drawText( + formatLargeValue(adjustedMin), + -4.dp.toPx(), + size.height, + labelPaint + ) + + if (touchIndex in axisValues[0].indices && axisValues[0].size >= 2) { + val x = touchIndex * stepX + drawLine( + color = Color.White.copy(alpha = 0.5f), + start = Offset(x, 0f), + end = Offset(x, size.height), + strokeWidth = 1f + ) + + val tooltipPaint = android.graphics.Paint().apply { + color = android.graphics.Color.WHITE + textSize = 24f + textAlign = android.graphics.Paint.Align.CENTER + isAntiAlias = true + isFakeBoldText = true + } + + val lines = axisValues.mapIndexed { i, values -> + "${axisLabels[i]}: ${formatLargeValue(values[touchIndex])}" + } + val tooltipText = lines.joinToString(" ") + val textWidth = tooltipPaint.measureText(tooltipText) + val padding = 12f + val tooltipHeight = tooltipPaint.textSize + padding * 2 + val rectWidth = textWidth + padding * 2 + val rectLeft = (x - rectWidth / 2).coerceIn(0f, size.width - rectWidth) + val rectTop = 4.dp.toPx() + + drawRoundRect( + color = Color(40, 40, 45, 230), + topLeft = Offset(rectLeft, rectTop), + size = Size(rectWidth, tooltipHeight), + cornerRadius = CornerRadius(8f, 8f) + ) + + drawContext.canvas.nativeCanvas.drawText( + tooltipText, + x, + rectTop + padding + tooltipPaint.textSize / 2 + 2f, + tooltipPaint + ) + + axisValues.forEachIndexed { axisIdx, values -> + val value = values[touchIndex] + val y = yForValue(value) + drawCircle( + color = axisColors[axisIdx], + radius = 4.dp.toPx(), + center = Offset(x, y) + ) + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 14.dp, end = 14.dp, bottom = 8.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + axisColors.forEachIndexed { i, color -> + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(8.dp) + .clip(RoundedCornerShape(2.dp)) + .background(color) + ) + Spacer(Modifier.width(4.dp)) + Text( + text = axisLabels[i], + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + private fun DrawScope.drawCrosshairTooltip(value: Float, x: Float, y: Float) { val text = formatLargeValue(value) val paint = android.graphics.Paint().apply { diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDetailScaffold.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDetailScaffold.kt index a1535b2..4ee4883 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDetailScaffold.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDetailScaffold.kt @@ -10,9 +10,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Download @@ -20,6 +23,7 @@ import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -46,6 +50,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.detail.sensorDisplayName import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -87,7 +92,8 @@ fun SensorDetailScaffold( Scaffold( topBar = { CenterAlignedTopAppBar( - title = { Text(sensorType.displayName) }, + modifier = Modifier.statusBarsPadding(), + title = { Text(sensorDisplayName(sensorType)) }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") @@ -103,12 +109,14 @@ fun SensorDetailScaffold( Row( modifier = Modifier .fillMaxWidth() + .navigationBarsPadding() .padding(16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Button( onClick = { viewModel.toggleLogging() }, modifier = Modifier.weight(1f), + shape = RoundedCornerShape(14.dp), colors = ButtonDefaults.buttonColors( containerColor = if (isLogging) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary @@ -123,7 +131,7 @@ fun SensorDetailScaffold( Text(if (isLogging) "Stop Logging" else "Start Logging") } - Button( + OutlinedButton( onClick = { scope.launch { val uri = exportToCsv(context, chartReadings, sensorType) @@ -133,6 +141,7 @@ fun SensorDetailScaffold( } }, modifier = Modifier.weight(1f), + shape = RoundedCornerShape(14.dp), enabled = chartReadings.isNotEmpty() ) { Icon( diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDisplayCard.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDisplayCard.kt index eb3cae2..94a18e2 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDisplayCard.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorDisplayCard.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType import com.shreyash.sensorapp.presentation.theme.SensorGreen +import com.shreyash.sensorapp.presentation.detail.sensorUnitText @Composable fun LiveValueDisplay( @@ -227,7 +228,7 @@ private fun SingleAxisDisplay(reading: SensorReading, sensorType: SensorType) { ) if (sensorType.unitSingle != null && sensorType.unitSingle.isNotEmpty()) { Text( - text = sensorType.unitSingle, + text = sensorUnitText(sensorType.unitSingle), style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary ) @@ -243,20 +244,20 @@ private fun MultiAxisDisplay(reading: SensorReading, sensorType: SensorType) { AxisValue( label = "X", value = reading.values.getOrNull(0) ?: 0f, - unit = sensorType.unitX + unit = if (sensorType.unitX.isNotEmpty()) sensorUnitText(sensorType.unitX) else "" ) if (sensorType.axisCount >= 2) { AxisValue( label = "Y", value = reading.values.getOrNull(1) ?: 0f, - unit = sensorType.unitY + unit = if (sensorType.unitY.isNotEmpty()) sensorUnitText(sensorType.unitY) else "" ) } if (sensorType.axisCount >= 3) { AxisValue( label = "Z", value = reading.values.getOrNull(2) ?: 0f, - unit = sensorType.unitZ + unit = if (sensorType.unitZ.isNotEmpty()) sensorUnitText(sensorType.unitZ) else "" ) } } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorStrings.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorStrings.kt new file mode 100644 index 0000000..c302494 --- /dev/null +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorStrings.kt @@ -0,0 +1,85 @@ +package com.shreyash.sensorapp.presentation.detail + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.shreyash.sensorapp.R +import com.shreyash.sensorapp.domain.model.SensorCategory +import com.shreyash.sensorapp.domain.model.SensorType + +@Composable +fun sensorDisplayName(type: SensorType): String = stringResource(sensorDisplayNameRes(type)) + +@StringRes +fun sensorDisplayNameRes(type: SensorType): Int = when (type) { + SensorType.ACCELEROMETER -> R.string.sensor_accelerometer + SensorType.GYROSCOPE -> R.string.sensor_gyroscope + SensorType.LINEAR_ACCELERATION -> R.string.sensor_linear_acceleration + SensorType.MAGNETOMETER -> R.string.sensor_magnetometer + SensorType.GRAVITY -> R.string.sensor_gravity + SensorType.ROTATION_VECTOR -> R.string.sensor_rotation_vector + SensorType.LIGHT -> R.string.sensor_light + SensorType.PROXIMITY -> R.string.sensor_proximity + SensorType.PRESSURE -> R.string.sensor_barometer + SensorType.STEP_COUNTER -> R.string.sensor_step_counter +} + +@Composable +fun sensorCategoryName(category: SensorCategory): String = stringResource(sensorCategoryRes(category)) + +@StringRes +fun sensorCategoryRes(category: SensorCategory): Int = when (category) { + SensorCategory.MOTION -> R.string.category_motion + SensorCategory.ENVIRONMENTAL -> R.string.category_environmental + SensorCategory.POSITION -> R.string.category_position + SensorCategory.ACTIVITY -> R.string.category_activity +} + +@Composable +fun sensorDescription(type: SensorType): String = stringResource(sensorDescriptionRes(type)) + +@StringRes +fun sensorDescriptionRes(type: SensorType): Int = when (type) { + SensorType.ACCELEROMETER -> R.string.sensor_accelerometer_desc + SensorType.GYROSCOPE -> R.string.sensor_gyroscope_desc + SensorType.LINEAR_ACCELERATION -> R.string.sensor_linear_acceleration_desc + SensorType.MAGNETOMETER -> R.string.sensor_magnetometer_desc + SensorType.GRAVITY -> R.string.sensor_gravity_desc + SensorType.ROTATION_VECTOR -> R.string.sensor_rotation_vector_desc + SensorType.LIGHT -> R.string.sensor_light_desc + SensorType.PROXIMITY -> R.string.sensor_proximity_desc + SensorType.PRESSURE -> R.string.sensor_barometer_desc + SensorType.STEP_COUNTER -> R.string.sensor_step_counter_desc +} + +@Composable +fun sensorUsageHint(type: SensorType): String = stringResource(sensorUsageHintRes(type)) + +@StringRes +fun sensorUsageHintRes(type: SensorType): Int = when (type) { + SensorType.ACCELEROMETER -> R.string.hint_accelerometer + SensorType.GYROSCOPE -> R.string.hint_gyroscope + SensorType.LINEAR_ACCELERATION -> R.string.hint_linear_acceleration + SensorType.MAGNETOMETER -> R.string.hint_magnetometer + SensorType.GRAVITY -> R.string.hint_gravity + SensorType.ROTATION_VECTOR -> R.string.hint_rotation_vector + SensorType.LIGHT -> R.string.hint_light + SensorType.PROXIMITY -> R.string.hint_proximity + SensorType.PRESSURE -> R.string.hint_pressure + SensorType.STEP_COUNTER -> R.string.hint_step_counter +} + +@Composable +fun sensorUnitText(unit: String): String = if (unit.isEmpty()) "" else stringResource(sensorUnitRes(unit)) + +@StringRes +fun sensorUnitRes(unit: String): Int = when (unit) { + "m/s²" -> R.string.unit_mps2 + "°/s" -> R.string.unit_dps + "µT" -> R.string.unit_microtesla + "lx" -> R.string.unit_lux + "cm" -> R.string.unit_cm + "hPa" -> R.string.unit_hpa + "steps" -> R.string.unit_steps + else -> throw IllegalArgumentException("Unknown unit: $unit") +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorUtils.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorUtils.kt index bca3bca..2a95e11 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorUtils.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/SensorUtils.kt @@ -23,9 +23,11 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.shreyash.sensorapp.R import com.shreyash.sensorapp.domain.model.SensorType import com.shreyash.sensorapp.presentation.theme.SensorGreen @@ -58,7 +60,7 @@ fun LiveIndicator() { ) Spacer(Modifier.width(8.dp)) Text( - text = "LIVE", + text = stringResource(R.string.live_label), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.Bold, color = SensorGreen @@ -69,21 +71,8 @@ fun LiveIndicator() { @Composable fun SensorUsageHint(sensorType: SensorType) { - val hint = when (sensorType) { - SensorType.ACCELEROMETER -> "Tilt or shake the device to see acceleration forces change across X, Y, and Z axes." - SensorType.GYROSCOPE -> "Rotate the device to measure the rate of rotation around each axis." - SensorType.LINEAR_ACCELERATION -> "Move the device quickly to measure acceleration excluding gravity." - SensorType.MAGNETOMETER -> "Move the device near a metal object or wave it in a figure-8 pattern to test." - SensorType.GRAVITY -> "Tilt the device to see how gravity distributes across each axis." - SensorType.ROTATION_VECTOR -> "Rotate the device to observe orientation changes relative to the world." - SensorType.LIGHT -> "Cover and uncover the light sensor (usually near the front camera) to see lux level changes." - SensorType.PROXIMITY -> "Cover the top of the device to trigger the sensor. Used to detect when the phone is held to the ear." - SensorType.PRESSURE -> "Ambient air pressure changes with altitude. Try moving to a different floor or elevation." - SensorType.STEP_COUNTER -> "Walk or simulate steps to see the step count increment in real time." - } - Text( - text = hint, + text = sensorUsageHint(sensorType), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), textAlign = TextAlign.Center diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/StepCounterScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/StepCounterScreen.kt index 2cb5c5f..93cf30e 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/StepCounterScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/StepCounterScreen.kt @@ -11,9 +11,11 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.shreyash.sensorapp.domain.model.SensorReading import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.theme.SensorAppTheme @Composable fun StepCounterScreen( @@ -61,3 +63,19 @@ private fun StepCounterContent( Spacer(Modifier.height(16.dp)) } } + +@Preview(showBackground = true, backgroundColor = 0xFF1A1C1E) +@Composable +private fun PreviewStepCounterScreen() { + SensorAppTheme { + StepCounterContent( + currentReading = SensorReading( + sensorType = SensorType.STEP_COUNTER, + values = listOf(1247f), + accuracy = 3, + timestampMs = System.currentTimeMillis() + ), + chartReadings = emptyList() + ) + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/history/HistoryScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/history/HistoryScreen.kt index 72d3fb5..324df33 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/history/HistoryScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -52,6 +53,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.shreyash.sensorapp.domain.model.LogSession import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.detail.sensorDisplayName import com.shreyash.sensorapp.presentation.sensorIcon import com.shreyash.sensorapp.presentation.theme.SensorAppTheme @@ -88,6 +90,7 @@ fun HistoryScreen( Scaffold( topBar = { CenterAlignedTopAppBar( + modifier = Modifier.statusBarsPadding(), title = { Text("History") }, navigationIcon = { IconButton(onClick = onBack) { @@ -251,7 +254,7 @@ private fun SessionItem(session: LogSession) { Spacer(Modifier.width(12.dp)) Column(modifier = Modifier.weight(1f)) { Text( - text = session.sensorType.displayName, + text = sensorDisplayName(session.sensorType), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold ) diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/permission/PermissionHandler.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/permission/PermissionHandler.kt index c4994a4..a2cc7f3 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/permission/PermissionHandler.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/permission/PermissionHandler.kt @@ -18,9 +18,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.core.app.ActivityCompat +import com.shreyash.sensorapp.R import com.shreyash.sensorapp.domain.model.SensorType +import com.shreyash.sensorapp.presentation.detail.sensorDisplayName import kotlinx.coroutines.launch @Composable @@ -122,22 +125,21 @@ fun PermissionDialog( AlertDialog( onDismissRequest = onDismiss, title = { - Text(text = "Permission needed", fontWeight = FontWeight.Bold) + Text(text = stringResource(R.string.permission_dialog_title), fontWeight = FontWeight.Bold) }, text = { Text( - "To use the ${sensorType.displayName}, SensorApp needs access to your " + - "physical activity. This data stays on your device and is never uploaded." + stringResource(R.string.permission_body_activity, sensorDisplayName(sensorType)) ) }, confirmButton = { TextButton(onClick = onConfirm) { - Text("Allow") + Text(stringResource(R.string.allow)) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Not now") + Text(stringResource(R.string.not_now)) } } ) diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsScreen.kt index 8638a3e..b27f144 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -57,6 +58,7 @@ fun SettingsScreen( Scaffold( topBar = { CenterAlignedTopAppBar( + modifier = Modifier.statusBarsPadding(), title = { Text("Settings") }, navigationIcon = { IconButton(onClick = onBack) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57488b4..db7ad3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,4 +24,60 @@ Database stats Total rows No readings yet + + + Accelerometer + Measures acceleration forces applied to the device, including gravity. + Gyroscope + Measures the device\'s rate of rotation around each axis. + Linear Acceleration + Measures the acceleration forces excluding gravity. + Magnetometer + Measures the ambient magnetic field in the environment. + Gravity + Measures the direction and magnitude of gravity. + Rotation Vector + Measures the orientation of the device relative to the world coordinate system. + Light + Measures the ambient light level (illuminance). + Proximity + Measures the distance of an object relative to the device screen. + Barometer + Measures the ambient air pressure. + Step Counter + Counts the number of steps taken since the last device reboot. + + + Motion + Environmental + Position + Activity + + + m/s² + °/s + µT + lx + cm + hPa + steps + + + Tilt or shake the device to see acceleration forces change across X, Y, and Z axes. + Rotate the device to measure the rate of rotation around each axis. + Move the device quickly to measure acceleration excluding gravity. + Move the device near a metal object or wave it in a figure-8 pattern to test. + Tilt the device to see how gravity distributes across each axis. + Rotate the device to observe orientation changes relative to the world. + Cover and uncover the light sensor (usually near the front camera) to see lux level changes. + Cover the top of the device to trigger the sensor. Used to detect when the phone is held to the ear. + Ambient air pressure changes with altitude. Try moving to a different floor or elevation. + Walk or simulate steps to see the step count increment in real time. + + + To use the %1$s, SensorApp needs access to your physical activity. This data stays on your device and is never uploaded. + + + Unavailable + Your device does not have a %1$s.\n\n%2$s