diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index abcd4e5..7b5bb46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,8 @@ + + diff --git a/app/src/main/java/com/shreyash/sensorapp/data/repository/SensorRepositoryImpl.kt b/app/src/main/java/com/shreyash/sensorapp/data/repository/SensorRepositoryImpl.kt index ea735e8..c73c4e3 100644 --- a/app/src/main/java/com/shreyash/sensorapp/data/repository/SensorRepositoryImpl.kt +++ b/app/src/main/java/com/shreyash/sensorapp/data/repository/SensorRepositoryImpl.kt @@ -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 { return sensorDataSource.observeSensor(sensorType, currentDelay) } @@ -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( diff --git a/app/src/main/java/com/shreyash/sensorapp/data/sensor/HapticManager.kt b/app/src/main/java/com/shreyash/sensorapp/data/sensor/HapticManager.kt new file mode 100644 index 0000000..230c0cc --- /dev/null +++ b/app/src/main/java/com/shreyash/sensorapp/data/sensor/HapticManager.kt @@ -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) + } + } +} diff --git a/app/src/main/java/com/shreyash/sensorapp/di/SensorModule.kt b/app/src/main/java/com/shreyash/sensorapp/di/SensorModule.kt index d2a3990..5a169e6 100644 --- a/app/src/main/java/com/shreyash/sensorapp/di/SensorModule.kt +++ b/app/src/main/java/com/shreyash/sensorapp/di/SensorModule.kt @@ -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 diff --git a/app/src/main/java/com/shreyash/sensorapp/domain/repository/SensorRepository.kt b/app/src/main/java/com/shreyash/sensorapp/domain/repository/SensorRepository.kt index 4b84d61..5d92bff 100644 --- a/app/src/main/java/com/shreyash/sensorapp/domain/repository/SensorRepository.kt +++ b/app/src/main/java/com/shreyash/sensorapp/domain/repository/SensorRepository.kt @@ -34,4 +34,8 @@ interface SensorRepository { suspend fun getDelay(): Int suspend fun setDelay(delay: Int) + + fun isHapticEnabled(): Boolean + + fun setHapticEnabled(enabled: Boolean) } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/DetailViewModel.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/DetailViewModel.kt index 68e68f8..628da35 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/detail/DetailViewModel.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/detail/DetailViewModel.kt @@ -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 @@ -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("sensorType") ?: "ACCELEROMETER" - ) + private var _sensorType: SensorType = SensorType.ACCELEROMETER + val sensorType: SensorType get() = _sensorType private val _currentReading = MutableStateFlow(null) val currentReading: StateFlow = _currentReading.asStateFlow() @@ -40,8 +40,13 @@ 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() } @@ -49,6 +54,9 @@ class DetailViewModel @Inject constructor( sensorJob?.cancel() sensorJob = viewModelScope.launch { observeSensorUseCase(sensorType).collect { reading -> + val prev = previousReading + previousReading = reading + _currentReading.value = reading _chartReadings.update { buffer -> (buffer + reading).takeLast(60) @@ -56,7 +64,42 @@ class DetailViewModel @Inject constructor( 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 -> {} } } 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 37c4d85..a1535b2 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 @@ -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 @@ -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 -> 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 9c94b10..8638a3e 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 @@ -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 @@ -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 = { @@ -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) ) } @@ -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( @@ -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), @@ -247,7 +290,9 @@ private fun PreviewSettingsScreen() { SettingsScreenContent( selectedDelay = 2, totalRows = 15234, + hapticEnabled = true, onDelayChanged = {}, + onHapticToggle = {}, modifier = Modifier.padding(padding) ) } diff --git a/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsViewModel.kt index 17c95bf..879e934 100644 --- a/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/shreyash/sensorapp/presentation/settings/SettingsViewModel.kt @@ -21,10 +21,14 @@ class SettingsViewModel @Inject constructor( private val _totalRows = MutableStateFlow(0) val totalRows: StateFlow = _totalRows.asStateFlow() + private val _hapticEnabled = MutableStateFlow(true) + val hapticEnabled: StateFlow = _hapticEnabled.asStateFlow() + init { viewModelScope.launch { _selectedDelay.value = repository.getDelay() _totalRows.value = repository.getTotalRowCount() + _hapticEnabled.value = repository.isHapticEnabled() } } @@ -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",