Skip to content
30 changes: 30 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: |
Expand All @@ -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:
Expand All @@ -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: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -76,6 +77,7 @@ fun CompassScreen(
Scaffold(
topBar = {
CenterAlignedTopAppBar(
modifier = Modifier.statusBarsPadding(),
title = { Text("Compass") },
navigationIcon = {
IconButton(onClick = onBack) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -80,6 +86,7 @@ fun DashboardScreen(
Scaffold(
topBar = {
CenterAlignedTopAppBar(
modifier = Modifier.statusBarsPadding(),
title = {
Text(
"Sensors",
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand All @@ -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)
)
Expand Down Expand Up @@ -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,
Expand All @@ -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)
)
Expand All @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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)
)
}
}
}
}
Loading
Loading