# KTag Apps A modular Android app that serves as a launcher for a collection of related subapps. The main screen displays a grid of icons, each launching an independent subapp. Each subapp is its own Gradle module, making it easy to develop and add new ones without touching existing code. ## Project Structure - **`app`** — Main launcher module. Displays the subapp grid and handles navigation. - **`core`** — Shared interfaces (`SubApp`, `SubAppRegistry`) that all modules depend on. - **`shared-services`** — Common settings infrastructure (`SettingsSubApp`, `BaseSettingsActivity`, `SummarizedEditTextPreference`) for subapps with user preferences. Also provides `SharedMqttClient`, a singleton MQTT client shared by all subapps, `DeviceInfoMqttSync` for cross-device info synchronization, and `LocationPublisher` for GPS position reporting. - [**`subapp-sample`**](subapp-sample/README.md) — Example subapp demonstrating the full pattern. - [**`subapp-bletool`**](subapp-bletool/README.md) — Tool for debugging KTag BLE issues. Identical functionality to the old [Android BLE Tool](https://git.ktag.clubk.club/Software/Android-BLE-Tool). - [**`subapp-koth`**](subapp-koth/README.md) — App for hosting King of the Hill games (with [MQTT](https://mqtt.org/)). - [**`subapp-medic`**](subapp-medic/README.md) — App for simulating a medic (with proximity-based healing and [MQTT](https://mqtt.org/)). - [**`subapp-terminal`**](subapp-terminal/README.md) — USB serial terminal for communicating with KTag devices using [usb-serial-for-android](https://github.com/mik3y/usb-serial-for-android). - [**`subapp-mine`**](subapp-mine/README.md) — App for automatically tagging nearby devices via BLE. - [**`subapp-konfigurator`**](subapp-konfigurator/README.md) — App for configuring KTag laser tag devices via BLE and coordinating game sessions. - [**`subapp-sentry`**](subapp-sentry/README.md) — Autonomous sentry gun app that uses the device camera and on-device ML (MediaPipe / EfficientDet-Lite0) to detect people and fire the laser tag gun via USB serial when a person intersects the crosshair. - **`mqtt-broker`** — Embedded MQTT broker module using [Moquette](https://github.com/moquette-io/moquette). Runs as a foreground service with optional SSL, authentication, and mDNS discovery. Also hosts the unified "MQTT Settings" page (client connection settings, broker settings, and mDNS broker discovery). Configured via the overflow menu in the main launcher. ## Building Open the project in Android Studio and sync Gradle, or run: ```bash ./gradlew assembleDebug ``` ## KTag Colors The KTag color palette should be used consistently across all subapps: | Color | Hex | Usage | |------------|-----------|--------------------------------------------| | Green | `#4BA838` | Success states, positive indicators | | Blue | `#4D6CFA` | Blue team, links, interactive elements | | Red | `#F34213` | Red team, warnings, destructive actions | | Yellow | `#FFC857` | Highlights, accents | | Purple | `#9B59B6` | All teams, combined team indicators | | Dark Gray | `#323031` | Backgrounds, text, icons | These colors are defined in each module's `Color.kt` file (e.g., `KTagGreen`, `KTagBlue`, `KTagRed`, `KTagYellow`, `KTagPurple`, `KTagDarkGray`). ## Adding a New SubApp ### 1. Create the module directory ``` subapp-yourname/ └── src/main/ ├── AndroidManifest.xml ├── java/club/clubk/ktag/apps/yourname/ │ ├── YourSubApp.kt │ ├── YourActivity.kt │ └── YourInitializer.kt └── res/drawable/ └── ic_yourname.xml ``` ### 2. Add `build.gradle.kts` ```kotlin plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.compose) } android { namespace = "club.clubk.ktag.apps.yourname" compileSdk = 34 defaultConfig { minSdk = 24 } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } buildFeatures { compose = true } } dependencies { implementation(project(":core")) implementation(libs.androidx.core.ktx) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.material3) implementation(libs.androidx.startup) } ``` ### 3. Implement the `SubApp` interface ```kotlin // YourSubApp.kt package club.clubk.ktag.apps.yourname import android.content.Context import android.content.Intent import club.clubk.ktag.apps.core.SubApp class YourSubApp : SubApp { override val id = "yourname" override val name = "Your App" override val icon = R.drawable.ic_yourname override fun createIntent(context: Context): Intent { return Intent(context, YourActivity::class.java) } } ``` ### 4. Create the entry Activity ```kotlin // YourActivity.kt package club.clubk.ktag.apps.yourname import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent class YourActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Your UI here } } } ``` ### 5. Create the Initializer This registers your subapp with the launcher automatically on app startup: ```kotlin // YourInitializer.kt package club.clubk.ktag.apps.yourname import android.content.Context import androidx.startup.Initializer import club.clubk.ktag.apps.core.SubAppRegistry class YourInitializer : Initializer { override fun create(context: Context) { SubAppRegistry.register(YourSubApp()) } override fun dependencies(): List>> = emptyList() } ``` ### 6. Declare the Activity and Initializer in `AndroidManifest.xml` ```xml ``` ### 7. Add a drawable icon Place a vector drawable at `src/main/res/drawable/ic_yourname.xml`. This is what appears in the launcher grid. ### 8. Register the module with the project In `settings.gradle.kts`, add: ```kotlin include(":subapp-yourname") ``` In `app/build.gradle.kts`, add the dependency: ```kotlin implementation(project(":subapp-yourname")) ``` Sync Gradle and run the app — your new subapp will appear in the launcher grid. ## Adding Settings to a SubApp If your subapp needs user-configurable settings, use the `shared-services` module. ### 1. Add the dependency In your subapp's `build.gradle.kts`: ```kotlin dependencies { implementation(project(":core")) implementation(project(":shared-services")) // ... other dependencies } ``` ### 2. Implement `SettingsSubApp` instead of `SubApp` ```kotlin // YourSubApp.kt package club.clubk.ktag.apps.yourname import android.content.Context import android.content.Intent import club.clubk.ktag.apps.sharedservices.SettingsSubApp class YourSubApp : SettingsSubApp { override val id = "yourname" override val name = "Your App" override val icon = R.drawable.ic_yourname override val settingsPreferencesResId = R.xml.settings_pref override val usesMqtt = true // set to false if not using MQTT override fun createIntent(context: Context): Intent { return Intent(context, YourActivity::class.java) } } ``` ### 3. Create a settings activity ```kotlin // YourSettingsActivity.kt package club.clubk.ktag.apps.yourname import android.content.Context import android.content.Intent import club.clubk.ktag.apps.sharedservices.BaseSettingsActivity class YourSettingsActivity : BaseSettingsActivity() { companion object { @JvmStatic fun createIntent(context: Context): Intent { return BaseSettingsActivity.createIntent(context, R.xml.settings_pref, YourSettingsActivity::class.java) } } } ``` ### 4. Create the preferences XML Create `src/main/res/xml/settings_pref.xml`: ```xml ``` ### 5. Declare the settings activity in `AndroidManifest.xml` ```xml ``` ### 6. Launch settings from your activity ```java // In Java: Intent intent = YourSettingsActivity.createIntent(this); startActivity(intent); ``` ```kotlin // In Kotlin: startActivity(YourSettingsActivity.createIntent(this)) ``` ### Using MQTT MQTT connection settings (server URI, username, password, battlefield) are configured centrally in the "MQTT Settings" page accessible from the main launcher's overflow menu. Subapps do **not** need their own MQTT settings UI. A single `SharedMqttClient` singleton (in `shared-services`) manages the MQTT connection for the entire app. It connects at app startup and reconnects automatically when settings change. Subapps publish and subscribe through it: ```kotlin import club.clubk.ktag.apps.sharedservices.SharedMqttClient import club.clubk.ktag.apps.sharedservices.MqttMessageListener // Publish a message val battlefield = SharedMqttClient.battlefield SharedMqttClient.publish("KTag/$battlefield/YourApp/Hello", "Hello!".toByteArray(), qos = 1, retained = false) // Subscribe to a topic val listener = object : MqttMessageListener { override fun onMessageReceived(topic: String, payload: ByteArray) { val text = String(payload) // handle message } } SharedMqttClient.subscribe("KTag/$battlefield/YourApp/Listen", listener) // Unsubscribe when done (e.g. in cleanup/onCleared) SharedMqttClient.unsubscribe("KTag/$battlefield/YourApp/Listen", listener) ``` ### GPS Location Publishing Once connected to an MQTT broker, the app automatically publishes the device's GPS position every **30 seconds** (subject to OS scheduling) to: ``` KTag/{Battlefield}/Devices/{Device ID}/Location ``` The payload is a JSON object: ```json {"lat":51.5074,"lon":-0.1278,"alt":12.3,"accuracy":5.0} ``` `alt` and `accuracy` are omitted if the device does not provide them. The message is published with QoS 1 and `retained=true`, so a subscriber joining mid-game will immediately receive the last known position. Location updates stop automatically when the MQTT connection is lost. The MQTT Settings page includes a **Device ID** field (defaults to the device's model name) used as the MQTT client identifier. When **Autodiscovery** is enabled, the client automatically finds a KTag broker on the local network via mDNS (`_mqtt._tcp.` with `purpose=KTag MQTT Broker`) and connects using the credentials `KTag` / `{Battlefield}`. The broker automatically ensures this credential exists on startup. ## License: [AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later.html) This software is part of the KTag project, a DIY laser tag game with customizable features and wide interoperability. 🛡 🃞 Copyright © 2025-2026 Joseph P. Kearney and the KTag developers. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. There should be a copy of the GNU Affero General Public License in the [LICENSE](LICENSE) file in the root of this repository. If not, see . ## Open-Source Software This software in turn makes use of the following open-source software libraries and components: | Name | Version | License ([SPDX](https://spdx.org/licenses/)) | URL |------------------------|--------------:|---------------------------------------------------------|--------------------------------------------- | Kotlin | 2.2.20 | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | https://kotlinlang.org/ | JetBrains Mono | 2.304 | [OFL-1.1](https://spdx.org/licenses/OFL-1.1.html) | https://github.com/JetBrains/JetBrainsMono | Moquette MQTT Broker | 0.17 | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | https://github.com/moquette-io/moquette | Eclipse Paho MQTT | 1.2.4 + 1.1.1 | [EPL-2.0](https://spdx.org/licenses/EPL-2.0.html) | https://github.com/eclipse/paho.mqtt.java | usb-serial-for-android | 3.10.0 | [MIT](https://spdx.org/licenses/MIT.html) | https://github.com/mik3y/usb-serial-for-android/ | CameraX | 1.4.0 | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | https://developer.android.com/jetpack/androidx/releases/camera | MediaPipe Tasks | 0.10.32 | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | https://developers.google.com/mediapipe | EfficientDet-Lite0 | 1 | [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) | https://tfhub.dev/tensorflow/lite-model/efficientdet/lite0/detection/metadata/1