5 Commits

Author SHA1 Message Date
Kara Zajac f4635967c7 Use Radar Eye logo in the app header + README
Build APK / build (push) Has been cancelled
- App: top-bar brand mark is now the Radar Eye (drawable/vigil_logo.xml) instead of a generic shield.
- README: centered logo header (docs/logo.png, rendered from docs/logo.svg).
Release 0.1.7.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
2026-07-15 18:03:10 -04:00
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
14 changed files with 136 additions and 12 deletions
+6
View File
@@ -1,7 +1,13 @@
<div align="center">
<img src="docs/logo.png" alt="VIGIL" width="96" height="96" />
# VIGIL # VIGIL
**What's been following you?** **What's been following you?**
</div>
A native Android (Kotlin) app that watches for personal item-trackers — Apple A native Android (Kotlin) app that watches for personal item-trackers — Apple
AirTags, Tile, Samsung Galaxy SmartTags, and Google Find My Device / DULT tags — AirTags, Tile, Samsung Galaxy SmartTags, and Google Find My Device / DULT tags —
that are **travelling with you over time**. It is the temporal counterpart to that are **travelling with you over time**. It is the temporal counterpart to
+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 = 8
versionName = "0.1.2" versionName = "0.1.7"
} }
// 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
@@ -8,7 +9,9 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.res.painterResource
import org.soulstone.vigil.service.ScanService import org.soulstone.vigil.service.ScanService
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -27,6 +30,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 +45,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
@@ -67,6 +72,7 @@ import org.soulstone.vigil.BuildConfig
import org.soulstone.vigil.data.db.TrackerEntity import org.soulstone.vigil.data.db.TrackerEntity
import org.soulstone.vigil.model.Sensitivity import org.soulstone.vigil.model.Sensitivity
import org.soulstone.vigil.model.TrackerEcosystem import org.soulstone.vigil.model.TrackerEcosystem
import org.soulstone.vigil.R
import org.soulstone.vigil.model.TrackerStatus import org.soulstone.vigil.model.TrackerStatus
import org.soulstone.vigil.ui.theme.VigilGreen import org.soulstone.vigil.ui.theme.VigilGreen
import org.soulstone.vigil.ui.theme.VigilPeach import org.soulstone.vigil.ui.theme.VigilPeach
@@ -83,7 +89,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) }
@@ -102,12 +109,25 @@ fun MainScreen(
TopAppBar( TopAppBar(
title = { title = {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.Shield, null, tint = MaterialTheme.colorScheme.primary) Image(
painterResource(R.drawable.vigil_logo),
contentDescription = null,
modifier = Modifier.size(26.dp)
)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
Text("VIGIL", fontWeight = FontWeight.Bold) Text("VIGIL", fontWeight = FontWeight.Bold)
} }
}, },
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 +452,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>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- VIGIL wordmark glyph (Radar Eye), tight-cropped for in-app use (top bar). -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M4,24 Q24,8 44,24 Q24,40 4,24 Z"
android:strokeColor="#89B4FA"
android:strokeWidth="3"
android:strokeLineJoin="round"
android:fillColor="#00000000" />
<path
android:pathData="M24,24 m-7.5,0 a7.5,7.5 0 1,0 15,0 a7.5,7.5 0 1,0 -15,0"
android:strokeColor="#89B4FA"
android:strokeWidth="2.4"
android:fillColor="#00000000" />
<path
android:pathData="M24,24 m-4.4,0 a4.4,4.4 0 1,0 8.8,0 a4.4,4.4 0 1,0 -8.8,0"
android:strokeColor="#B4BEFE"
android:strokeWidth="2"
android:fillColor="#00000000" />
<path
android:pathData="M24,24 m-2.2,0 a2.2,2.2 0 1,0 4.4,0 a2.2,2.2 0 1,0 -4.4,0"
android:fillColor="#A6E3A1" />
</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>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108 108" width="108" height="108" role="img" aria-label="VIGIL">
<defs>
<radialGradient id="bg" cx="50%" cy="36%" r="78%">
<stop offset="0" stop-color="#2a2a40"/>
<stop offset="1" stop-color="#11111b"/>
</radialGradient>
</defs>
<rect x="4" y="4" width="100" height="100" rx="26" fill="url(#bg)" stroke="#313244"/>
<path d="M26 54 Q54 30 82 54 Q54 78 26 54 Z" fill="none" stroke="#89b4fa" stroke-width="4.5" stroke-linejoin="round"/>
<circle cx="54" cy="54" r="11" fill="none" stroke="#89b4fa" stroke-width="3.2"/>
<circle cx="54" cy="54" r="6.5" fill="none" stroke="#b4befe" stroke-width="2.8"/>
<circle cx="54" cy="54" r="3.2" fill="#a6e3a1"/>
</svg>

After

Width:  |  Height:  |  Size: 740 B