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
169 changes: 151 additions & 18 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,162 @@
# SensorApp

Android Kotlin app (minSdk 24, targetSdk 36, v1.7). Package: `com.shreyash.sensorapp`.
Android Kotlin app (minSdk 24, targetSdk 36). Package: `com.shreyash.sensorapp`.

## Tech
Kotlin 1.9.22 · Jetpack Compose (BOM 2024.02.00) · Material 3 · Hilt 2.50 · Room 2.6.1 · Coroutines 1.7.3 · Compose Navigation 2.7.7

## Architecture
Clean Architecture: data/domain/presentation layers with Dagger Hilt DI.
## Tech Stack
- **Kotlin** 1.9.22 · **Jetpack Compose** (BOM 2024.02.00) · **Material 3**
- **Hilt** 2.50 · **Room** 2.6.1 · **Coroutines** 1.7.3 · **Compose Navigation** 2.7.7

## Build
`./gradlew assembleDebug` — requires JDK 17+ (use Android Studio JBR at `/Applications/Android Studio.app/Contents/jbr/Contents/Home`).
```bash
./gradlew assembleDebug
```
Requires JDK 17+ (use Android Studio JBR at `/Applications/Android Studio.app/Contents/jbr/Contents/Home`).

CI: `.github/workflows/build.yml` — lint on PR, bump+signed AAB+upload+commit on merge to `main`.

---

## Architecture — Clean Architecture

```
data/ ← SensorDataSource (callbackFlow), Room DB, RepositoryImpl, HapticManager
domain/ ← Models (SensorType, SensorReading), Repository interface, UseCases
presentation/ ← Screens, ViewModels, Navigation, Theme
di/ ← Hilt modules (SensorModule, DatabaseModule)
```

---

## Sensors (10)
Accelerometer, Gyroscope, Linear Acceleration, Magnetometer, Gravity, Rotation Vector, Light, Proximity, Barometer, Step Counter.

| Sensor | Type | Category | Axes | Units |
|--------|------|----------|------|-------|
| Accelerometer | Hardware | Motion | 3 | m/s² |
| Gyroscope | Hardware | Motion | 3 | °/s |
| Linear Acceleration | Virtual | Motion | 3 | m/s² |
| Magnetometer | Hardware | Position | 3 | µT |
| Gravity | Virtual | Position | 3 | m/s² |
| Rotation Vector | Virtual | Position | 3 | — |
| Light | Hardware | Environmental | 1 | lx |
| Proximity | Hardware | Environmental | 1 | binary (OBSTRUCTED/CLEAR) |
| Barometer | Hardware | Environmental | 1 | hPa |
| Step Counter | Hardware | Activity | 1 | steps |

---

## Screens
- **Dashboard** — 2-col grid grouped by category (Motion/Position/Environmental/Activity). Compass card at top.
- **Compass** — tilt-compensated heading via accel+mag fusion with Canvas-drawn rose.
- **Sensor Detail** — live values, Canvas chart (60 readings), logging toggle, CSV export.
- **History** — session list with search, sort, clear.
- **Settings** — polling rate (FASTEST/GAME/UI/NORMAL), DB stats, credits.

### Dashboard
- 2-column `LazyVerticalGrid`, grouped by `SensorCategory`
- `CompassDashboardCard` at top spanning full width
- Each `SensorGridItem` shows icon, name, availability
- Unavailable sensors show `ModalBottomSheet` on tap
- Permission dialog for `ACTIVITY_RECOGNITION` (step counter)
- **No real-time observation** (removed for perf — was causing lag)

### Compass
- Rotating dial (iPhone-style), fixed red indicator at top
- Degree labels (30/60/120/150/…) drawn radially around dial
- Tilt-compensated heading via `SensorManager.getRotationMatrix()`
- Small disclaimer: "Accuracy depends on device calibration"
- Dark canvas, no Card/LIVE indicator

### Sensor Detail Screens (10 individual files)
Each sensor has its own screen composable in `presentation/detail/`:

| File | Sensor | Unique UI |
|------|--------|-----------|
| `AccelerometerScreen.kt` | Accelerometer | X/Y/Z values + chart |
| `GyroscopeScreen.kt` | Gyroscope | X/Y/Z values + **3D cube** + chart |
| `LinearAccelerationScreen.kt` | Linear Acceleration | X/Y/Z values + chart |
| `MagnetometerScreen.kt` | Magnetometer | X/Y/Z values + chart |
| `GravityScreen.kt` | Gravity | X/Y/Z values + chart |
| `RotationVectorScreen.kt` | Rotation Vector | X/Y/Z values + chart |
| `LightScreen.kt` | Light | Brightness level card (DARK→SUNLIGHT) + gradient bar + chart |
| `ProximityScreen.kt` | Proximity | OBSTRUCTED/CLEAR card + chart |
| `PressureScreen.kt` | Barometer | Weather condition card (STORM/RAIN/NORMAL/HIGH) + chart |
| `StepCounterScreen.kt` | Step Counter | Step count + chart |

All screens share:
- `SensorDetailScaffold` — top bar, lifecycle binding, bottom bar with **Logging toggle** + **CSV Export**
- `LiveLineChart` — Canvas line chart (60 readings), touch crosshair with tooltip
- `LiveIndicator` — animated pulsing green dot
- `SensorUsageHint` — contextual usage tip per sensor type
- `SensorDetailScaffold` applies content padding (fixed: was hiding behind toolbar)

### History
- Session list from Room DB, search bar, sort toggle (newest/oldest)
- Clear all button with confirmation

### Settings
- **Polling Rate** — RadioGroup: FASTEST / GAME / UI / NORMAL
- **Haptic Feedback** — Switch toggle (on by default), controls vibration for proximity/step/gyro
- **Database Stats** — total row count
- **Credits** — developer info

---

## Key Shared Components (`presentation/detail/`)

| File | What it exports |
|------|-----------------|
| `SensorDetailScaffold.kt` | `SensorDetailScaffold()` — shared scaffold + lifecycle + CSV export |
| `SensorUtils.kt` | `LiveIndicator()`, `SensorUsageHint()`, `formatLargeValue()`, `formatDetailValue()` |
| `SensorChart.kt` | `LiveLineChart()` — Canvas chart with touch crosshair |
| `SensorDisplayCard.kt` | `LiveValueDisplay()`, `AxisValue()`, `PressureDisplay()`, `LightDisplay()`, `ProximityDisplay()` |
| `GyroscopeCube.kt` | `GyroscopeCube()` — 3D rotating cube on Canvas |
| `DetailViewModel.kt` | `DetailViewModel` — sensor observation, 60-reading buffer, logging, **haptic triggers** |

---

## Haptic Feedback (#14)

Implemented in `DetailViewModel.checkHapticTriggers()`:

| Sensor | Trigger | Haptic |
|--------|---------|--------|
| Proximity | OBSTRUCTED ↔ CLEAR transition | `doubleTick()` |
| Step Counter | Step count increments | `tick()` |
| Gyroscope | Rotation magnitude > 5 rad/s | `tick()` |

`HapticManager` (`data/sensor/HapticManager.kt`) wraps `Vibrator`/`VibratorManager`, compatible with API 24+. Toggle in Settings (stored in-memory via `@Volatile` in `SensorRepositoryImpl`).

**Requires `android.permission.VIBRATE`** in manifest (normal permission, granted at install time).

---

## Navigation

`Routes.kt` — sealed `Route` class with per-sensor routes:
```
dashboard, history, settings, compass
sensor/accelerometer, sensor/gyroscope, sensor/linear_acceleration, sensor/magnetometer
sensor/gravity, sensor/rotation_vector, sensor/light, sensor/proximity
sensor/pressure, sensor/step_counter
```

`SensorType.toRoute()` extension maps each enum to its route string. `AppNavGraph.kt` registers individual composable destinations. No nav arguments needed — each screen passes sensor type directly to `SensorDetailScaffold`.

---

## Data Flow

```
SensorManager callbackFlow
→ SensorDataSource.observeSensor(type, delay)
→ SensorRepositoryImpl.observeSensor(type)
→ ObserveSensorUseCase(sensorType)
→ DetailViewModel (collect → buffer 60 → check haptics)
→ Screen composable (collectAsStateWithLifecycle)
```

Polling delay from `SensorRepositoryImpl.currentDelay` (`@Volatile`, defaults to `SENSOR_DELAY_UI`). Haptic toggle from `SensorRepositoryImpl.hapticEnabled`.

---

## Key Patterns
- Sensor observation via `callbackFlow` in `SensorDataSource`, scaled by polling delay from Settings.
- `combine` in CompassViewModel fuses accel+mag flows using `SensorManager.getRotationMatrix()`.
- Navigation via sealed `Route` class in `Routes.kt`.
- Previews use extracted `*ScreenContent` composables with mock data (no Hilt).
- Dark theme only.
- Sensor observation via `callbackFlow` in `SensorDataSource`
- DI via Hilt (`@HiltViewModel`, `@Inject`, `@Module`)
- Dark theme only (no light/auto switch yet)
- Previews use `*Content` composables with mock data (no Hilt)
- No DataStore/SharedPreferences — all settings are in-memory `@Volatile` vars (lost on app restart)
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)
}
Loading
Loading