Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/android-unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Android Unit Tests

on:
pull_request:
push:

jobs:
android-unit-tests:
name: android-unit-tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'

- name: Setup Android SDK
uses: android-actions/setup-android@v3

- name: Write local.properties
run: |
cat > android/local.properties <<EOF_INNER
sdk.dir=${ANDROID_SDK_ROOT}
EOF_INNER

- name: Run Android unit tests
run: GRADLE_USER_HOME=/tmp/gradle-home ./android/gradlew -p android testDebugUnitTest --no-daemon
22 changes: 22 additions & 0 deletions .github/workflows/desktop-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Desktop Tests

on:
pull_request:
push:

jobs:
desktop-tests:
name: desktop-tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Run desktop tests
run: python -m unittest discover -s desktop/tests -v
17 changes: 17 additions & 0 deletions .github/workflows/firmware-host-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Firmware Host Tests

on:
pull_request:
push:

jobs:
firmware-host-tests:
name: firmware-host-tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Run firmware host tests
run: ./scripts/run_host_tests.sh
25 changes: 25 additions & 0 deletions .github/workflows/web-core-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Web Core Tests

on:
pull_request:
push:

jobs:
web-core-tests:
name: web-core-tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install web dependencies
run: npm --prefix web ci

- name: Run web core tests
run: npm --prefix web run test:core
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
cmake_minimum_required(VERSION 3.16)

file(READ "${CMAKE_SOURCE_DIR}/VERSION" WIFINDER_VERSION_RAW)
string(STRIP "${WIFINDER_VERSION_RAW}" WIFINDER_VERSION)
if(WIFINDER_VERSION STREQUAL "")
message(FATAL_ERROR "VERSION file is empty")
endif()
set(PROJECT_VER "${WIFINDER_VERSION}")

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(wifinder)
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ Desktop protocol/state/CSV tests:
python -m unittest discover -s desktop/tests -v
```

Android unit tests:

```bash
GRADLE_USER_HOME=/tmp/gradle-home ./android/gradlew -p android testDebugUnitTest --no-daemon
```

## CI required checks

GitHub Actions now publishes four stable check names intended for branch-protection required checks:

- `firmware-host-tests` → `./scripts/run_host_tests.sh`
- `android-unit-tests` → `GRADLE_USER_HOME=/tmp/gradle-home ./android/gradlew -p android testDebugUnitTest --no-daemon`
- `desktop-tests` → `python -m unittest discover -s desktop/tests -v`
- `web-core-tests` → `npm --prefix web ci && npm --prefix web run test:core`

## Notes

- ESP32 scanning in this project is 2.4 GHz only.
Expand Down
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.9
39 changes: 37 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@ plugins {
id("org.jetbrains.kotlin.android")
}

val repoVersion: String by lazy {
val file = rootProject.projectDir.parentFile.resolve("VERSION")
if (!file.exists()) {
throw GradleException("Missing VERSION file at ${file.absolutePath}")
}
val value = file.readText().trim()
if (value.isBlank()) {
throw GradleException("VERSION file is empty")
}
value
}

fun parseSemVer(version: String): Triple<Int, Int, Int> {
val parts = version.split('.')
if (parts.size !in 2..3) {
throw GradleException("VERSION must be MAJOR.MINOR or MAJOR.MINOR.PATCH, got '$version'")
}
val major = parts[0].toIntOrNull()
?: throw GradleException("Invalid major version in '$version'")
val minor = parts[1].toIntOrNull()
?: throw GradleException("Invalid minor version in '$version'")
val patch = when (parts.size) {
3 -> parts[2].toIntOrNull()
?: throw GradleException("Invalid patch version in '$version'")
else -> 0
}
if (major < 0 || minor < 0 || patch < 0) {
throw GradleException("VERSION components must be non-negative, got '$version'")
}
return Triple(major, minor, patch)
}

val (versionMajor, versionMinor, versionPatch) = parseSemVer(repoVersion)
val computedVersionCode = (versionMajor * 10000) + (versionMinor * 100) + versionPatch

android {
namespace = "com.wifinder.android"
compileSdk = 34
Expand All @@ -11,8 +46,8 @@ android {
applicationId = "com.wifinder.android"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = computedVersionCode
versionName = repoVersion

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
7 changes: 7 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
Expand All @@ -17,6 +23,7 @@
android:allowBackup="true"
android:icon="@android:drawable/sym_def_app_icon"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:supportsRtl="true"
android:theme="@style/Theme.WiFinder">
<activity
Expand Down
71 changes: 71 additions & 0 deletions android/app/src/main/java/com/wifinder/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ class MainActivity : ComponentActivity() {
onDownloadBacklog = viewModel::downloadBacklog,
onSeedDebugBacklog = viewModel::seedDebugBacklog,
onPushPhoneGpsNow = viewModel::pushPhoneGpsNow,
onSelectBleDevice = viewModel::selectBleDevice,
onDismissBlePicker = viewModel::dismissBleDevicePicker,
onForgetPreferredBleDevice = viewModel::forgetPreferredBleDevice,
onToggleSection = viewModel::toggleSection,
onToggleDashboard = viewModel::toggleDashboardMode,
)
Expand All @@ -164,6 +167,7 @@ class MainActivity : ComponentActivity() {
permissions += Manifest.permission.BLUETOOTH_CONNECT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissions += Manifest.permission.NEARBY_WIFI_DEVICES
permissions += Manifest.permission.POST_NOTIFICATIONS
}
return permissions
Expand Down Expand Up @@ -300,6 +304,9 @@ private fun WiFinderScreen(
onDownloadBacklog: () -> Unit,
onSeedDebugBacklog: () -> Unit,
onPushPhoneGpsNow: () -> Unit,
onSelectBleDevice: (String) -> Unit,
onDismissBlePicker: () -> Unit,
onForgetPreferredBleDevice: () -> Unit,
onToggleSection: (String) -> Unit,
onToggleDashboard: () -> Unit,
) {
Expand All @@ -310,6 +317,45 @@ private fun WiFinderScreen(
color = MaterialTheme.colorScheme.background,
) {
Column(modifier = Modifier.fillMaxSize()) {
if (state.bleDevicePickerVisible) {
AlertDialog(
onDismissRequest = onDismissBlePicker,
title = { Text("Select WiFinder Device") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
state.bleDeviceCandidates.forEach { candidate ->
OutlinedButton(
onClick = { onSelectBleDevice(candidate.address) },
modifier = Modifier.fillMaxWidth(),
) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = candidate.name,
fontWeight = FontWeight.SemiBold,
)
Text(
text = "${candidate.address} • RSSI ${candidate.rssi}",
style = MaterialTheme.typography.bodySmall,
)
if (candidate.remembered) {
Text(
text = "Remembered target",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary,
)
}
}
}
}
}
},
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismissBlePicker) { Text("Cancel") }
},
)
}

// ── Top bar ─────────────────────────────────────────────
TopAppBar(
title = {
Expand Down Expand Up @@ -393,6 +439,7 @@ private fun WiFinderScreen(
onDownloadBacklog = onDownloadBacklog,
onSeedDebugBacklog = onSeedDebugBacklog,
onPushPhoneGpsNow = onPushPhoneGpsNow,
onForgetPreferredBleDevice = onForgetPreferredBleDevice,
)
}

Expand Down Expand Up @@ -662,6 +709,7 @@ private fun QuickActions(
onDownloadBacklog: () -> Unit,
onSeedDebugBacklog: () -> Unit,
onPushPhoneGpsNow: () -> Unit,
onForgetPreferredBleDevice: () -> Unit,
) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
Expand All @@ -671,6 +719,29 @@ private fun QuickActions(
modifier = Modifier.padding(10.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
if (state.preferredBleDeviceAddress.isNotBlank()) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val label = state.preferredBleDeviceName.takeIf { it.isNotBlank() }
?: state.preferredBleDeviceAddress
Text(
text = "Preferred: $label",
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
CompactButton(
label = "Forget",
primary = false,
modifier = Modifier.weight(0.45f),
onClick = onForgetPreferredBleDevice,
)
}
}
// Row 1: connection + scanning
Row(
modifier = Modifier.fillMaxWidth(),
Expand Down
Loading
Loading