VIGIL 0.1.0 — temporal counter-tracking (prototype)
Android app that detects personal item-trackers (AirTag, Tile, Samsung SmartTag, Google Find My Device / DULT) following the user over time. Sibling to OVERWATCH: OVERWATCH is spatial, VIGIL is temporal. - BLE scan + per-ecosystem wire-format parsing (Apple 0x004C/0x12, FMDN 0xFEAA, Samsung 0xFD5A, Tile 0xFEED/FEEC, DULT 0xFCB2) - Room temporal store (14-day sightings), co-movement evaluator with RSSI proximity gate - Allowlist + learned offline baseline (auto-trust household tags) - Foreground service, Compose UI (Catppuccin Mocha) - Privacy: NO INTERNET permission — fully on-device - Reuses OVERWATCH Gradle setup + committed debug keystore - docs/detection-rotation-clone.md: design for the rotation-clone presence engine (#1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0187UiEtasyowhEBYs6s9iF5
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package org.soulstone.vigil
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings as AndroidSettings
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.soulstone.vigil.data.TrackerRepository
|
||||
import org.soulstone.vigil.data.db.VigilDatabase
|
||||
import org.soulstone.vigil.data.settings.Settings
|
||||
import org.soulstone.vigil.service.ScanService
|
||||
import org.soulstone.vigil.ui.MainScreen
|
||||
import org.soulstone.vigil.ui.theme.VigilTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private lateinit var settings: Settings
|
||||
private lateinit var repo: TrackerRepository
|
||||
private val permissionsGranted = mutableStateOf(false)
|
||||
|
||||
private val requiredPermissions: Array<String>
|
||||
get() = buildList {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
add(Manifest.permission.BLUETOOTH)
|
||||
add(Manifest.permission.BLUETOOTH_ADMIN)
|
||||
}
|
||||
add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
add(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
// NOTE: ACCESS_BACKGROUND_LOCATION ("Allow all the time") must be
|
||||
// granted separately from app settings; scanning still runs via the
|
||||
// foreground service while the app is open. Prompt for it later.
|
||||
}.toTypedArray()
|
||||
|
||||
private val permissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { result ->
|
||||
val granted = result.all { it.value }
|
||||
permissionsGranted.value = granted
|
||||
if (granted) ScanService.start(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
settings = Settings.get(this)
|
||||
repo = TrackerRepository(VigilDatabase.get(this))
|
||||
permissionsGranted.value = hasAllPermissions()
|
||||
|
||||
setContent {
|
||||
VigilTheme {
|
||||
val running by ScanService.running.collectAsState()
|
||||
val trackers by repo.observeTrackers().collectAsState(initial = emptyList())
|
||||
val sensitivity by settings.sensitivity.collectAsState()
|
||||
val granted by permissionsGranted
|
||||
|
||||
MainScreen(
|
||||
running = running,
|
||||
trackers = trackers,
|
||||
sensitivity = sensitivity,
|
||||
permissionMessage = if (granted) null
|
||||
else "Grant Bluetooth + location to start watching",
|
||||
onStartStop = {
|
||||
if (running) {
|
||||
ScanService.stop(this)
|
||||
} else if (granted) {
|
||||
ScanService.start(this)
|
||||
} else {
|
||||
permissionLauncher.launch(requiredPermissions)
|
||||
}
|
||||
},
|
||||
onSetSensitivity = { settings.setSensitivity(it) },
|
||||
onApprove = { id, approved ->
|
||||
lifecycleScope.launch { repo.setApproved(id, approved) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
permissionsGranted.value = hasAllPermissions()
|
||||
}
|
||||
|
||||
private fun hasAllPermissions(): Boolean = requiredPermissions.all {
|
||||
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
private fun openAppSettings() {
|
||||
startActivity(
|
||||
Intent(
|
||||
AndroidSettings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", packageName, null)
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user