4 Commits

Author SHA1 Message Date
Kara Zajac 41ab2f21a4 New app icon: Radar Eye (adaptive + themed monochrome)
Build APK / build (push) Has been cancelled
Replaces the inherited OVERWATCH shield with VIGIL's own mark: a watchful eye with a radar-target iris and a green centre contact, in Catppuccin Mocha. Foreground vector (safe-zone sized) + radial-gradient background layer + monochrome layer for Android 13+ themed icons. Release 0.1.6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 17:56:00 -04:00
Kara Zajac 333dd291cb Honest ring feedback (write-ACK != actually ringing)
Build APK / build (push) Has been cancelled
The DULT/FMDN path reported 'Ringing…' off the GATT write acknowledgement, not the tag's real response — so a tag that ACKs but declines to ring (e.g. because it's not separated from its owner) still showed success. Reworded to 'Ring command sent — a tag only chirps when separated from its owner.' Release 0.1.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 13:22:32 -04:00
Kara Zajac bf15ead1ef Add 'Clear all' button to wipe the tracker list
Build APK / build (push) Has been cancelled
Trash action in the top bar (shown when the list is non-empty) clears all tracker rows + sighting history via repo.clearAll(). Devices re-populate as they're re-detected; learned baseline places are kept. Release 0.1.4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 12:42:06 -04:00
Kara Zajac 6c460cbff7 Fix: back button on finder screen exits app instead of closing finder
Build APK / build (push) Has been cancelled
The hot/cold FinderScreen is a full-screen overlay with no BackHandler, so system-back fell through to the Activity and finished it. Added BackHandler(onBack = onClose) so back closes the finder and returns to the main list. Release 0.1.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 10:02:55 -04:00
10 changed files with 82 additions and 11 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId = "org.soulstone.vigil" applicationId = "org.soulstone.vigil"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 3 versionCode = 7
versionName = "0.1.2" versionName = "0.1.6"
} }
// Fixed debug keystore committed to the repo (a debug key is non-secret — its // Fixed debug keystore committed to the repo (a debug key is non-secret — its
@@ -95,7 +95,8 @@ class MainActivity : ComponentActivity() {
onDistrust = { id -> onDistrust = { id ->
lifecycleScope.launch { repo.clearBaseline(id) } lifecycleScope.launch { repo.clearBaseline(id) }
}, },
onRing = { tracker -> ringTracker(tracker) } onRing = { tracker -> ringTracker(tracker) },
onClearAll = { lifecycleScope.launch { repo.clearAll() } }
) )
} }
} }
@@ -44,6 +44,13 @@ class TrackerRepository(private val db: VigilDatabase) {
db.trackerDao().pruneStale(cutoff) db.trackerDao().pruneStale(cutoff)
} }
/** Wipe the whole tracker list + sighting history (they re-populate as devices
* are re-detected). Keeps learned baseline places. */
suspend fun clearAll() = mutex.withLock {
db.trackerDao().clearAll()
db.sightingDao().clearAll()
}
/** /**
* Ingest one sighting. Persists it, updates the baseline, and re-evaluates the * Ingest one sighting. Persists it, updates the baseline, and re-evaluates the
* tracker's risk. Returns the updated row and whether this crossed into ALERTING. * tracker's risk. Returns the updated row and whether this crossed into ALERTING.
@@ -91,6 +91,9 @@ interface TrackerDao {
// stale, non-approved rows so the table and the UI list don't grow without bound. // stale, non-approved rows so the table and the UI list don't grow without bound.
@Query("DELETE FROM trackers WHERE lastSeen < :cutoff AND approved = 0") @Query("DELETE FROM trackers WHERE lastSeen < :cutoff AND approved = 0")
suspend fun pruneStale(cutoff: Long) suspend fun pruneStale(cutoff: Long)
@Query("DELETE FROM trackers")
suspend fun clearAll()
} }
@Dao @Dao
@@ -103,6 +106,9 @@ interface SightingDao {
@Query("DELETE FROM sightings WHERE timestamp < :cutoff") @Query("DELETE FROM sightings WHERE timestamp < :cutoff")
suspend fun prune(cutoff: Long) suspend fun prune(cutoff: Long)
@Query("DELETE FROM sightings")
suspend fun clearAll()
} }
@Dao @Dao
@@ -148,7 +148,8 @@ object TrackerRinger {
done("Ringing… listen for the AirTag.", g) done("Ringing… listen for the AirTag.", g)
} else { } else {
done( done(
if (status == BluetoothGatt.GATT_SUCCESS) "Ringing… listen for the tracker." if (status == BluetoothGatt.GATT_SUCCESS)
"Ring command sent. A tag only chirps when it's separated from its owner — your own tag that's with you won't."
else "The tracker refused the ring command.", g else "The tracker refused the ring command.", g
) )
} }
@@ -1,6 +1,7 @@
package org.soulstone.vigil.ui package org.soulstone.vigil.ui
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.filled.NotificationsActive import androidx.compose.material.icons.filled.NotificationsActive
import androidx.compose.material.icons.filled.Sensors import androidx.compose.material.icons.filled.Sensors
@@ -27,6 +28,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Bluetooth
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material.icons.filled.GppMaybe import androidx.compose.material.icons.filled.GppMaybe
import androidx.compose.material.icons.filled.Lock import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Radar import androidx.compose.material.icons.filled.Radar
@@ -41,6 +43,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -83,7 +86,8 @@ fun MainScreen(
onSetSensitivity: (Sensitivity) -> Unit, onSetSensitivity: (Sensitivity) -> Unit,
onApprove: (String, Boolean) -> Unit, onApprove: (String, Boolean) -> Unit,
onDistrust: (String) -> Unit, onDistrust: (String) -> Unit,
onRing: (TrackerEntity) -> Unit onRing: (TrackerEntity) -> Unit,
onClearAll: () -> Unit
) { ) {
var detail by remember { mutableStateOf<TrackerEntity?>(null) } var detail by remember { mutableStateOf<TrackerEntity?>(null) }
var finding by remember { mutableStateOf<TrackerEntity?>(null) } var finding by remember { mutableStateOf<TrackerEntity?>(null) }
@@ -108,6 +112,15 @@ fun MainScreen(
} }
}, },
actions = { actions = {
if (trackers.isNotEmpty()) {
IconButton(onClick = onClearAll) {
Icon(
Icons.Filled.DeleteSweep,
contentDescription = "Clear all trackers",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp)) { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(end = 12.dp)) {
Icon( Icon(
Icons.Filled.Lock, null, Icons.Filled.Lock, null,
@@ -432,6 +445,8 @@ private fun DetailRow(label: String, value: String) {
* proximity meter. Works even on silent/modified tags that refuse to ring. */ * proximity meter. Works even on silent/modified tags that refuse to ring. */
@Composable @Composable
private fun FinderScreen(t: TrackerEntity, onRing: (TrackerEntity) -> Unit, onClose: () -> Unit) { private fun FinderScreen(t: TrackerEntity, onRing: (TrackerEntity) -> Unit, onClose: () -> Unit) {
// Intercept system back so it closes the finder instead of exiting the app.
BackHandler(onBack = onClose)
val rssi by ScanService.finderRssi.collectAsState() val rssi by ScanService.finderRssi.collectAsState()
val smoothed = remember { mutableStateOf(-100f) } val smoothed = remember { mutableStateOf(-100f) }
LaunchedEffect(rssi) { rssi?.let { smoothed.value = 0.35f * it + 0.65f * smoothed.value } } LaunchedEffect(rssi) { rssi?.let { smoothed.value = 0.35f * it + 0.65f * smoothed.value } }
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Mocha radial ground: a subtle lift toward the top-centre, falling to crust. -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M0,0h108v108h-108z">
<aapt:attr name="android:fillColor">
<gradient
android:type="radial"
android:centerX="54"
android:centerY="42"
android:gradientRadius="82"
android:startColor="#2A2A40"
android:endColor="#11111B" />
</aapt:attr>
</path>
</vector>
@@ -1,13 +1,32 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- VIGIL "Radar Eye" — a watchful eye whose iris is a radar target. Content sits
within the adaptive-icon safe zone (centre ~66dp of the 108dp canvas). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="108"
android:viewportHeight="108"> android:viewportHeight="108">
<!-- eye outline -->
<path <path
android:fillColor="#1FAA59" android:pathData="M26,54 Q54,30 82,54 Q54,78 26,54 Z"
android:pathData="M54,30 m-18,0 a18,18 0 1,0 36,0 a18,18 0 1,0 -36,0" /> android:strokeColor="#89B4FA"
android:strokeWidth="4.5"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<!-- radar iris — outer ring -->
<path <path
android:fillColor="#F4F6FA" android:pathData="M54,54 m-11,0 a11,11 0 1,0 22,0 a11,11 0 1,0 -22,0"
android:pathData="M54,30 m-6,0 a6,6 0 1,0 12,0 a6,6 0 1,0 -12,0" /> android:strokeColor="#89B4FA"
android:strokeWidth="3.2"
android:fillColor="#00000000" />
<!-- radar iris — inner ring -->
<path
android:pathData="M54,54 m-6.5,0 a6.5,6.5 0 1,0 13,0 a6.5,6.5 0 1,0 -13,0"
android:strokeColor="#B4BEFE"
android:strokeWidth="2.8"
android:fillColor="#00000000" />
<!-- centre contact (the detected device) -->
<path
android:pathData="M54,54 m-3.2,0 a3.2,3.2 0 1,0 6.4,0 a3.2,3.2 0 1,0 -6.4,0"
android:fillColor="#A6E3A1" />
</vector> </vector>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/bg_dark" /> <background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon> </adaptive-icon>