haunt / commit
Layer 1: project scaffold + RTT availability check
.gitignore | 10 ++++ app/build.gradle.kts | 44 +++++++++++++++++ app/src/main/AndroidManifest.xml | 28 +++++++++++ .../main/java/net/fuzzyworld/haunt/MainActivity.kt | 55 ++++++++++++++++++++++ .../net/fuzzyworld/haunt/model/DistanceTriple.kt | 19 ++++++++ .../net/fuzzyworld/haunt/model/ZoneFingerprint.kt | 7 +++ app/src/main/res/layout/activity_main.xml | 45 ++++++++++++++++++ app/src/main/res/values/strings.xml | 9 ++++ build.gradle.kts | 4 ++ gradle/libs.versions.toml | 17 +++++++ gradle/wrapper/gradle-wrapper.properties | 7 +++ settings.gradle.kts | 17 +++++++ 12 files changed, 262 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10cfdbf --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..188f1b7 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) +} + +android { + namespace = "net.fuzzyworld.haunt" + compileSdk = 35 + + defaultConfig { + applicationId = "net.fuzzyworld.haunt" + minSdk = 28 + targetSdk = 35 + versionCode = 1 + versionName = "1.0" + } + + buildTypes { + release { + isMinifyEnabled = false + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + viewBinding = true + } +} + +dependencies { + implementation(libs.kotlinx.coroutines.android) + implementation(libs.retrofit) + implementation(libs.retrofit.gson) + implementation(libs.okhttp) + implementation(libs.gson) +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..08a4ca7 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" /> + <uses-permission android:name="android.permission.INTERNET" /> + + <uses-feature android:name="android.hardware.wifi.rtt" android:required="true" /> + + <application + android:allowBackup="true" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/Theme.AppCompat.Light.DarkActionBar"> + + <activity + android:name=".MainActivity" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + + </application> + +</manifest> diff --git a/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt b/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt new file mode 100644 index 0000000..7e8e5d6 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/MainActivity.kt @@ -0,0 +1,55 @@ +package net.fuzzyworld.haunt + +import android.Manifest +import android.content.pm.PackageManager +import android.net.wifi.rtt.WifiRttManager +import android.os.Bundle +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import net.fuzzyworld.haunt.databinding.ActivityMainBinding + +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + + private val requestPermission = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + if (granted) { + checkRttAvailability() + } else { + Toast.makeText(this, R.string.permission_required, Toast.LENGTH_LONG).show() + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + binding.calibrateButton.setOnClickListener { + binding.modeText.text = "Mode: Calibrate (not yet implemented)" + } + binding.liveButton.setOnClickListener { + binding.modeText.text = "Mode: Live (not yet implemented)" + } + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED + ) { + checkRttAvailability() + } else { + requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) + } + } + + private fun checkRttAvailability() { + val rttManager = getSystemService(WifiRttManager::class.java) + val available = rttManager?.isAvailable == true + binding.rttStatusText.text = getString( + if (available) R.string.rtt_available else R.string.rtt_unavailable + ) + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/model/DistanceTriple.kt b/app/src/main/java/net/fuzzyworld/haunt/model/DistanceTriple.kt new file mode 100644 index 0000000..9043d05 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/model/DistanceTriple.kt @@ -0,0 +1,19 @@ +package net.fuzzyworld.haunt.model + +import kotlin.math.pow +import kotlin.math.sqrt + +data class DistanceTriple(val d1: Double, val d2: Double, val d3: Double) { + + fun distanceTo(other: DistanceTriple): Double = sqrt( + (d1 - other.d1).pow(2) + (d2 - other.d2).pow(2) + (d3 - other.d3).pow(2) + ) + + companion object { + fun average(readings: List<DistanceTriple>) = DistanceTriple( + readings.map { it.d1 }.average(), + readings.map { it.d2 }.average(), + readings.map { it.d3 }.average(), + ) + } +} diff --git a/app/src/main/java/net/fuzzyworld/haunt/model/ZoneFingerprint.kt b/app/src/main/java/net/fuzzyworld/haunt/model/ZoneFingerprint.kt new file mode 100644 index 0000000..37c12a1 --- /dev/null +++ b/app/src/main/java/net/fuzzyworld/haunt/model/ZoneFingerprint.kt @@ -0,0 +1,7 @@ +package net.fuzzyworld.haunt.model + +data class ZoneFingerprint( + val name: String, + val hueLightId: Int, + val triple: DistanceTriple, +) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..c4108ad --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/rttStatusText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:textSize="16sp" /> + + <TextView + android:id="@+id/modeText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:padding="16dp" + android:text="Mode: —" + android:textSize="14sp" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="16dp"> + + <Button + android:id="@+id/calibrateButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/tab_calibrate" /> + + <Button + android:id="@+id/liveButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_marginStart="8dp" + android:text="@string/tab_live" /> + + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..e62e520 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="app_name">Haunt</string> + <string name="tab_calibrate">Calibrate</string> + <string name="tab_live">Live</string> + <string name="rtt_available">WiFi RTT: available</string> + <string name="rtt_unavailable">WiFi RTT: NOT available on this device</string> + <string name="permission_required">Location permission required for RTT ranging</string> +</resources> diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..7629126 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,4 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..e09e4d8 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,17 @@ +[versions] +agp = "8.7.3" +kotlin = "2.0.0" +coroutines = "1.8.1" +retrofit = "2.11.0" +okhttp = "4.12.0" + +[libraries] +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +retrofit-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +gson = { module = "com.google.code.gson:gson", version = "2.11.0" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..09523c0 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..fd6876b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "haunt" +include(":app")