Reformat to use more standard Android code style

Follows stock Android Studio styles for Kotlin and XML except that we
use an indent of two and allow up to three consecutive empty lines.
This commit is contained in:
Reinout Meliesie 2026-01-22 19:29:45 +01:00
commit eebb3a589d
Signed by: zedfrigg
GPG key ID: 3AFCC06481308BC6
17 changed files with 512 additions and 481 deletions

View file

@ -1,18 +1,17 @@
import java.io.FileInputStream
import java.util.Properties
import org.gradle.kotlin.dsl.android
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.FileInputStream
import java.util.Properties
val keystoreProperties = Properties () val keystoreProperties = Properties()
keystoreProperties . load ( FileInputStream ( rootProject . file ("keystore.properties") ) ) keystoreProperties.load(FileInputStream(rootProject.file("keystore.properties")))
plugins { plugins {
id ("com.android.application") . version ("9.0.0") id("com.android.application").version("9.0.0")
id ("org.jetbrains.kotlin.plugin.compose") . version ("2.3.0") id("org.jetbrains.kotlin.plugin.compose").version("2.3.0")
id ("org.jetbrains.kotlin.plugin.serialization") . version ("2.3.0") id("org.jetbrains.kotlin.plugin.serialization").version("2.3.0")
} }
android { android {
@ -28,27 +27,28 @@ android {
} }
signingConfigs { signingConfigs {
create ("kernelmaft") { create("kernelmaft") {
keyAlias = "kernelmaft" keyAlias = "kernelmaft"
keyPassword = keystoreProperties ["keyPassword"] as String keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file ( keystoreProperties ["storeFile"] as String ) storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties ["storePassword"] as String storePassword = keystoreProperties["storePassword"] as String
} }
} }
buildTypes { buildTypes {
debug { debug {
signingConfig = signingConfigs . getByName ("kernelmaft") signingConfig = signingConfigs.getByName("kernelmaft")
} }
release { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
signingConfig = signingConfigs . getByName ("kernelmaft") signingConfig = signingConfigs.getByName("kernelmaft")
} }
} }
compileOptions { compileOptions {
// Required even though we don't have any Java sources because it needs to match Kotlin's JVM version // Required even though we don't have any Java sources because it needs to match Kotlin's JVM
targetCompatibility = JavaVersion . VERSION_25 // version.
targetCompatibility = JavaVersion.VERSION_25
} }
buildFeatures { buildFeatures {
compose = true compose = true
@ -57,29 +57,29 @@ android {
kotlin { kotlin {
compilerOptions { compilerOptions {
jvmTarget = JvmTarget . JVM_25 jvmTarget = JvmTarget.JVM_25
} }
} }
dependencies { dependencies {
// Android runtime libraries // Android runtime libraries.
implementation ( "com.google.android.material:material:1.13.0" ) implementation("com.google.android.material:material:1.13.0")
implementation ( "androidx.activity:activity-compose:1.12.2" ) implementation("androidx.activity:activity-compose:1.12.2")
implementation ( "androidx.core:core-ktx:1.17.0" ) implementation("androidx.core:core-ktx:1.17.0")
implementation ( "androidx.compose.material3:material3:1.4.0" ) implementation("androidx.compose.material3:material3:1.4.0")
implementation ( "androidx.compose.ui:ui:1.10.1" ) implementation("androidx.compose.ui:ui:1.10.1")
implementation ( "androidx.compose.ui:ui-graphics:1.10.1" ) implementation("androidx.compose.ui:ui-graphics:1.10.1")
debugImplementation ( "androidx.compose.ui:ui-tooling:1.10.1" ) debugImplementation("androidx.compose.ui:ui-tooling:1.10.1")
implementation ( "androidx.lifecycle:lifecycle-runtime-ktx:2.10.0" ) implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.10.0")
implementation ( "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2" ) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
implementation ( "org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0" ) implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.10.0")
// Other libraries // Other libraries.
implementation ( "io.github.davidepianca98:kmqtt-common:1.0.0" ) implementation("io.github.davidepianca98:kmqtt-common:1.0.0")
implementation ( "io.github.davidepianca98:kmqtt-client:1.0.0" ) implementation("io.github.davidepianca98:kmqtt-client:1.0.0")
} }
tasks . withType ( KotlinCompile :: class ) . all { tasks.withType(KotlinCompile::class).all {
compilerOptions { compilerOptions {
freeCompilerArgs . addAll ("-opt-in=kotlin.ExperimentalUnsignedTypes") freeCompilerArgs.addAll("-opt-in=kotlin.ExperimentalUnsignedTypes")
} }
} }

View file

@ -1,22 +1,22 @@
<?xml version = "1.0" encoding = "utf-8" ?> <?xml version = "1.0" encoding = "utf-8" ?>
<manifest xmlns:android = "http://schemas.android.com/apk/res/android" > <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name = "android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name = "android.permission.NEARBY_WIFI_DEVICES" /> <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
<application <application
android:label = "Zanbur" android:icon="@mipmap/ic_launcher"
android:icon = "@mipmap/ic_launcher" android:label="Zanbur"
android:roundIcon = "@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl = "true" > android:supportsRtl="true">
<activity <activity
android:name = "com.kernelmaft.zanbur.ui.MainActivity" android:name="com.kernelmaft.zanbur.ui.MainActivity"
android:exported = "true" > android:exported="true">
<intent-filter> <intent-filter>
<action android:name = "android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name = "android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>

View file

@ -2,30 +2,32 @@ package com.kernelmaft.zanbur.common
enum class ChangeSource { Local , Remote } enum class ChangeSource { Local, Remote }
typealias CurrentSceneSubscriber = ( Group , Scene , ChangeSource ) -> Unit typealias CurrentSceneSubscriber = (Group, Scene, ChangeSource) -> Unit
typealias GroupAddedSubscriber = ( Group , ChangeSource ) -> Unit typealias GroupAddedSubscriber = (Group, ChangeSource) -> Unit
object AppState { object AppState {
private val currentSceneSubscribers : MutableList <CurrentSceneSubscriber> = mutableListOf () private val currentSceneSubscribers: MutableList<CurrentSceneSubscriber> = mutableListOf()
private val groupAddedSubscribers : MutableList <GroupAddedSubscriber> = mutableListOf () private val groupAddedSubscribers: MutableList<GroupAddedSubscriber> = mutableListOf()
fun setCurrentScene ( group : Group , newScene : Scene , source : ChangeSource ) { fun setCurrentScene(group: Group, newScene: Scene, source: ChangeSource) {
for ( subscriber in currentSceneSubscribers ) { for (subscriber in currentSceneSubscribers) {
subscriber ( group , newScene , source ) subscriber(group, newScene, source)
} }
} }
fun subscribeToCurrentScene ( subscriber : CurrentSceneSubscriber ) {
currentSceneSubscribers . add (subscriber)
}
fun addGroup ( newGroup : Group , source : ChangeSource ) { fun subscribeToCurrentScene(subscriber: CurrentSceneSubscriber) {
for ( subscriber in groupAddedSubscribers ) { currentSceneSubscribers.add(subscriber)
subscriber ( newGroup , source ) }
fun addGroup(newGroup: Group, source: ChangeSource) {
for (subscriber in groupAddedSubscribers) {
subscriber(newGroup, source)
} }
} }
fun subscribeToGroupAdded ( subscriber : GroupAddedSubscriber ) {
groupAddedSubscribers . add (subscriber) fun subscribeToGroupAdded(subscriber: GroupAddedSubscriber) {
groupAddedSubscribers.add(subscriber)
} }
} }

View file

@ -7,12 +7,14 @@ object Config {
const val MQTT_SERVER_PORT = 1883 const val MQTT_SERVER_PORT = 1883
const val MQTT_TOPIC = "zigbee2mqtt" const val MQTT_TOPIC = "zigbee2mqtt"
val groups = listOf ( val groups = listOf(
Group ( 0 , "All lights" , listOf ( Group(
Scene ( 0 , "Warm" ) , 0, "All lights", listOf(
Scene ( 1 , "Warm dim" ) , Scene(0, "Warm"),
Scene ( 2 , "Warm dim with purple" ) , Scene(1, "Warm dim"),
Scene ( 100 , "All off" ) , Scene(2, "Warm dim with purple"),
) ) Scene(100, "All off"),
)
)
) )
} }

View file

@ -2,14 +2,14 @@ package com.kernelmaft.zanbur.common
data class Group ( data class Group(
val id : Int , val id: Int,
val name : String , val name: String,
val scenes : List <Scene> , val scenes: List<Scene>,
val currentScene : Scene ? = null , val currentScene: Scene? = null,
) )
data class Scene ( data class Scene(
val id : Int , val id: Int,
val name : String , val name: String,
) )

View file

@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineExceptionHandler
val exceptionPrinter = CoroutineExceptionHandler { _ , throwable -> val exceptionPrinter = CoroutineExceptionHandler { _, throwable ->
throwable . printStackTrace () throwable.printStackTrace()
throw throwable throw throwable
} }

View file

@ -1,45 +1,45 @@
package com.kernelmaft.zanbur.network package com.kernelmaft.zanbur.network
import com.kernelmaft.zanbur.common.Config.MQTT_SERVER_HOST import com.kernelmaft.zanbur.common.Config
import com.kernelmaft.zanbur.common.Config.MQTT_SERVER_PORT import io.github.davidepianca98.MQTTClient
import com.kernelmaft.zanbur.common.Config.MQTT_TOPIC import io.github.davidepianca98.mqtt.MQTTVersion
import io.github.davidepianca98.* import io.github.davidepianca98.mqtt.Subscription
import io.github.davidepianca98.mqtt.* import io.github.davidepianca98.mqtt.packets.Qos
import io.github.davidepianca98.mqtt.MQTTVersion.* import io.github.davidepianca98.mqtt.packets.mqtt.MQTTPublish
import io.github.davidepianca98.mqtt.packets.Qos.* import kotlinx.coroutines.CoroutineScope
import io.github.davidepianca98.mqtt.packets.mqtt.* import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.* import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO import kotlinx.serialization.json.Json
import kotlinx.serialization.json.*
typealias MqttPublishHandler = ( MQTTPublish , Json ) -> Unit typealias MqttPublishHandler = (MQTTPublish, Json) -> Unit
object MqttClient { object MqttClient {
private var client : MQTTClient ? = null private var client: MQTTClient? = null
private var coroutineScope : CoroutineScope ? = null private var coroutineScope: CoroutineScope? = null
private val publishHandlers : MutableList <MqttPublishHandler> = mutableListOf () private val publishHandlers: MutableList<MqttPublishHandler> = mutableListOf()
fun run ( coroutineScope : CoroutineScope ) { fun run(coroutineScope: CoroutineScope) {
this . coroutineScope = coroutineScope this.coroutineScope = coroutineScope
val json = Json { ignoreUnknownKeys = true } val json = Json { ignoreUnknownKeys = true }
coroutineScope . launch ( IO + exceptionPrinter ) { coroutineScope.launch(Dispatchers.IO + exceptionPrinter) {
client = MQTTClient ( MQTT5 , MQTT_SERVER_HOST , MQTT_SERVER_PORT , null ) { client =
for ( handler in publishHandlers ) handler ( it , json ) MQTTClient(MQTTVersion.MQTT5, Config.MQTT_SERVER_HOST, Config.MQTT_SERVER_PORT, null) {
for (handler in publishHandlers) handler(it, json)
} }
client !! . subscribe ( listOf ( Subscription ( MQTT_TOPIC + "/#" ) ) ) client!!.subscribe(listOf(Subscription(Config.MQTT_TOPIC + "/#")))
client !! . run () client!!.run()
} }
} }
fun addPublishHandler ( handler : MqttPublishHandler ) = publishHandlers . add (handler) fun addPublishHandler(handler: MqttPublishHandler) = publishHandlers.add(handler)
fun publish ( topic : String , payload : UByteArray ) { fun publish(topic: String, payload: UByteArray) {
coroutineScope !! . launch ( IO + exceptionPrinter ) { coroutineScope!!.launch(Dispatchers.IO + exceptionPrinter) {
client !! . publish ( false , AT_MOST_ONCE , topic , payload ) client!!.publish(false, Qos.AT_MOST_ONCE, topic, payload)
} }
} }
} }

View file

@ -1,20 +1,23 @@
package com.kernelmaft.zanbur.network package com.kernelmaft.zanbur.network
import com.kernelmaft.zanbur.common.* import com.kernelmaft.zanbur.common.Config
import com.kernelmaft.zanbur.common.Config.MQTT_TOPIC import com.kernelmaft.zanbur.common.Group
import kotlinx.serialization.* import com.kernelmaft.zanbur.common.Scene
import kotlinx.serialization.json.* import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
fun publishSceneChange ( group : Group , newScene : Scene ) { fun publishSceneChange(group: Group, newScene: Scene) {
val topic = MQTT_TOPIC + "/" + group . name + "/set" val topic = Config.MQTT_TOPIC + "/" + group.name + "/set"
val packet = Json . encodeToString ( SceneRecallPacket ( newScene . id ) ) val packet = Json.encodeToString(SceneRecallPacket(newScene.id))
. toByteArray () .toByteArray()
. asUByteArray () .asUByteArray()
MqttClient . publish ( topic , packet ) MqttClient.publish(topic, packet)
} }
@Serializable private data class SceneRecallPacket ( @Serializable
@SerialName ("scene_recall") val sceneRecall : Int , private data class SceneRecallPacket(
@SerialName("scene_recall") val sceneRecall: Int,
) )

View file

@ -1,46 +1,62 @@
package com.kernelmaft.zanbur.ui package com.kernelmaft.zanbur.ui
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement.Center import androidx.compose.foundation.layout.Column
import androidx.compose.material3.* import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonDefaults.buttonColors import androidx.compose.material3.ButtonDefaults.buttonColors
import androidx.compose.material3.ButtonDefaults.filledTonalButtonColors import androidx.compose.material3.ButtonDefaults.filledTonalButtonColors
import androidx.compose.material3.ButtonDefaults.shape import androidx.compose.material3.Scaffold
import androidx.compose.runtime.* import androidx.compose.material3.Text
import androidx.compose.ui.* import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Alignment
import com.kernelmaft.zanbur.common.* import androidx.compose.ui.Modifier
import com.kernelmaft.zanbur.common.Group
import com.kernelmaft.zanbur.common.Scene
@Composable fun AppFrame ( content : @Composable () -> Unit ) { @Composable
fun AppFrame(content: @Composable () -> Unit) {
ZanburTheme { ZanburTheme {
Scaffold { scaffoldPadding -> Scaffold { scaffoldPadding ->
CenteringColumn ( Modifier . padding (scaffoldPadding) . fillMaxSize () ) { CenteringColumn(
content () Modifier
.padding(scaffoldPadding)
.fillMaxSize()
) {
content()
} }
} }
} }
} }
@Composable fun SceneSwitcher ( group : Group , onSwitch : (Scene) -> Unit ) { @Composable
fun SceneSwitcher(group: Group, onSwitch: (Scene) -> Unit) {
CenteringColumn { CenteringColumn {
Text ( group . name ) Text(group.name)
Spacer ( Modifier . height ( compactSpacing ) ) Spacer(Modifier.height(compactSpacing))
for ( scene in group . scenes ) { for (scene in group.scenes) {
val colors = val colors =
if ( scene . id == group . currentScene ?. id ) buttonColors () if (scene.id == group.currentScene?.id) buttonColors()
else filledTonalButtonColors () else filledTonalButtonColors()
Button ( { onSwitch (scene) } , Modifier . fillMaxWidth () , true , shape , colors ) { Button({ onSwitch(scene) }, Modifier.fillMaxWidth(), true, ButtonDefaults.shape, colors) {
Text ( scene . name ) Text(scene.name)
} }
} }
} }
} }
@Composable fun CenteringColumn ( @Composable
modifier : Modifier = Modifier , fun CenteringColumn(
content : @Composable ColumnScope . () -> Unit , modifier: Modifier = Modifier,
) = Column ( modifier , Center , CenterHorizontally , content ) content: @Composable ColumnScope.() -> Unit,
) = Column(modifier, Arrangement.Center, Alignment.CenterHorizontally, content)

View file

@ -1,21 +1,25 @@
package com.kernelmaft.zanbur.ui package com.kernelmaft.zanbur.ui
import androidx.compose.runtime.* import androidx.compose.runtime.MutableState
import com.kernelmaft.zanbur.common.* import androidx.compose.runtime.mutableStateOf
import com.kernelmaft.zanbur.common.AppState
import com.kernelmaft.zanbur.common.Group
fun createGroupsComposeState () : MutableState < List <Group> > { fun createGroupsComposeState(): MutableState<List<Group>> {
val groups : MutableState < List <Group> > = mutableStateOf ( emptyList () ) val groups: MutableState<List<Group>> = mutableStateOf(emptyList())
AppState . subscribeToGroupAdded { newGroup , _ -> AppState.subscribeToGroupAdded { newGroup, _ ->
groups . value = groups . value . plus (newGroup) groups.value = groups.value.plus(newGroup)
} }
AppState . subscribeToCurrentScene { group , newScene , source -> AppState.subscribeToCurrentScene { group, newScene, source ->
groups . value = groups . value . map { when ( it . id ) { groups.value = groups.value.map {
group . id -> it . copy ( currentScene = newScene ) when (it.id) {
group.id -> it.copy(currentScene = newScene)
else -> it else -> it
} } }
}
} }
return groups return groups

View file

@ -1,43 +1,46 @@
package com.kernelmaft.zanbur.ui package com.kernelmaft.zanbur.ui
import android.os.* import android.os.Bundle
import androidx.activity.compose.* import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Column
import androidx.compose.ui.* import androidx.compose.foundation.layout.width
import androidx.compose.ui.unit.* import androidx.compose.ui.Modifier
import androidx.lifecycle.* import androidx.compose.ui.unit.dp
import com.kernelmaft.zanbur.common.* import androidx.lifecycle.lifecycleScope
import com.kernelmaft.zanbur.common.ChangeSource.* import com.kernelmaft.zanbur.common.AppState
import com.kernelmaft.zanbur.network.* import com.kernelmaft.zanbur.common.ChangeSource
import com.kernelmaft.zanbur.common.Config
import com.kernelmaft.zanbur.network.MqttClient
import com.kernelmaft.zanbur.network.publishSceneChange
class MainActivity : EdgeToEdgeActivity () { class MainActivity : EdgeToEdgeActivity() {
override fun onCreate ( savedInstanceState : Bundle ? ) { override fun onCreate(savedInstanceState: Bundle?) {
super . onCreate (savedInstanceState) super.onCreate(savedInstanceState)
val groups = createGroupsComposeState () val groups = createGroupsComposeState()
AppState . subscribeToCurrentScene { group , newScene , source -> AppState.subscribeToCurrentScene { group, newScene, source ->
if ( source == Local ) { if (source == ChangeSource.Local) {
publishSceneChange ( group , newScene ) publishSceneChange(group, newScene)
} }
} }
Config . groups . forEach { AppState . addGroup ( it , Remote ) } Config.groups.forEach { AppState.addGroup(it, ChangeSource.Remote) }
setContent { setContent {
AppFrame { AppFrame {
Column ( Modifier . width ( 300 . dp ) ) { Column(Modifier.width(300.dp)) {
groups . value . forEach { group -> groups.value.forEach { group ->
SceneSwitcher (group) { newScene -> SceneSwitcher(group) { newScene ->
AppState . setCurrentScene ( group , newScene , Local ) AppState.setCurrentScene(group, newScene, ChangeSource.Local)
} }
} }
} }
} }
} }
MqttClient . run (lifecycleScope) MqttClient.run(lifecycleScope)
} }
} }

View file

@ -1,34 +1,35 @@
package com.kernelmaft.zanbur.ui package com.kernelmaft.zanbur.ui
import android.os.* import android.os.Bundle
import androidx.activity.* import androidx.activity.ComponentActivity
import androidx.compose.foundation.* import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.shapes import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.dp
val compactSpacing = 16 . dp val compactSpacing = 16.dp
@Composable fun ZanburTheme ( content : @Composable () -> Unit ) { @Composable
fun ZanburTheme(content: @Composable () -> Unit) {
val colorScheme = run { val colorScheme = run {
if ( isSystemInDarkTheme () ) if (isSystemInDarkTheme())
dynamicDarkColorScheme ( LocalContext . current ) dynamicDarkColorScheme(LocalContext.current)
else else
dynamicLightColorScheme ( LocalContext . current ) dynamicLightColorScheme(LocalContext.current)
} }
MaterialTheme ( colorScheme, shapes , typography , content ) MaterialTheme(colorScheme, MaterialTheme.shapes, MaterialTheme.typography, content)
} }
open class EdgeToEdgeActivity : ComponentActivity () { open class EdgeToEdgeActivity : ComponentActivity() {
override fun onCreate ( savedInstanceState : Bundle ? ) { override fun onCreate(savedInstanceState: Bundle?) {
super . onCreate (savedInstanceState) super.onCreate(savedInstanceState)
actionBar ?. hide () actionBar?.hide()
} }
} }

View file

@ -1,16 +1,16 @@
pluginManagement { pluginManagement {
repositories { repositories {
google () google()
mavenCentral () mavenCentral()
gradlePluginPortal () gradlePluginPortal()
} }
} }
dependencyResolutionManagement { dependencyResolutionManagement {
repositories { repositories {
google () google()
mavenCentral () mavenCentral()
} }
} }
rootProject . name = "Zanbur" rootProject.name = "Zanbur"
include (":app") include(":app")