Skip to content

Commit cc0b138

Browse files
committed
bettter
1 parent 1755bf5 commit cc0b138

13 files changed

Lines changed: 2143 additions & 116 deletions

File tree

OEM_INTEGRATION.md

Lines changed: 469 additions & 44 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,36 @@ cd Flock-You-Android
474474
# APK: app/build/outputs/apk/debug/app-debug.apk
475475
```
476476

477+
### OEM / System App Installation
478+
479+
For GrapheneOS, CalyxOS, LineageOS, or other AOSP-based ROM integration, see the [OEM Integration Guide](OEM_INTEGRATION.md).
480+
481+
**Quick integration for GrapheneOS:**
482+
```bash
483+
# Automated integration with platform signing (OEM mode)
484+
./system/integrate-grapheneos.sh ~/grapheneos
485+
486+
# Or with pre-signed APK (System mode)
487+
./system/integrate-grapheneos.sh ~/grapheneos presigned
488+
```
489+
490+
**Build variants:**
491+
| Variant | Command | Privileges |
492+
|---------|---------|------------|
493+
| Sideload | `./gradlew assembleSideloadRelease` | Standard user app |
494+
| System | `./gradlew assembleSystemRelease` | Privileged system app |
495+
| OEM | `./gradlew assembleOemRelease` | Platform-signed, maximum privileges |
496+
497+
**Integration files in `system/`:**
498+
| File | Purpose |
499+
|------|---------|
500+
| `Android.bp` | Soong build module (Android 11+) |
501+
| `Android.mk` | Legacy Make build module |
502+
| `flockyou.mk` | Device makefile include |
503+
| `integrate-grapheneos.sh` | Automated integration script |
504+
| `privapp-permissions-flockyou.xml` | Privileged permissions whitelist |
505+
| `default-permissions-flockyou.xml` | Runtime permissions pre-grant |
506+
477507
### Verify APK Attestation
478508
All release APKs include [SLSA Build Provenance](https://slsa.dev/spec/v1.0/provenance) attestation. Verify authenticity with:
479509
```bash
@@ -673,6 +703,36 @@ For optimal detection coverage:
673703

674704
The app uses a foreground service with wake locks to maintain reliable scanning. This is necessary for consistent detection but comes at a battery cost.
675705

706+
## 🔐 Data Collection & Privacy
707+
708+
> **To detect if you're being surveilled, this app must surveil you first.**
709+
710+
This app collects and stores locally:
711+
- **Location data** attached to every detection event
712+
- **Cell tower history** with timestamps and coordinates
713+
- **WiFi network profiles** with location mapping
714+
- **Bluetooth device records** with signal data
715+
- **Ultrasonic events** and RF environment data
716+
717+
**Database encryption**: SQLCipher with AES-256-GCM, key in Android Keystore.
718+
719+
**What encryption protects against**:
720+
- Casual device theft
721+
- Locked device extraction (mostly)
722+
723+
**What encryption does NOT protect against**:
724+
- Unlocked device forensics (Cellebrite, GrayKey)
725+
- Compelled device unlock
726+
- Malware with app-level access
727+
728+
**Recommendations**:
729+
- Use shortest retention period acceptable (Settings > Data Retention)
730+
- Clear data before high-risk situations (border crossings, protests)
731+
- Consider disabling features you don't need
732+
- For OEM deployments, see [detailed security analysis](OEM_INTEGRATION.md#data-collection--the-surveillance-paradox)
733+
734+
**TPM/StrongBox binding** would provide marginal improvement for locked-device attacks but doesn't help when the device is unlocked (which is when the app needs to function). See [OEM Integration Guide](OEM_INTEGRATION.md#would-tpm-bound-secrets-help) for detailed analysis.
735+
676736
## ⚖️ Legal Notice
677737

678738
**For educational and research purposes only.**

app/src/main/java/com/flockyou/MainActivity.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import com.flockyou.ui.screens.NotificationSettingsScreen
4646
import com.flockyou.ui.screens.RuleSettingsScreen
4747
import com.flockyou.ui.screens.DetectionSettingsScreen
4848
import com.flockyou.ui.screens.SecuritySettingsScreen
49+
import com.flockyou.ui.screens.PermissionSetupWizard
4950
import com.flockyou.ui.theme.FlockYouTheme
5051
import dagger.hilt.android.AndroidEntryPoint
5152
import kotlinx.coroutines.launch
@@ -73,6 +74,7 @@ class MainActivity : FragmentActivity() {
7374
add(Manifest.permission.ACCESS_FINE_LOCATION)
7475
add(Manifest.permission.ACCESS_COARSE_LOCATION)
7576
add(Manifest.permission.READ_PHONE_STATE)
77+
add(Manifest.permission.RECORD_AUDIO) // For ultrasonic beacon detection
7678
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
7779
add(Manifest.permission.BLUETOOTH_SCAN)
7880
add(Manifest.permission.BLUETOOTH_CONNECT)
@@ -149,8 +151,9 @@ class MainActivity : FragmentActivity() {
149151
) {
150152
when {
151153
!permissionsGranted -> {
152-
PermissionScreen(
153-
onRequestPermissions = { requestPermissions() }
154+
PermissionSetupWizard(
155+
onRequestPermissions = { requestPermissions() },
156+
onRequestBackgroundLocation = { requestBackgroundLocation() }
154157
)
155158
}
156159
!batteryOptimizationChecked -> {
@@ -212,6 +215,12 @@ class MainActivity : FragmentActivity() {
212215
private fun requestPermissions() {
213216
permissionLauncher.launch(requiredPermissions.toTypedArray())
214217
}
218+
219+
private fun requestBackgroundLocation() {
220+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
221+
permissionLauncher.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION))
222+
}
223+
}
215224
}
216225

217226
@Composable

app/src/main/java/com/flockyou/data/repository/Database.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,10 @@ interface DetectionDao {
8585

8686
@Query("SELECT COUNT(*) FROM detections")
8787
suspend fun getTotalDetectionCountSync(): Int
88-
88+
89+
@Query("SELECT * FROM detections ORDER BY lastSeenTimestamp DESC")
90+
suspend fun getAllDetectionsSnapshot(): List<Detection>
91+
8992
@Insert(onConflict = OnConflictStrategy.REPLACE)
9093
suspend fun insertDetection(detection: Detection)
9194

app/src/main/java/com/flockyou/data/repository/DetectionRepository.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ class DetectionRepository @Inject constructor(
4545
suspend fun getTotalDetectionCount(): Int {
4646
return detectionDao.getTotalDetectionCountSync()
4747
}
48-
48+
49+
suspend fun getAllDetectionsSnapshot(): List<Detection> {
50+
return detectionDao.getAllDetectionsSnapshot()
51+
}
52+
4953
suspend fun insertDetection(detection: Detection) {
5054
detectionDao.insertDetection(detection)
5155
}

app/src/main/java/com/flockyou/service/ScanningService.kt

Lines changed: 107 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ import com.google.android.gms.location.*
2727
import com.google.gson.Gson
2828
import dagger.hilt.android.AndroidEntryPoint
2929
import kotlinx.coroutines.*
30+
import kotlinx.coroutines.flow.MutableSharedFlow
3031
import kotlinx.coroutines.flow.MutableStateFlow
32+
import kotlinx.coroutines.flow.SharedFlow
3133
import kotlinx.coroutines.flow.StateFlow
34+
import kotlinx.coroutines.flow.asSharedFlow
3235
import java.util.*
3336
import javax.inject.Inject
3437

@@ -106,6 +109,11 @@ class ScanningService : Service() {
106109
// Scan statistics
107110
val scanStats = MutableStateFlow(ScanStatistics())
108111

112+
// Detection refresh event - emits when detections are added/updated
113+
// This ensures UI updates even if Room's Flow emissions fail with SQLCipher
114+
private val _detectionRefreshEvent = MutableSharedFlow<Unit>(replay = 0, extraBufferCapacity = 1)
115+
val detectionRefreshEvent: SharedFlow<Unit> = _detectionRefreshEvent.asSharedFlow()
116+
109117
// Learning mode - for capturing unknown device signatures
110118
val learningModeEnabled = MutableStateFlow(false)
111119
val learnedSignatures = MutableStateFlow<List<LearnedSignature>>(emptyList())
@@ -834,19 +842,24 @@ class ScanningService : Service() {
834842
// Check if we already have this detection (use unique SSID)
835843
val existing = det.ssid?.let { repository.getDetectionBySsid(it) }
836844
if (existing == null) {
837-
repository.insertDetection(det)
838-
839-
// Alert and vibrate for high-severity anomalies
840-
if (anomaly.severity == ThreatLevel.CRITICAL ||
841-
anomaly.severity == ThreatLevel.HIGH) {
842-
alertUser(det)
843-
}
844-
845-
lastDetection.value = det
846-
detectionCount.value = repository.getTotalDetectionCount()
847-
848-
if (BuildConfig.DEBUG) {
849-
Log.w(TAG, "CELLULAR ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
845+
try {
846+
repository.insertDetection(det)
847+
848+
// Alert and vibrate for high-severity anomalies
849+
if (anomaly.severity == ThreatLevel.CRITICAL ||
850+
anomaly.severity == ThreatLevel.HIGH) {
851+
alertUser(det)
852+
}
853+
854+
lastDetection.value = det
855+
detectionCount.value = repository.getTotalDetectionCount()
856+
_detectionRefreshEvent.tryEmit(Unit)
857+
858+
if (BuildConfig.DEBUG) {
859+
Log.w(TAG, "CELLULAR ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
860+
}
861+
} catch (e: Exception) {
862+
Log.e(TAG, "Error saving cellular detection: ${e.message}", e)
850863
}
851864
}
852865
}
@@ -1051,18 +1064,23 @@ class ScanningService : Service() {
10511064
?: det.ssid?.let { repository.getDetectionBySsid(it) }
10521065

10531066
if (existing == null) {
1054-
repository.insertDetection(det)
1055-
1056-
if (anomaly.severity == ThreatLevel.CRITICAL ||
1057-
anomaly.severity == ThreatLevel.HIGH) {
1058-
alertUser(det)
1059-
}
1060-
1061-
lastDetection.value = det
1062-
detectionCount.value = repository.getTotalDetectionCount()
1063-
1064-
if (BuildConfig.DEBUG) {
1065-
Log.w(TAG, "WIFI ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
1067+
try {
1068+
repository.insertDetection(det)
1069+
1070+
if (anomaly.severity == ThreatLevel.CRITICAL ||
1071+
anomaly.severity == ThreatLevel.HIGH) {
1072+
alertUser(det)
1073+
}
1074+
1075+
lastDetection.value = det
1076+
detectionCount.value = repository.getTotalDetectionCount()
1077+
_detectionRefreshEvent.tryEmit(Unit)
1078+
1079+
if (BuildConfig.DEBUG) {
1080+
Log.w(TAG, "WIFI ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
1081+
}
1082+
} catch (e: Exception) {
1083+
Log.e(TAG, "Error saving WiFi detection: ${e.message}", e)
10661084
}
10671085
}
10681086
}
@@ -1129,11 +1147,16 @@ class ScanningService : Service() {
11291147
detection?.let { det ->
11301148
val existing = det.macAddress?.let { repository.getDetectionByMacAddress(it) }
11311149
if (existing == null) {
1132-
repository.insertDetection(det)
1133-
alertUser(det)
1134-
lastDetection.value = det
1135-
detectionCount.value = repository.getTotalDetectionCount()
1136-
Log.w(TAG, "DRONE DETECTED: ${drone.manufacturer} at ${drone.estimatedDistance}")
1150+
try {
1151+
repository.insertDetection(det)
1152+
alertUser(det)
1153+
lastDetection.value = det
1154+
detectionCount.value = repository.getTotalDetectionCount()
1155+
_detectionRefreshEvent.tryEmit(Unit)
1156+
Log.w(TAG, "DRONE DETECTED: ${drone.manufacturer} at ${drone.estimatedDistance}")
1157+
} catch (e: Exception) {
1158+
Log.e(TAG, "Error saving drone detection: ${e.message}", e)
1159+
}
11371160
}
11381161
}
11391162
}
@@ -1154,18 +1177,23 @@ class ScanningService : Service() {
11541177
// Use timestamp-based unique ID for RF anomalies
11551178
val existing = repository.getDetectionBySsid(det.deviceName ?: "")
11561179
if (existing == null) {
1157-
repository.insertDetection(det)
1158-
1159-
if (anomaly.severity == ThreatLevel.CRITICAL ||
1160-
anomaly.severity == ThreatLevel.HIGH) {
1161-
alertUser(det)
1162-
}
1163-
1164-
lastDetection.value = det
1165-
detectionCount.value = repository.getTotalDetectionCount()
1166-
1167-
if (BuildConfig.DEBUG) {
1168-
Log.w(TAG, "RF ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
1180+
try {
1181+
repository.insertDetection(det)
1182+
1183+
if (anomaly.severity == ThreatLevel.CRITICAL ||
1184+
anomaly.severity == ThreatLevel.HIGH) {
1185+
alertUser(det)
1186+
}
1187+
1188+
lastDetection.value = det
1189+
detectionCount.value = repository.getTotalDetectionCount()
1190+
_detectionRefreshEvent.tryEmit(Unit)
1191+
1192+
if (BuildConfig.DEBUG) {
1193+
Log.w(TAG, "RF ANOMALY: ${anomaly.type.displayName} - ${anomaly.description}")
1194+
}
1195+
} catch (e: Exception) {
1196+
Log.e(TAG, "Error saving RF detection: ${e.message}", e)
11691197
}
11701198
}
11711199
}
@@ -1249,17 +1277,22 @@ class ScanningService : Service() {
12491277
// Use frequency as unique identifier
12501278
val existing = det.ssid?.let { repository.getDetectionBySsid(it) }
12511279
if (existing == null) {
1252-
repository.insertDetection(det)
1280+
try {
1281+
repository.insertDetection(det)
12531282

1254-
if (anomaly.severity == ThreatLevel.CRITICAL ||
1255-
anomaly.severity == ThreatLevel.HIGH) {
1256-
alertUser(det)
1257-
}
1283+
if (anomaly.severity == ThreatLevel.CRITICAL ||
1284+
anomaly.severity == ThreatLevel.HIGH) {
1285+
alertUser(det)
1286+
}
12581287

1259-
lastDetection.value = det
1260-
detectionCount.value = repository.getTotalDetectionCount()
1288+
lastDetection.value = det
1289+
detectionCount.value = repository.getTotalDetectionCount()
1290+
_detectionRefreshEvent.tryEmit(Unit)
12611291

1262-
Log.w(TAG, "ULTRASONIC: ${anomaly.type.displayName} - ${anomaly.frequency}Hz")
1292+
Log.w(TAG, "ULTRASONIC: ${anomaly.type.displayName} - ${anomaly.frequency}Hz")
1293+
} catch (e: Exception) {
1294+
Log.e(TAG, "Error saving ultrasonic detection: ${e.message}", e)
1295+
}
12631296
}
12641297
}
12651298
}
@@ -1855,22 +1888,30 @@ class ScanningService : Service() {
18551888
// ==================== Detection Handling ====================
18561889

18571890
private suspend fun handleDetection(detection: Detection) {
1858-
// Use upsert - this will update seen count if existing, or insert if new
1859-
val isNew = repository.upsertDetection(detection)
1860-
1861-
if (isNew) {
1862-
// New detection
1863-
detectionCount.value++
1864-
lastDetection.value = detection
1865-
1866-
Log.d(TAG, "New detection: ${detection.deviceType} - ${detection.macAddress ?: detection.ssid}")
1867-
1868-
// Alert user
1869-
alertUser(detection)
1870-
} else {
1871-
// Existing detection - update lastDetection to refresh UI
1872-
lastDetection.value = detection
1873-
Log.d(TAG, "Updated detection: ${detection.deviceType} - ${detection.macAddress ?: detection.ssid}")
1891+
try {
1892+
// Use upsert - this will update seen count if existing, or insert if new
1893+
val isNew = repository.upsertDetection(detection)
1894+
1895+
if (isNew) {
1896+
// New detection
1897+
detectionCount.value++
1898+
lastDetection.value = detection
1899+
1900+
Log.d(TAG, "New detection: ${detection.deviceType} - ${detection.macAddress ?: detection.ssid}")
1901+
1902+
// Alert user
1903+
alertUser(detection)
1904+
} else {
1905+
// Existing detection - update lastDetection to refresh UI
1906+
lastDetection.value = detection
1907+
Log.d(TAG, "Updated detection: ${detection.deviceType} - ${detection.macAddress ?: detection.ssid}")
1908+
}
1909+
1910+
// Emit refresh event to ensure UI updates even if Room Flow doesn't trigger
1911+
_detectionRefreshEvent.tryEmit(Unit)
1912+
} catch (e: Exception) {
1913+
Log.e(TAG, "Error handling detection: ${e.message}", e)
1914+
logError("Detection", 1001, "Failed to save detection: ${e.message}")
18741915
}
18751916
}
18761917

0 commit comments

Comments
 (0)