Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.VIBRATE" />

<!-- Declared but requested just-in-time -->
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class SensorRepositoryImpl @Inject constructor(
@Volatile
private var currentDelay: Int = android.hardware.SensorManager.SENSOR_DELAY_UI

@Volatile
private var hapticEnabled: Boolean = true

override fun observeSensor(sensorType: SensorType): Flow<SensorReading> {
return sensorDataSource.observeSensor(sensorType, currentDelay)
}
Expand Down Expand Up @@ -114,6 +117,12 @@ class SensorRepositoryImpl @Inject constructor(
override suspend fun setDelay(delay: Int) {
currentDelay = delay
}

override fun isHapticEnabled(): Boolean = hapticEnabled

override fun setHapticEnabled(enabled: Boolean) {
hapticEnabled = enabled
}
}

private fun SensorReadingEntity.toDomainModel() = SensorReading(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.shreyash.sensorapp.data.sensor

import android.content.Context
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import android.os.VibratorManager
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class HapticManager @Inject constructor(
@ApplicationContext context: Context
) {
private val vibrator: Vibrator? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vm = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager
vm?.defaultVibrator
} else {
@Suppress("DEPRECATION")
context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
}

fun tick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator?.vibrate(
VibrationEffect.createOneShot(20L, VibrationEffect.DEFAULT_AMPLITUDE)
)
} else {
@Suppress("DEPRECATION")
vibrator?.vibrate(20L)
}
}

fun doubleTick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator?.vibrate(
VibrationEffect.createWaveform(longArrayOf(0L, 20L, 40L, 20L), intArrayOf(0, 128, 0, 128), -1)
)
} else {
@Suppress("DEPRECATION")
vibrator?.vibrate(longArrayOf(0, 20, 40, 20), -1)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.shreyash.sensorapp.di

import android.content.Context
import android.hardware.SensorManager
import com.shreyash.sensorapp.data.sensor.HapticManager
import com.shreyash.sensorapp.data.sensor.SensorDataSource
import com.shreyash.sensorapp.data.repository.SensorRepositoryImpl
import com.shreyash.sensorapp.domain.repository.SensorRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ interface SensorRepository {
suspend fun getDelay(): Int

suspend fun setDelay(delay: Int)

fun isHapticEnabled(): Boolean

fun setHapticEnabled(enabled: Boolean)
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.shreyash.sensorapp.presentation.detail

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.shreyash.sensorapp.data.sensor.HapticManager
import com.shreyash.sensorapp.domain.model.SensorReading
import com.shreyash.sensorapp.domain.model.SensorType
import com.shreyash.sensorapp.domain.usecase.LogSensorReadingUseCase
Expand All @@ -16,18 +16,18 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.math.sqrt

@HiltViewModel
class DetailViewModel @Inject constructor(
private val observeSensorUseCase: ObserveSensorUseCase,
private val logSensorReadingUseCase: LogSensorReadingUseCase,
private val repository: SensorRepository,
savedStateHandle: SavedStateHandle
private val hapticManager: HapticManager
) : ViewModel() {

val sensorType: SensorType = SensorType.valueOf(
savedStateHandle.get<String>("sensorType") ?: "ACCELEROMETER"
)
private var _sensorType: SensorType = SensorType.ACCELEROMETER
val sensorType: SensorType get() = _sensorType

private val _currentReading = MutableStateFlow<SensorReading?>(null)
val currentReading: StateFlow<SensorReading?> = _currentReading.asStateFlow()
Expand All @@ -40,23 +40,66 @@ class DetailViewModel @Inject constructor(

private var sensorJob: Job? = null
private var currentSessionId: Long? = null
private var previousReading: SensorReading? = null
private var initialized = false

init {
fun initialize(sensorType: SensorType) {
if (initialized) return
initialized = true
_sensorType = sensorType
startObserving()
}

fun startObserving() {
sensorJob?.cancel()
sensorJob = viewModelScope.launch {
observeSensorUseCase(sensorType).collect { reading ->
val prev = previousReading
previousReading = reading

_currentReading.value = reading
_chartReadings.update { buffer ->
(buffer + reading).takeLast(60)
}
if (_isLogging.value) {
logSensorReadingUseCase(reading)
}

if (repository.isHapticEnabled()) {
checkHapticTriggers(reading, prev)
}
}
}
}

private fun checkHapticTriggers(reading: SensorReading, prev: SensorReading?) {
when (sensorType) {
SensorType.PROXIMITY -> {
val currVal = reading.values.getOrNull(0) ?: return
val prevVal = prev?.values?.getOrNull(0) ?: return
val currObstructed = currVal < 1f
val prevObstructed = prevVal < 1f
if (prevObstructed != currObstructed) {
hapticManager.doubleTick()
}
}
SensorType.STEP_COUNTER -> {
val currVal = reading.values.getOrNull(0) ?: 0f
val prevVal = prev?.values?.getOrNull(0) ?: currVal
if (currVal > prevVal) {
hapticManager.tick()
}
}
SensorType.GYROSCOPE -> {
val gx = reading.values.getOrNull(0) ?: 0f
val gy = reading.values.getOrNull(1) ?: 0f
val gz = reading.values.getOrNull(2) ?: 0f
val magnitude = sqrt((gx * gx + gy * gy + gz * gz).toDouble()).toFloat()
if (magnitude > 5f) {
hapticManager.tick()
}
}
else -> {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
Expand Down Expand Up @@ -66,6 +67,10 @@ fun SensorDetailScaffold(
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

LaunchedEffect(sensorType) {
viewModel.initialize(sensorType)
}

val lifecycleOwner = LocalLifecycleOwner.current
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
Expand All @@ -51,6 +52,7 @@ fun SettingsScreen(
) {
val selectedDelay by viewModel.selectedDelay.collectAsStateWithLifecycle()
val totalRows by viewModel.totalRows.collectAsStateWithLifecycle()
val hapticEnabled by viewModel.hapticEnabled.collectAsStateWithLifecycle()

Scaffold(
topBar = {
Expand All @@ -70,7 +72,9 @@ fun SettingsScreen(
SettingsScreenContent(
selectedDelay = selectedDelay,
totalRows = totalRows,
hapticEnabled = hapticEnabled,
onDelayChanged = { viewModel.setDelay(it) },
onHapticToggle = { viewModel.setHapticEnabled(it) },
modifier = Modifier.padding(padding)
)
}
Expand All @@ -80,7 +84,9 @@ fun SettingsScreen(
private fun SettingsScreenContent(
selectedDelay: Int,
totalRows: Int,
hapticEnabled: Boolean,
onDelayChanged: (Int) -> Unit,
onHapticToggle: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
Column(
Expand Down Expand Up @@ -125,6 +131,43 @@ private fun SettingsScreenContent(

Spacer(Modifier.height(24.dp))

SettingsSection(title = "Haptic Feedback") {
Card(
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onHapticToggle(!hapticEnabled) }
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Sensor haptics",
style = MaterialTheme.typography.bodyLarge
)
Text(
text = "Vibrate on proximity change, step count, and fast rotation",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(Modifier.width(16.dp))
Switch(
checked = hapticEnabled,
onCheckedChange = onHapticToggle
)
}
}
}

Spacer(Modifier.height(24.dp))

SettingsSection(title = "Database Stats") {
Card(
shape = RoundedCornerShape(12.dp),
Expand Down Expand Up @@ -247,7 +290,9 @@ private fun PreviewSettingsScreen() {
SettingsScreenContent(
selectedDelay = 2,
totalRows = 15234,
hapticEnabled = true,
onDelayChanged = {},
onHapticToggle = {},
modifier = Modifier.padding(padding)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ class SettingsViewModel @Inject constructor(
private val _totalRows = MutableStateFlow(0)
val totalRows: StateFlow<Int> = _totalRows.asStateFlow()

private val _hapticEnabled = MutableStateFlow(true)
val hapticEnabled: StateFlow<Boolean> = _hapticEnabled.asStateFlow()

init {
viewModelScope.launch {
_selectedDelay.value = repository.getDelay()
_totalRows.value = repository.getTotalRowCount()
_hapticEnabled.value = repository.isHapticEnabled()
}
}

Expand All @@ -35,6 +39,13 @@ class SettingsViewModel @Inject constructor(
}
}

fun setHapticEnabled(enabled: Boolean) {
viewModelScope.launch {
repository.setHapticEnabled(enabled)
_hapticEnabled.value = enabled
}
}

companion object {
val delayOptions = listOf(
0 to "FASTEST",
Expand Down
Loading