Skip to content

Commit 2bf3b48

Browse files
authored
Merge pull request #16 from nbschultz97/codex/find-remaining-tasks-for-app-functionality
Implement Wi-Fi scanning flow and Android build plumbing
2 parents 264691b + 119dfed commit 2bf3b48

16 files changed

Lines changed: 440 additions & 27 deletions

File tree

.gitignore

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1-
# Generated binary assets
2-
__pycache__/
3-
gradle/wrapper/gradle-wrapper.jar
1+
# Gradle and build outputs
2+
.gradle/
3+
build/
4+
app/build/
5+
6+
# Local Android Studio config
7+
.idea/
8+
*.iml
9+
local.properties
10+
11+
# Generated assets from bootstrap
12+
app/src/main/assets/
13+
app/src/main/res/drawable/*.png
14+
15+
# OS cruft
16+
.DS_Store

README.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
# DroneDetectAndroid
22

3-
This repo hosts Android-side utilities for drone rotor detection.
4-
Binary assets (TensorFlow Lite model and icons) are stored in base64 to
5-
keep the repo lean for air-gapped deployments.
3+
Android-side utilities for rotor detection and Wi‑Fi reconnaissance. Assets ship as base64 to keep the repo lean for air-gapped deployments and are restored automatically during Gradle builds.
64

75
## Bootstrap
86

9-
After cloning, restore the binary assets:
7+
After cloning, restore the binary assets (TensorFlow Lite model and PNG drawables):
108

119
```bash
1210
python3 scripts/bootstrap_assets.py
1311
```
1412

15-
This will recreate the `rotor_v1.tflite` model and the required PNG
16-
drawables under `app/src/main/...`.
13+
Gradle also depends on this script via the `preBuild` hook, so IDE and CI builds will automatically decode assets into `app/src/main/...` if Python is available.
1714

1815
## Pipeline
1916

20-
1. **Asset bootstrap** – Decode the model and icons from `assets/*.b64` using the script above. Generated files land under `app/src/main/...`.
21-
2. **Runtime wiring**`MainActivity` creates a `DroneSignalDetector`, which launches a coroutine to fetch flight data and schedule Wi‑Fi scans.
22-
3. **Scan handling**`WifiScanReceiver` receives scan results. Future work will feed the TFLite model for rotor classification.
17+
1. **Asset bootstrap** – Decode the model and icons from `assets/*.b64` using the script above (or let Gradle call it).
18+
2. **Runtime wiring**`MainActivity` requests Wi‑Fi/location permissions, wires a simple UI, and calls `DroneSignalDetector.startScan()`.
19+
3. **Scan handling**`DroneSignalDetector` schedules repeated `WifiManager.startScan()` calls, ingests stub flight data, and surfaces scan results plus the last telemetry snapshot to the UI via callbacks.
20+
21+
## Building
22+
23+
This repo ships a minimal Gradle wrapper script for offline/air-gapped work. Supply a compatible `gradle/wrapper/gradle-wrapper.jar` (from Gradle 8.2.1) in your environment, then run:
24+
25+
```bash
26+
./gradlew assembleDebug
27+
```
28+
29+
If you prefer a hosted toolchain, open the project in Android Studio (Giraffe+), accept SDK prompts, and build the `app` module. Remember to run the asset bootstrap script if Python is missing from your build host.
2330

2431
## Testing
2532

26-
Verify the asset bootstrap step restores the binaries:
33+
To validate the asset bootstrap step:
2734

2835
```bash
2936
python3 scripts/bootstrap_assets.py
3037
```
3138

32-
Each asset should report a `decoded` message. Remove the generated `app/src/main/assets` and `app/src/main/res` directories after testing to keep the repo text‑only.
39+
Each asset should report a `decoded` message. Remove the generated `app/src/main/assets` and `app/src/main/res/drawable` contents after testing to keep the repo text‑only.

app/build.gradle

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
plugins {
2+
id "com.android.application"
3+
id "org.jetbrains.kotlin.android"
4+
}
5+
6+
android {
7+
namespace "com.example.dronedetect"
8+
compileSdk = 34
9+
10+
defaultConfig {
11+
applicationId = "com.example.dronedetect"
12+
minSdk = 26
13+
targetSdk = 34
14+
versionCode = 1
15+
versionName = "1.0"
16+
17+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18+
vectorDrawables {
19+
useSupportLibrary = true
20+
}
21+
}
22+
23+
buildTypes {
24+
release {
25+
isMinifyEnabled = false
26+
proguardFiles(
27+
getDefaultProguardFile("proguard-android-optimize.txt"),
28+
"proguard-rules.pro"
29+
)
30+
}
31+
debug {
32+
isMinifyEnabled = false
33+
}
34+
}
35+
36+
compileOptions {
37+
sourceCompatibility = JavaVersion.VERSION_11
38+
targetCompatibility = JavaVersion.VERSION_11
39+
}
40+
kotlinOptions {
41+
jvmTarget = "11"
42+
}
43+
44+
buildFeatures {
45+
viewBinding = true
46+
}
47+
48+
tasks.named("preBuild") {
49+
dependsOn("bootstrapAssets")
50+
}
51+
}
52+
53+
val bootstrapAssets by tasks.registering { task ->
54+
task.group = "assets"
55+
task.description = "Decode bundled base64 assets into their binary forms"
56+
task.doLast {
57+
exec {
58+
commandLine("python3", rootProject.file("scripts/bootstrap_assets.py").absolutePath)
59+
}
60+
}
61+
}
62+
63+
dependencies {
64+
implementation("androidx.core:core-ktx:1.12.0")
65+
implementation("androidx.appcompat:appcompat:1.6.1")
66+
implementation("com.google.android.material:material:1.11.0")
67+
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
68+
}

app/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Keep file intentionally minimal; add rules when obfuscation is enabled.

app/src/main/AndroidManifest.xml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="com.example.dronedetect">
4+
5+
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
6+
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
7+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
8+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
9+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
10+
<uses-permission
11+
android:name="android.permission.NEARBY_WIFI_DEVICES"
12+
android:usesPermissionFlags="neverForLocation"
13+
android:required="false" />
14+
15+
<uses-feature android:name="android.hardware.wifi" android:required="true" />
16+
17+
<application
18+
android:allowBackup="true"
19+
android:icon="@drawable/drone_icon"
20+
android:label="@string/app_name"
21+
android:roundIcon="@drawable/drone_icon"
22+
android:supportsRtl="true"
23+
android:theme="@style/Theme.DroneDetect">
24+
<activity
25+
android:name=".MainActivity"
26+
android:exported="true">
27+
<intent-filter>
28+
<action android:name="android.intent.action.MAIN" />
29+
30+
<category android:name="android.intent.category.LAUNCHER" />
31+
</intent-filter>
32+
</activity>
33+
</application>
34+
</manifest>

app/src/main/java/com/example/dronedetect/DroneSignalDetector.kt

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,100 @@ package com.example.dronedetect
33
import android.content.BroadcastReceiver
44
import android.content.Context
55
import android.content.Intent
6+
import android.content.IntentFilter
7+
import android.net.wifi.ScanResult
68
import android.net.wifi.WifiManager
79
import android.os.Handler
810
import android.os.Looper
11+
import android.util.Log
912
import kotlinx.coroutines.CoroutineScope
1013
import kotlinx.coroutines.Dispatchers
1114
import kotlinx.coroutines.SupervisorJob
1215
import kotlinx.coroutines.cancel
1316
import kotlinx.coroutines.launch
1417
import kotlinx.coroutines.withContext
18+
import java.time.Instant
1519

16-
class DroneSignalDetector(private val context: Context) {
20+
class DroneSignalDetector(
21+
private val context: Context,
22+
private val onResults: (List<ScanResult>, FlightSnapshot) -> Unit = { _, _ -> }
23+
) {
1724
private val handler = Handler(Looper.getMainLooper())
1825
private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
19-
private val receiver = WifiScanReceiver()
20-
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
26+
private val receiver = WifiScanReceiver(wifiManager, ::handleScanResults)
27+
private var scanScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
28+
private var isScanning = false
29+
private var latestFlightData: FlightSnapshot = FlightSnapshot(timestamp = Instant.now(), source = "bootstrap", payload = emptyMap())
2130

2231
fun startScan() {
23-
scope.launch {
24-
fetchFlightData()
25-
// TODO: implement scanning logic
26-
}
32+
if (isScanning) return
33+
isScanning = true
34+
context.registerReceiver(receiver, IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION))
35+
scanScope.launch { fetchFlightData() }
36+
requestScan()
2737
}
2838

2939
suspend fun fetchFlightData() = withContext(Dispatchers.IO) {
30-
// TODO: implement fetching flight data
40+
// Placeholder for real RF/telemetry ingestion.
41+
// Here we stamp a deterministic payload so downstream processing can
42+
// align Wi-Fi RSSI snapshots with the last known telemetry sample.
43+
latestFlightData = FlightSnapshot(
44+
timestamp = Instant.now(),
45+
source = "local_stub",
46+
payload = mapOf(
47+
"note" to "Replace with CSI/telemetry feed",
48+
"status" to "idle"
49+
)
50+
)
51+
Log.d(TAG, "Flight data refreshed at ${latestFlightData.timestamp}")
3152
}
3253

3354
fun stopScan() {
55+
if (!isScanning) return
56+
isScanning = false
3457
handler.removeCallbacksAndMessages(null)
35-
context.unregisterReceiver(receiver)
36-
scope.cancel()
58+
runCatching { context.unregisterReceiver(receiver) }
59+
scanScope.cancel()
60+
scanScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
61+
}
62+
63+
private fun requestScan() {
64+
val started = wifiManager.startScan()
65+
if (!started) {
66+
Log.w(TAG, "Wi-Fi scan request rejected by platform")
67+
}
68+
handler.postDelayed({
69+
if (isScanning) {
70+
requestScan()
71+
}
72+
}, SCAN_INTERVAL_MS)
73+
}
74+
75+
private fun handleScanResults(results: List<ScanResult>) {
76+
if (!isScanning) return
77+
onResults(results, latestFlightData)
78+
}
79+
80+
companion object {
81+
private const val TAG = "DroneSignalDetector"
82+
private const val SCAN_INTERVAL_MS = 10_000L
3783
}
3884
}
3985

40-
class WifiScanReceiver : BroadcastReceiver() {
86+
data class FlightSnapshot(
87+
val timestamp: Instant,
88+
val source: String,
89+
val payload: Map<String, String>
90+
)
91+
92+
private class WifiScanReceiver(
93+
private val wifiManager: WifiManager,
94+
private val onResults: (List<ScanResult>) -> Unit
95+
) : BroadcastReceiver() {
4196
override fun onReceive(context: Context?, intent: Intent?) {
42-
// TODO: handle scan results
97+
val action = intent?.action ?: return
98+
if (action != WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) return
99+
val results = wifiManager.scanResults.orEmpty()
100+
onResults(results)
43101
}
44102
}

0 commit comments

Comments
 (0)