Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f4635967c7 | |||
| 41ab2f21a4 | |||
| 333dd291cb | |||
| bf15ead1ef | |||
| 6c460cbff7 |
@@ -1,7 +1,13 @@
|
||||
<div align="center">
|
||||
|
||||
<img src="docs/logo.png" alt="VIGIL" width="96" height="96" />
|
||||
|
||||
# VIGIL
|
||||
|
||||
**What's been following you?**
|
||||
|
||||
</div>
|
||||
|
||||
A native Android (Kotlin) app that watches for personal item-trackers — Apple
|
||||
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
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId = "org.soulstone.vigil"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 3
|
||||
versionName = "0.1.2"
|
||||
versionCode = 8
|
||||
versionName = "0.1.7"
|
||||
}
|
||||
|
||||
// Fixed debug keystore committed to the repo (a debug key is non-secret — its
|
||||
|
||||
@@ -95,7 +95,8 @@ class MainActivity : ComponentActivity() {
|
||||
onDistrust = { 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)
|
||||
}
|
||||
|
||||
/** 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
|
||||
* 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.
|
||||
@Query("DELETE FROM trackers WHERE lastSeen < :cutoff AND approved = 0")
|
||||
suspend fun pruneStale(cutoff: Long)
|
||||
|
||||
@Query("DELETE FROM trackers")
|
||||
suspend fun clearAll()
|
||||
}
|
||||
|
||||
@Dao
|
||||
@@ -103,6 +106,9 @@ interface SightingDao {
|
||||
|
||||
@Query("DELETE FROM sightings WHERE timestamp < :cutoff")
|
||||
suspend fun prune(cutoff: Long)
|
||||
|
||||
@Query("DELETE FROM sightings")
|
||||
suspend fun clearAll()
|
||||
}
|
||||
|
||||
@Dao
|
||||
|
||||
@@ -148,7 +148,8 @@ object TrackerRinger {
|
||||
done("Ringing… listen for the AirTag.", g)
|
||||
} else {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.soulstone.vigil.ui
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.icons.filled.NotificationsActive
|
||||
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.collectAsState
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import org.soulstone.vigil.service.ScanService
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.filled.Bluetooth
|
||||
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.Lock
|
||||
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.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
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.model.Sensitivity
|
||||
import org.soulstone.vigil.model.TrackerEcosystem
|
||||
import org.soulstone.vigil.R
|
||||
import org.soulstone.vigil.model.TrackerStatus
|
||||
import org.soulstone.vigil.ui.theme.VigilGreen
|
||||
import org.soulstone.vigil.ui.theme.VigilPeach
|
||||
@@ -83,7 +89,8 @@ fun MainScreen(
|
||||
onSetSensitivity: (Sensitivity) -> Unit,
|
||||
onApprove: (String, Boolean) -> Unit,
|
||||
onDistrust: (String) -> Unit,
|
||||
onRing: (TrackerEntity) -> Unit
|
||||
onRing: (TrackerEntity) -> Unit,
|
||||
onClearAll: () -> Unit
|
||||
) {
|
||||
var detail by remember { mutableStateOf<TrackerEntity?>(null) }
|
||||
var finding by remember { mutableStateOf<TrackerEntity?>(null) }
|
||||
@@ -102,12 +109,25 @@ fun MainScreen(
|
||||
TopAppBar(
|
||||
title = {
|
||||
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))
|
||||
Text("VIGIL", fontWeight = FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
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)) {
|
||||
Icon(
|
||||
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. */
|
||||
@Composable
|
||||
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 smoothed = remember { mutableStateOf(-100f) }
|
||||
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"?>
|
||||
<!-- 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"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<!-- eye outline -->
|
||||
<path
|
||||
android:fillColor="#1FAA59"
|
||||
android:pathData="M54,30 m-18,0 a18,18 0 1,0 36,0 a18,18 0 1,0 -36,0" />
|
||||
android:pathData="M26,54 Q54,30 82,54 Q54,78 26,54 Z"
|
||||
android:strokeColor="#89B4FA"
|
||||
android:strokeWidth="4.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:fillColor="#00000000" />
|
||||
<!-- radar iris — outer ring -->
|
||||
<path
|
||||
android:fillColor="#F4F6FA"
|
||||
android:pathData="M54,30 m-6,0 a6,6 0 1,0 12,0 a6,6 0 1,0 -12,0" />
|
||||
android:pathData="M54,54 m-11,0 a11,11 0 1,0 22,0 a11,11 0 1,0 -22,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>
|
||||
|
||||
@@ -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"?>
|
||||
<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" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
@@ -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 |
Reference in New Issue
Block a user