From b052df81d8e7986247080263ffed82ba6c294f9a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 08:58:30 -0500 Subject: [PATCH 01/63] initial transition to jetpack changed lintOptions -> lint, packagingOptions -> packaging removed package from manifest added context specifiers to broadcasters upgraded bleBeacon, ste, meshtastic app compact to androidx app compact upgraded gradle to 8.2, target sdk 34 added kotlin support changed switch case for navigation to if else --- build.gradle | 6 ++++-- gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- sensorhub-android-app/AndroidManifest.xml | 1 - sensorhub-android-app/build.gradle | 20 +++++++++---------- .../org/sensorhub/android/MainActivity.java | 19 ++++++++---------- .../sensorhub/android/SOSServiceWithIPC.java | 2 +- .../sensor/swe/ProxySensor/ProxySensor.java | 2 +- sensorhub-android-blebeacon/build.gradle | 2 +- .../src/main/AndroidManifest.xml | 3 +-- .../AndroidManifest.xml | 1 - sensorhub-android-controller/build.gradle | 2 +- sensorhub-android-flirone/AndroidManifest.xml | 1 - sensorhub-android-lib/AndroidManifest.xml | 1 - sensorhub-android-lib/build.gradle | 9 +++++---- sensorhub-android-meshtastic/build.gradle | 2 +- .../src/main/AndroidManifest.xml | 5 +---- sensorhub-android-polar/AndroidManifest.xml | 3 +-- sensorhub-android-polar/build.gradle | 2 +- sensorhub-android-service/AndroidManifest.xml | 1 - sensorhub-android-service/build.gradle | 2 +- .../android/comm/BluetoothManager.java | 2 +- sensorhub-android-ste/build.gradle | 2 +- .../src/main/AndroidManifest.xml | 5 +---- .../AndroidManifest.xml | 3 +-- sensorhub-android-template/build.gradle | 2 +- .../AndroidManifest.xml | 3 +-- sensorhub-android-wardriving/build.gradle | 2 +- .../impl/sensor/wardriving/Wardriving.java | 2 +- sensorhub-driver-android/AndroidManifest.xml | 1 - sensorhub-driver-android/build.gradle | 2 +- 31 files changed, 47 insertions(+), 64 deletions(-) diff --git a/build.gradle b/build.gradle index 46c4df04..3428c442 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,20 @@ ext.oshCoreVersion = '2.0.0-beta' ext.compileSdkVersion = 34 ext.minSdkVersion = 33 -ext.targetSdkVersion = 30 +ext.targetSdkVersion = 34 ext.buildToolsVersion = "34.0.0" version = '4.0.2' buildscript { + ext.kotlin_version = '1.9.22' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'com.android.tools.build:gradle:8.2.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/gradle.properties b/gradle.properties index 94adc3a3..70df13bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true +android.nonTransitiveRClass=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c684..15de9024 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index c19d1841..259971e7 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -1,7 +1,6 @@ diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index cc935964..ff862abf 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' description = 'OSH Android App' ext.details = 'OSH app for Android' @@ -35,6 +36,8 @@ dependencies { implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.github.tony19:logback-android:3.0.0' + implementation 'androidx.core:core-ktx:1.3.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } allprojects { @@ -69,10 +72,10 @@ android { targetCompatibility JavaVersion.VERSION_17 } - packagingOptions { - exclude 'META-INF/LICENSE' - exclude '**/empty' - exclude 'META-INF/rxjava.properties' + packaging { + resources { + excludes += ['META-INF/LICENSE', '**/empty', 'META-INF/rxjava.properties', 'META-INF/INDEX.LIST'] + } } @@ -85,18 +88,13 @@ android { } } - lintOptions { + lint { abortOnError false } + buildFeatures { viewBinding true } - packagingOptions { - resources { - excludes += ["META-INF/INDEX.LIST"] - } - } - } diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 1d6c2caf..0e6133fc 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -630,16 +630,13 @@ else if (sensorsFragment != null && !sensorsFragment.isHidden()) BottomNavigationView bottomNav = findViewById(R.id.bottom_nav); bottomNav.setOnNavigationItemSelectedListener(item -> { - switch (item.getItemId()) { - case R.id.dashboard: - switchFragment(homeFragment, getString(R.string.app_name)); - break; - case R.id.sensors: - switchFragment(sensorsFragment, getString(R.string.tab_sensors)); - break; - case R.id.settings: - switchFragment(settingsFragment, getString(R.string.tab_settings)); - break; + int id = item.getItemId(); + if (id == R.id.dashboard) { + switchFragment(homeFragment, getString(R.string.app_name)); + } else if (id == R.id.sensors) { + switchFragment(sensorsFragment, getString(R.string.tab_sensors)); + } else if (id == R.id.settings) { + switchFragment(settingsFragment, getString(R.string.tab_settings)); } return true; }); @@ -865,6 +862,6 @@ public void onReceive(Context context, Intent intent) { }; IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_BROADCAST_RECEIVER); - registerReceiver(broadcastReceiver, filter); + registerReceiver(broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED); } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java index 2cd5d5e3..e816a63a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java @@ -54,7 +54,7 @@ public void onReceive(Context context, Intent intent) IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_SOS); - androidContext.registerReceiver(receiver, filter); + androidContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); } @Override diff --git a/sensorhub-android-app/src/org/sensorhub/impl/sensor/swe/ProxySensor/ProxySensor.java b/sensorhub-android-app/src/org/sensorhub/impl/sensor/swe/ProxySensor/ProxySensor.java index 1d839606..57b1b019 100644 --- a/sensorhub-android-app/src/org/sensorhub/impl/sensor/swe/ProxySensor/ProxySensor.java +++ b/sensorhub-android-app/src/org/sensorhub/impl/sensor/swe/ProxySensor/ProxySensor.java @@ -72,7 +72,7 @@ public void onReceive(Context context, Intent intent) { IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_PROXY); - androidContext.registerReceiver(receiver, filter); + androidContext.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); Log.d(TAG, "Starting Proxy Sensor"); diff --git a/sensorhub-android-blebeacon/build.gradle b/sensorhub-android-blebeacon/build.gradle index e8002a7e..018076e9 100644 --- a/sensorhub-android-blebeacon/build.gradle +++ b/sensorhub-android-blebeacon/build.gradle @@ -36,7 +36,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'androidx.appcompat:appcompat:1.6.1' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' diff --git a/sensorhub-android-blebeacon/src/main/AndroidManifest.xml b/sensorhub-android-blebeacon/src/main/AndroidManifest.xml index f6a6a09d..a2f47b60 100644 --- a/sensorhub-android-blebeacon/src/main/AndroidManifest.xml +++ b/sensorhub-android-blebeacon/src/main/AndroidManifest.xml @@ -1,3 +1,2 @@ - + diff --git a/sensorhub-android-controller/AndroidManifest.xml b/sensorhub-android-controller/AndroidManifest.xml index 26edfd10..d55efa33 100644 --- a/sensorhub-android-controller/AndroidManifest.xml +++ b/sensorhub-android-controller/AndroidManifest.xml @@ -1,6 +1,5 @@ diff --git a/sensorhub-android-controller/build.gradle b/sensorhub-android-controller/build.gradle index 1dbc2de0..67f9a5b5 100644 --- a/sensorhub-android-controller/build.gradle +++ b/sensorhub-android-controller/build.gradle @@ -27,7 +27,7 @@ android { targetSdkVersion rootProject.targetSdkVersion } - lintOptions { + lint { abortOnError false } diff --git a/sensorhub-android-flirone/AndroidManifest.xml b/sensorhub-android-flirone/AndroidManifest.xml index 2f9f4968..b9bb3103 100644 --- a/sensorhub-android-flirone/AndroidManifest.xml +++ b/sensorhub-android-flirone/AndroidManifest.xml @@ -1,5 +1,4 @@ diff --git a/sensorhub-android-lib/AndroidManifest.xml b/sensorhub-android-lib/AndroidManifest.xml index 97065f54..3eef495c 100644 --- a/sensorhub-android-lib/AndroidManifest.xml +++ b/sensorhub-android-lib/AndroidManifest.xml @@ -1,5 +1,4 @@ diff --git a/sensorhub-android-lib/build.gradle b/sensorhub-android-lib/build.gradle index 64f9aa0d..b011eed0 100644 --- a/sensorhub-android-lib/build.gradle +++ b/sensorhub-android-lib/build.gradle @@ -47,9 +47,10 @@ android { targetSdkVersion rootProject.targetSdkVersion } - packagingOptions { - exclude '**/empty' - exclude 'META-INF/rxjava.properties' + packaging { + resources { + excludes += ['**/empty', 'META-INF/rxjava.properties'] + } } sourceSets { @@ -61,7 +62,7 @@ android { } } - lintOptions { + lint { abortOnError false } } diff --git a/sensorhub-android-meshtastic/build.gradle b/sensorhub-android-meshtastic/build.gradle index a2b9cbf0..b1167997 100644 --- a/sensorhub-android-meshtastic/build.gradle +++ b/sensorhub-android-meshtastic/build.gradle @@ -11,7 +11,7 @@ dependencies { api project(':sensorhub-core') implementation ('org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5') implementation("com.google.protobuf:protobuf-java:4.33.0") - implementation "com.android.support:appcompat-v7:28.0.0" + implementation "androidx.appcompat:appcompat:1.6.1" implementation project(':sensorhub-android-service') implementation project(':sensorhub-driver-android') } diff --git a/sensorhub-android-meshtastic/src/main/AndroidManifest.xml b/sensorhub-android-meshtastic/src/main/AndroidManifest.xml index 5bad25b2..b2442030 100644 --- a/sensorhub-android-meshtastic/src/main/AndroidManifest.xml +++ b/sensorhub-android-meshtastic/src/main/AndroidManifest.xml @@ -1,7 +1,4 @@ - - - + diff --git a/sensorhub-android-polar/AndroidManifest.xml b/sensorhub-android-polar/AndroidManifest.xml index 7c8aba0c..bc41117b 100644 --- a/sensorhub-android-polar/AndroidManifest.xml +++ b/sensorhub-android-polar/AndroidManifest.xml @@ -1,6 +1,5 @@ + xmlns:tools="http://schemas.android.com/tools"> diff --git a/sensorhub-android-polar/build.gradle b/sensorhub-android-polar/build.gradle index bbc4c9d2..2a51d7e8 100644 --- a/sensorhub-android-polar/build.gradle +++ b/sensorhub-android-polar/build.gradle @@ -35,7 +35,7 @@ android { targetSdkVersion rootProject.targetSdkVersion } - lintOptions { + lint { abortOnError false } diff --git a/sensorhub-android-service/AndroidManifest.xml b/sensorhub-android-service/AndroidManifest.xml index 207c451c..fcab6cce 100644 --- a/sensorhub-android-service/AndroidManifest.xml +++ b/sensorhub-android-service/AndroidManifest.xml @@ -1,5 +1,4 @@ diff --git a/sensorhub-android-service/build.gradle b/sensorhub-android-service/build.gradle index fc30c826..2af35e2a 100644 --- a/sensorhub-android-service/build.gradle +++ b/sensorhub-android-service/build.gradle @@ -43,7 +43,7 @@ android { targetCompatibility JavaVersion.VERSION_17 } - lintOptions { + lint { abortOnError false } diff --git a/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java b/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java index 3d47b583..cfe275db 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java +++ b/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java @@ -94,7 +94,7 @@ public void onReceive(Context context, Intent intent) } } } - }, filter); // Don't forget to unregister during onDestroy + }, filter, Context.RECEIVER_EXPORTED); // Don't forget to unregister during onDestroy // Getting the Bluetooth adapter BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); diff --git a/sensorhub-android-ste/build.gradle b/sensorhub-android-ste/build.gradle index 7d649d06..f64388fd 100644 --- a/sensorhub-android-ste/build.gradle +++ b/sensorhub-android-ste/build.gradle @@ -5,7 +5,7 @@ ext.details = 'Driver for STE Radiation Pager' dependencies { api project(':sensorhub-core') - implementation "com.android.support:appcompat-v7:28.0.0" + implementation "androidx.appcompat:appcompat:1.6.1" implementation project(':sensorhub-android-service') implementation project(':sensorhub-driver-android') diff --git a/sensorhub-android-ste/src/main/AndroidManifest.xml b/sensorhub-android-ste/src/main/AndroidManifest.xml index b97ad3b0..b2442030 100644 --- a/sensorhub-android-ste/src/main/AndroidManifest.xml +++ b/sensorhub-android-ste/src/main/AndroidManifest.xml @@ -1,7 +1,4 @@ - - - + diff --git a/sensorhub-android-template/AndroidManifest.xml b/sensorhub-android-template/AndroidManifest.xml index d2a3abe1..6f6edbca 100644 --- a/sensorhub-android-template/AndroidManifest.xml +++ b/sensorhub-android-template/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/sensorhub-android-template/build.gradle b/sensorhub-android-template/build.gradle index be7d7faa..a2aa12d0 100644 --- a/sensorhub-android-template/build.gradle +++ b/sensorhub-android-template/build.gradle @@ -30,7 +30,7 @@ android { targetSdkVersion rootProject.targetSdkVersion } - lintOptions { + lint { abortOnError false } diff --git a/sensorhub-android-wardriving/AndroidManifest.xml b/sensorhub-android-wardriving/AndroidManifest.xml index d2a3abe1..6f6edbca 100644 --- a/sensorhub-android-wardriving/AndroidManifest.xml +++ b/sensorhub-android-wardriving/AndroidManifest.xml @@ -1,5 +1,4 @@ - + diff --git a/sensorhub-android-wardriving/build.gradle b/sensorhub-android-wardriving/build.gradle index ef5842a0..5ef089c0 100644 --- a/sensorhub-android-wardriving/build.gradle +++ b/sensorhub-android-wardriving/build.gradle @@ -30,7 +30,7 @@ android { targetSdkVersion rootProject.targetSdkVersion } - lintOptions { + lint { abortOnError false } diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java index f806e839..f2f2fd24 100644 --- a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java @@ -133,7 +133,7 @@ public void onReceive(Context ctx, Intent intent) { }; IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); - context.registerReceiver(wifiReceiver, filter); + context.registerReceiver(wifiReceiver, filter, Context.RECEIVER_EXPORTED); // start GPS location updates startLocationUpdates(); diff --git a/sensorhub-driver-android/AndroidManifest.xml b/sensorhub-driver-android/AndroidManifest.xml index e9a28838..701a6c98 100644 --- a/sensorhub-driver-android/AndroidManifest.xml +++ b/sensorhub-driver-android/AndroidManifest.xml @@ -1,5 +1,4 @@ diff --git a/sensorhub-driver-android/build.gradle b/sensorhub-driver-android/build.gradle index 3a68a1b2..aeca0d46 100644 --- a/sensorhub-driver-android/build.gradle +++ b/sensorhub-driver-android/build.gradle @@ -29,7 +29,7 @@ android { targetSdkVersion rootProject.targetSdkVersion } - lintOptions { + lint { abortOnError false } From ba2a8add3909db0d5b83509f4d569a91fb8b10dd Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 09:00:19 -0500 Subject: [PATCH 02/63] transitioned ble bleacon to use android x dependencies --- gradle.properties | 1 - sensorhub-android-blebeacon/build.gradle | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 70df13bf..0237ab84 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true -android.enableJetifier=true android.nonTransitiveRClass=true diff --git a/sensorhub-android-blebeacon/build.gradle b/sensorhub-android-blebeacon/build.gradle index 018076e9..aae7f280 100644 --- a/sensorhub-android-blebeacon/build.gradle +++ b/sensorhub-android-blebeacon/build.gradle @@ -38,8 +38,8 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation project(path: ':sensorhub-core') // implementation 'org.altbeacon:android-beacon-library:2.16.2' // implementation 'org.altbeacon:android-beacon-library:2.13.1' From ffdf09cb35e62633f7714ab3858972b9dd28cf4f Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 09:03:21 -0500 Subject: [PATCH 03/63] Added compose to app gradle --- sensorhub-android-app/build.gradle | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index ff862abf..631c1fec 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -36,8 +36,15 @@ dependencies { implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.github.tony19:logback-android:3.0.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + // compose deps + implementation platform('androidx.compose:compose-bom:2024.02.02') + implementation 'androidx.compose.ui:ui' + implementation 'androidx.compose.ui:ui-tooling-preview' + implementation 'androidx.compose.material3:material3' + implementation 'androidx.activity:activity-compose:1.8.2' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' + debugImplementation 'androidx.compose.ui:ui-tooling' + debugImplementation 'androidx.compose.ui:ui-test-manifest' } allprojects { @@ -47,9 +54,9 @@ allprojects { exclude group: 'ch.qos.logback', module: 'logback-core' resolutionStrategy { - force "org.jetbrains.kotlin:kotlin-stdlib:1.8.10" - force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10" - force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10" + force "org.jetbrains.kotlin:kotlin-stdlib:1.9.22" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.22" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.22" } } } @@ -72,6 +79,10 @@ android { targetCompatibility JavaVersion.VERSION_17 } + kotlinOptions { + jvmTarget = '17' + } + packaging { resources { excludes += ['META-INF/LICENSE', '**/empty', 'META-INF/rxjava.properties', 'META-INF/INDEX.LIST'] @@ -94,6 +105,12 @@ android { buildFeatures { viewBinding true + compose true + buildConfig true + } + + composeOptions { + kotlinCompilerExtensionVersion = '1.5.10' } } From 63e010ca12d2426fd974ff3b9fb788fe701816b0 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 14:42:42 -0500 Subject: [PATCH 04/63] create theme, and reusable components: button, topappbar, and switch --- sensorhub-android-app/build.gradle | 1 + .../sensorhub/android/ui/components/Button.kt | 101 +++++++++++ .../android/ui/components/OSHTopAppbar.kt | 167 ++++++++++++++++++ .../sensorhub/android/ui/components/Switch.kt | 45 +++++ .../org/sensorhub/android/ui/theme/Color.kt | 75 ++++++++ .../org/sensorhub/android/ui/theme/Theme.kt | 59 +++++++ 6 files changed, 448 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Switch.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/theme/Theme.kt diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 631c1fec..73cb9286 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -41,6 +41,7 @@ dependencies { implementation 'androidx.compose.ui:ui' implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material-icons-extended' implementation 'androidx.activity:activity-compose:1.8.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' debugImplementation 'androidx.compose.ui:ui-tooling' diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt new file mode 100644 index 00000000..44a4a262 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -0,0 +1,101 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import org.sensorhub.android.ui.theme.* + +private val PillShape = RoundedCornerShape(10) + +@Composable +fun OSHButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = PillShape, + colors = ButtonDefaults.buttonColors( + containerColor = Primary, + contentColor = OnPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 8.dp, + pressedElevation = 12.dp, + disabledElevation = 0.dp + ) + ) { + Text(text) + } +} + +@Composable +fun OSHOutlinedButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + OutlinedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = PillShape, + border = BorderStroke(1.dp, Outline), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = TextPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 8.dp, + pressedElevation = 12.dp, + disabledElevation = 0.dp + ) + ) { + Text(text) + } +} + +@Composable +fun OSHTextButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = PillShape, + colors = ButtonDefaults.textButtonColors( + contentColor = TextSecondary + ) + ) { + Text(text) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ButtonPreview() { + OSHTheme { + Column { + OSHButton(onClick = {}, text = "Primary Button") + Spacer(modifier = Modifier.height(8.dp)) + OSHOutlinedButton(onClick = {}, text = "Outlined Button") + Spacer(modifier = Modifier.height(8.dp)) + OSHTextButton(onClick = {}, text = "Text Button") + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt new file mode 100644 index 00000000..342d3bcd --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt @@ -0,0 +1,167 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.R +import org.sensorhub.android.ui.theme.* + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OSHTopAppBarWithLogo( + title: String, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {} +) { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.logo), + contentDescription = "OSH Logo", + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(title) + } + }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = ToolbarBg, + titleContentColor = ToolbarTitle, + navigationIconContentColor = ToolbarTitle, + actionIconContentColor = ToolbarTitle + ) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OSHTopAppBarWithLogoAndActions( + title: String, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, +) { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(id = R.drawable.logo), + contentDescription = "OSH Logo", + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(title) + } + }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.Menu, + contentDescription = "Localized description" + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = ToolbarBg, + titleContentColor = ToolbarTitle, + navigationIconContentColor = ToolbarTitle, + actionIconContentColor = ToolbarTitle + ) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OSHTopAppBar( + title: String, + modifier: Modifier = Modifier, + navigationIcon: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {} +) { + TopAppBar( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(title) + } + }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = actions, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = ToolbarBg, + titleContentColor = ToolbarTitle, + navigationIconContentColor = ToolbarTitle, + actionIconContentColor = ToolbarTitle + ) + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OSHTopAppBarWithBack( + title: String, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {} +) { + OSHTopAppBar( + title = title, + modifier = modifier, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = actions + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun TopAppBarPreview() { + OSHTheme { + OSHTopAppBarWithLogo(title = "OpenSensorHub") + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun TopAppBarWithBackPreview() { + OSHTheme { + OSHTopAppBarWithBack(title = "Server Profiles", onBackClick = {}) + } +} + + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun TopAppBarWithLogoAndActionPreview() { + OSHTheme { + OSHTopAppBarWithLogoAndActions(title = "Settings") + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Switch.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Switch.kt new file mode 100644 index 00000000..648a68e9 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Switch.kt @@ -0,0 +1,45 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import org.sensorhub.android.ui.theme.* + + +@Composable +fun OSHSwitch( + enabled: Boolean = true, + checked: Boolean = true, + onCheckedChange: ((Boolean) -> Unit)? +) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + colors = SwitchDefaults.colors( + checkedThumbColor = OnPrimary, + checkedTrackColor = PrimaryContainer, + uncheckedThumbColor = Secondary, + uncheckedTrackColor = SecondaryContainer, + ) ) +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SwitchPreview() { + var checked by remember { mutableStateOf(true) } + + OSHTheme { + Column { + OSHSwitch(true, checked, onCheckedChange = { + checked = it + }) + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt new file mode 100644 index 00000000..48086d5b --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt @@ -0,0 +1,75 @@ +package org.sensorhub.android.ui.theme + +import androidx.compose.ui.graphics.Color + +// Primary +val Primary = Color(0xFFE64A19) +val OnPrimary = Color(0xFFFFFFFF) +val PrimaryContainer = Color(0xFFE64A19) +val OnPrimaryContainer = Color(0xFFFFCCBC) +val PrimaryDark = Color(0xFFBF360C) + +// Secondary +val Secondary = Color(0xFF9E9E9E) +val OnSecondary = Color(0xFF0A0A0A) +val SecondaryContainer = Color(0xFF272727) +val OnSecondaryContainer = Color(0xFFE0E0E0) + +// Tertiary +val Tertiary = Color(0xFFFF7043) +val OnTertiary = Color(0xFFFFFFFF) +val TertiaryContainer = Color(0xFF4E1700) +val OnTertiaryContainer = Color(0xFFFFCCBC) + +// Error +val Error = Color(0xFFCF6679) +val OnError = Color(0xFF1C0006) +val ErrorContainer = Color(0xFF93000A) +val OnErrorContainer = Color(0xFFFFDAD6) + +// Background & Surface +val Background = Color(0xFF121212) +val OnBackground = Color(0xFFE0E0E0) +val Surface = Color(0xFF1E1E1E) +val OnSurface = Color(0xFFE0E0E0) +val SurfaceVariant = Color(0xFF2C2C2C) +val OnSurfaceVariant = Color(0xFFBDBDBD) +val Outline = Color(0xFF616161) + +// Surface levels +val SurfaceLow = Color(0xFF1E1E1E) +val SurfaceCard = Color(0xFF2C2C2C) +val SurfaceElevated = Color(0xFF333333) + +// Text +val TextPrimary = Color(0xFFFFFFFF) +val TextSecondary = Color(0xFFBDBDBD) +val TextMuted = Color(0xFF757575) + +// Accent +val AccentOrange = Color(0xFFE64A19) +val AccentOrangeDark = Color(0xFFBF360C) +val AccentOrangeDim = Color(0x1AE64A19) + +// Toolbar +val ToolbarBg = Color(0xFFE64A19) +val ToolbarTitle = Color(0xFFFFFFFF) + +// Bottom Nav +val BottomNavBg = Color(0xFF1E1E1E) +val BottomNavSelected = Color(0xFFE64A19) +val BottomNavUnselected = Color(0xFF757575) + +// Status +val StatusStarted = Color(0xFF4CAF50) +val StatusStopped = Color(0xFFEF5350) +val StatusInitializing = Color(0xFFFF9800) +val StatusUnknown = Color(0xFF757575) + +// Overlays +val OverlayLight = Color(0x1AFFFFFF) +val OverlayDark = Color(0x99000000) + +// Cards & Dialogs +val CardInfoBg = Color(0xFF212121) +val DialogBg = Color(0xFF212121) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Theme.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Theme.kt new file mode 100644 index 00000000..0d7fc3bf --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Theme.kt @@ -0,0 +1,59 @@ +package org.sensorhub.android.ui.theme + +import android.app.Activity +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +private val OSHColorScheme = darkColorScheme( + primary = Primary, + onPrimary = OnPrimary, + primaryContainer = PrimaryContainer, + onPrimaryContainer = OnPrimaryContainer, + secondary = Secondary, + onSecondary = OnSecondary, + secondaryContainer = SecondaryContainer, + onSecondaryContainer = OnSecondaryContainer, + tertiary = Tertiary, + onTertiary = OnTertiary, + tertiaryContainer = TertiaryContainer, + onTertiaryContainer = OnTertiaryContainer, + error = Error, + onError = OnError, + errorContainer = ErrorContainer, + onErrorContainer = OnErrorContainer, + background = Background, + onBackground = OnBackground, + surface = Surface, + onSurface = OnSurface, + surfaceVariant = SurfaceVariant, + onSurfaceVariant = OnSurfaceVariant, + outline = Outline +) + +@Composable +fun OSHTheme(content: @Composable () -> Unit) { + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + window.statusBarColor = AccentOrange.toArgb() + window.navigationBarColor = SurfaceLow.toArgb() + WindowCompat.getInsetsController(window, view).apply { + isAppearanceLightStatusBars = false + isAppearanceLightNavigationBars = false + } + } + } + + MaterialTheme( + colorScheme = OSHColorScheme, + typography = OSHTypography, + shapes = OSHShapes, + content = content + ) +} From 3526d292e5e3b8b0ddcefc9f6d371146d4d92a61 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 15:54:12 -0500 Subject: [PATCH 05/63] removed unneeded appbar --- .../components/{OSHTopAppbar.kt => Appbar.kt} | 47 ------------------- .../sensorhub/android/ui/components/Button.kt | 2 +- 2 files changed, 1 insertion(+), 48 deletions(-) rename sensorhub-android-app/src/org/sensorhub/android/ui/components/{OSHTopAppbar.kt => Appbar.kt} (70%) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Appbar.kt similarity index 70% rename from sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/components/Appbar.kt index 342d3bcd..f35439b3 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHTopAppbar.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Appbar.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -51,44 +50,6 @@ fun OSHTopAppBarWithLogo( ) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun OSHTopAppBarWithLogoAndActions( - title: String, - modifier: Modifier = Modifier, - navigationIcon: @Composable () -> Unit = {}, -) { - TopAppBar( - title = { - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - painter = painterResource(id = R.drawable.logo), - contentDescription = "OSH Logo", - modifier = Modifier.size(32.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(title) - } - }, - modifier = modifier, - navigationIcon = navigationIcon, - actions = { - IconButton(onClick = { /* do something */ }) { - Icon( - imageVector = Icons.Filled.Menu, - contentDescription = "Localized description" - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = ToolbarBg, - titleContentColor = ToolbarTitle, - navigationIconContentColor = ToolbarTitle, - actionIconContentColor = ToolbarTitle - ) - ) -} - @OptIn(ExperimentalMaterial3Api::class) @Composable fun OSHTopAppBar( @@ -157,11 +118,3 @@ private fun TopAppBarWithBackPreview() { } -@OptIn(ExperimentalMaterial3Api::class) -@Preview(showBackground = true, backgroundColor = 0xFF121212) -@Composable -private fun TopAppBarWithLogoAndActionPreview() { - OSHTheme { - OSHTopAppBarWithLogoAndActions(title = "Settings") - } -} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt index 44a4a262..e99c4d1c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import org.sensorhub.android.ui.theme.* -private val PillShape = RoundedCornerShape(10) +private val PillShape = RoundedCornerShape(25) @Composable fun OSHButton( From bee030de1bf893bf78dfaec3775b1173f38b5150 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 16:19:04 -0500 Subject: [PATCH 06/63] Create different types of cards --- .../sensorhub/android/ui/components/Card.kt | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt new file mode 100644 index 00000000..b9424cf3 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -0,0 +1,280 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.theme.* + +@Composable +fun OSHCard( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier.fillMaxWidth().padding(16.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Secondary, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + content = content + ) +} + + +@Composable +fun OSHClickableCard( + onCardClick: () -> Unit, + title: String, + subtitle: String, + modifier: Modifier = Modifier +) { + Card( + onClick = onCardClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.width(30.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(10.dp)) + + Icon(Icons.Filled.ChevronRight, contentDescription = null) + + } + } +} + + +@Composable +fun OSHSwitchCard( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + OSHSwitch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } + } +} + +@Composable +fun OSHActionCard( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + onClicked: () -> Unit, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + OSHSwitch( + checked = checked, + onCheckedChange = onCheckedChange + ) + IconButton(onClick = { onClicked() }) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun CardPreview() { + OSHTheme { + Column { + OSHCard { + Text( + text = "Card content goes here", + modifier = Modifier.padding(16.dp) + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SwitchCardPreview() { + OSHTheme { + Column { + OSHSwitchCard( + title = "Accelerometer", + subtitle = "Stream real-time accelerometer motion data", + checked = true, + onCheckedChange = {} + ) + OSHSwitchCard( + title = "GPS", + subtitle = "Stream location data", + checked = false, + onCheckedChange = {} + ) + OSHSwitchCard( + title = "Connected Systems API", + subtitle = "OGC Standard REST API service this devices sensor data", + checked = true, + onCheckedChange = {} + ) + OSHSwitchCard( + title = "Discovery Service", + subtitle = "Service providfcing discovery based on definable rulesets", + checked = false, + onCheckedChange = {} + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SettingsItemPreview() { + OSHTheme { + Column { + OSHClickableCard( + onCardClick = {}, + title = "Manage Sensors", + subtitle = "1 of 2 server(s) enabled", + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ServerItemsCard() { + OSHTheme { + Column { + OSHActionCard( + title = "Local Server", + subtitle = "http:localhost:8080/sensorhub/api", + checked = true, + onCheckedChange = {}, + onClicked = {}, + imageVector = Icons.Filled.Edit, + contentDescription = "Edit Server" + ) + } + } +} \ No newline at end of file From bc9d5615598e4edf59d160ae47136e6348ca0242 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 16:39:32 -0500 Subject: [PATCH 07/63] created FAB component for start/stop smarthub :0 --- .../sensorhub/android/ui/components/Fab.kt | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt new file mode 100644 index 00000000..d42b56a3 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt @@ -0,0 +1,64 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Stop +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.ui.theme.OnPrimary +import org.sensorhub.android.ui.theme.Secondary +import org.sensorhub.android.ui.theme.SecondaryContainer + + +@Composable +fun OSHFAB( + onClick: ((Boolean) -> Unit)? = null, + started: Boolean? = null, + ) { + var internalStarted by remember { mutableStateOf(false) } + val isStarted = started ?: internalStarted + val toggleStart = { + val newValue = !isStarted + if (onClick != null) { + onClick(newValue) + } else { + internalStarted = newValue + } + } + + FloatingActionButton( + onClick = { toggleStart() }, + containerColor = SecondaryContainer, + contentColor = OnPrimary + + ) { + + Icon( + imageVector = if (isStarted) + Icons.Filled.Stop + else + Icons.Filled.PlayArrow, + contentDescription = if (isStarted) "Stop SmartHub" else "Start SmartHub" + ) + + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ServerItemsCard() { + OSHTheme { + Column { + OSHFAB() + } + } +} \ No newline at end of file From f5d1c298608d26833c1a82e14468424f6be9d2ce Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 16:41:58 -0500 Subject: [PATCH 08/63] Added secondary button option --- .../sensorhub/android/ui/components/Button.kt | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt index e99c4d1c..f276d74c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -40,6 +40,32 @@ fun OSHButton( } } +@Composable +fun OSHTonalButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + Button( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = PillShape, + colors = ButtonDefaults.buttonColors( + containerColor = SecondaryContainer, + contentColor = OnPrimary + ), + elevation = ButtonDefaults.buttonElevation( + defaultElevation = 8.dp, + pressedElevation = 12.dp, + disabledElevation = 0.dp + ) + ) { + Text(text) + } +} + @Composable fun OSHOutlinedButton( onClick: () -> Unit, @@ -92,6 +118,7 @@ private fun ButtonPreview() { OSHTheme { Column { OSHButton(onClick = {}, text = "Primary Button") + OSHTonalButton(onClick = {}, text = "Secondary Button") Spacer(modifier = Modifier.height(8.dp)) OSHOutlinedButton(onClick = {}, text = "Outlined Button") Spacer(modifier = Modifier.height(8.dp)) From f13ff1415fa755054e23129a9a66246fc2236436 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 16:56:52 -0500 Subject: [PATCH 09/63] Add single select segmented buttons --- .../sensorhub/android/ui/components/Button.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt index f276d74c..5cfc490d 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -9,8 +9,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import org.sensorhub.android.ui.theme.* +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.SegmentedButton + private val PillShape = RoundedCornerShape(25) @@ -112,6 +120,37 @@ fun OSHTextButton( } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun OSHSegmentedButton( + modifier: Modifier = Modifier, + options: List +) { + var selectedIndex by remember { mutableIntStateOf(0) } + + SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { + options.forEachIndexed { index, label -> + SegmentedButton( + modifier = Modifier.weight(1f), + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = options.size + ), + onClick = { selectedIndex = index }, + selected = index == selectedIndex, + label = { Text(label, maxLines = 1) }, + colors = SegmentedButtonDefaults.colors( + activeContainerColor = Primary, + activeContentColor = OnPrimary, + activeBorderColor = Primary + ), + icon = {}, + ) + } + } +} + @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun ButtonPreview() { @@ -126,3 +165,13 @@ private fun ButtonPreview() { } } } + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SegmentedButtonPreview() { + OSHTheme { + OSHSegmentedButton( + options = listOf("CS API Client", "SOS-T Client") + ) + } +} From 2a4a81daf70b1781cb82e35750f45b56f0123f16 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 17:03:14 -0500 Subject: [PATCH 10/63] Add 3 main screens --- .../android/ui/screens/HomeScreen.kt | 22 ++++++++++++++++++ .../android/ui/screens/SensorsScreen.kt | 23 +++++++++++++++++++ .../android/ui/screens/SettingsScreen.kt | 22 ++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt new file mode 100644 index 00000000..842febd5 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -0,0 +1,22 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun HomeScreen() { + Box (modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ){ + Text( + text = "Home Screen", + style = MaterialTheme.typography.headlineLarge + ) + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt new file mode 100644 index 00000000..cdd46e48 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -0,0 +1,23 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + + +@Composable +fun SensorsScreen() { + Box (modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ){ + Text( + text = "Sensors Screen", + style = MaterialTheme.typography.headlineLarge + ) + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt new file mode 100644 index 00000000..b0beb7ef --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt @@ -0,0 +1,22 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun SettingsScreen() { + Box (modifier = Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ){ + Text( + text = "Settings Screen", + style = MaterialTheme.typography.headlineLarge + ) + } +} \ No newline at end of file From 78dd6f0e401a3fd85bfc4b0ddc6e33c97cc3889b Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 19:14:42 -0500 Subject: [PATCH 11/63] Added navbar with screens :) --- sensorhub-android-app/build.gradle | 2 + .../src/org/sensorhub/android/ui/Navbar.kt | 89 +++++++++++++++++++ .../src/org/sensorhub/android/ui/Screen.kt | 7 ++ .../org/sensorhub/android/ui/theme/Color.kt | 2 +- 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 73cb9286..2a508560 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -46,6 +46,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' debugImplementation 'androidx.compose.ui:ui-tooling' debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation("androidx.navigation:navigation-compose:2.8.4") + } allprojects { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt new file mode 100644 index 00000000..68847535 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt @@ -0,0 +1,89 @@ +package org.sensorhub.android.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Sensors +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.ui.theme.BottomNavBg +import org.sensorhub.android.ui.theme.BottomNavSelected +import org.sensorhub.android.ui.theme.BottomNavUnselected +import org.sensorhub.android.ui.theme.AccentOrangeDim + +@Composable +fun Navbar( + navController: NavController +){ + val selectedNavigationIndex = rememberSaveable { mutableIntStateOf(0) } + val navigationItems = listOf( + NavigationItem( + title = "Dashboard", + icon = Icons.Default.Home, + route = Screen.Dashboard.rout + ), + NavigationItem( + title = "Sensors", + icon = Icons.Default.Sensors, + route = Screen.Sensors.rout + ), + NavigationItem( + title = "Settings", + icon = Icons.Default.Settings, + route = Screen.Settings.rout + ) + ) + + NavigationBar( + containerColor = BottomNavBg + ) { + navigationItems.forEachIndexed { index, item -> + NavigationBarItem( + selected = selectedNavigationIndex.intValue == index, + onClick = { + selectedNavigationIndex.intValue = index + navController.navigate(item.route) + }, + icon = { + Icon(imageVector = item.icon, contentDescription = item.title) + }, + label = { + Text( + item.title, + color = if (index == selectedNavigationIndex.intValue) + BottomNavSelected + else BottomNavUnselected + ) + }, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = BottomNavSelected, + indicatorColor = AccentOrangeDim + ) + + )} + } +} + +data class NavigationItem( + val title: String, + val icon: ImageVector, + val route: String +) + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun NavbarPreview() { + org.sensorhub.android.ui.theme.OSHTheme { + Navbar(navController = rememberNavController()) + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt new file mode 100644 index 00000000..cc8c0421 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -0,0 +1,7 @@ +package org.sensorhub.android.ui + +sealed class Screen(val rout: String) { + object Dashboard: Screen("dashboard_screen") + object Settings: Screen("settings_screen") + object Sensors: Screen("sensors_screen") +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt index 48086d5b..184f2c1b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Color.kt @@ -56,7 +56,7 @@ val ToolbarBg = Color(0xFFE64A19) val ToolbarTitle = Color(0xFFFFFFFF) // Bottom Nav -val BottomNavBg = Color(0xFF1E1E1E) +val BottomNavBg = Color(0xFF121212) val BottomNavSelected = Color(0xFFE64A19) val BottomNavUnselected = Color(0xFF757575) From d4be1ebc25cd95c6765fe9f3a4efb204b3ee12bd Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 20:18:33 -0500 Subject: [PATCH 12/63] added nav host and compose main activity, started settings screen with card components --- sensorhub-android-app/AndroidManifest.xml | 6 ++ .../sensorhub/android/ComposeMainActivity.kt | 35 ++++++++ .../src/org/sensorhub/android/ui/Navbar.kt | 6 +- .../org/sensorhub/android/ui/OSHNavHost.kt | 67 +++++++++++++++ .../src/org/sensorhub/android/ui/Screen.kt | 8 +- .../android/ui/screens/SettingsScreen.kt | 81 +++++++++++++++---- 6 files changed, 181 insertions(+), 22 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 259971e7..f32781fc 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -79,6 +79,12 @@ android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="false" /> + diff --git a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt new file mode 100644 index 00000000..02b9ddf4 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt @@ -0,0 +1,35 @@ +package org.sensorhub.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.OSHNavHost +import org.sensorhub.android.ui.theme.OSHTheme + +class ComposeMainActivity: ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + OSHTheme { + MainScreen() + } + } + } +} + +@Composable +fun MainScreen() { + val navController = rememberNavController() + + Scaffold( + bottomBar = { Navbar(navController = navController) } + ) { padding -> + OSHNavHost(navController = navController, modifier = Modifier.padding(padding)) + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt index 68847535..1c342e24 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt @@ -30,17 +30,17 @@ fun Navbar( NavigationItem( title = "Dashboard", icon = Icons.Default.Home, - route = Screen.Dashboard.rout + route = Screen.Dashboard.route ), NavigationItem( title = "Sensors", icon = Icons.Default.Sensors, - route = Screen.Sensors.rout + route = Screen.Sensors.route ), NavigationItem( title = "Settings", icon = Icons.Default.Settings, - route = Screen.Settings.rout + route = Screen.Settings.route ) ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt new file mode 100644 index 00000000..f83b4736 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -0,0 +1,67 @@ +package org.sensorhub.android.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import org.sensorhub.android.R +import org.sensorhub.android.ui.screens.FaqItem +import org.sensorhub.android.ui.screens.HelpFaqScreen +import org.sensorhub.android.ui.screens.HomeScreen +import org.sensorhub.android.ui.screens.SensorsScreen +import org.sensorhub.android.ui.screens.SettingsScreen + +@Composable +fun OSHNavHost( + navController: NavHostController, + modifier: Modifier = Modifier +) { + NavHost( + navController = navController, + startDestination = Screen.Dashboard.route, + modifier = modifier + ) { + composable(Screen.Dashboard.route) { + HomeScreen() + } + composable(Screen.Sensors.route) { + SensorsScreen() + } + composable(Screen.Settings.route) { + SettingsScreen(navController = navController) + } + composable(Screen.HelpFaq.route) { + val context = LocalContext.current + val questions = context.resources.getStringArray(R.array.faq_questions) + val answers = context.resources.getStringArray(R.array.faq_answers) + val categories = context.resources.getStringArray(R.array.faq_categories) + + val faqItems = buildFaqItems(questions, answers, categories) + + HelpFaqScreen( + items = faqItems, + onBackClick = { navController.popBackStack() } + ) + } + } +} + +private fun buildFaqItems( + questions: Array, + answers: Array, + categories: Array +): List { + val items = mutableListOf() + var currentCategory = "" + for (i in questions.indices) { + val category = categories[i] + if (category != currentCategory) { + items.add(FaqItem.Header(category)) + currentCategory = category + } + items.add(FaqItem.Entry(questions[i], answers[i])) + } + return items +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index cc8c0421..5d2e8dbe 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -1,7 +1,7 @@ package org.sensorhub.android.ui -sealed class Screen(val rout: String) { - object Dashboard: Screen("dashboard_screen") - object Settings: Screen("settings_screen") - object Sensors: Screen("sensors_screen") +sealed class Screen(val route: String) { + object Dashboard : Screen("dashboard_screen") + object Settings : Screen("settings_screen") + object Sensors : Screen("sensors_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt index b0beb7ef..0e8fb957 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt @@ -1,22 +1,73 @@ package org.sensorhub.android.ui.screens -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.components.OSHClickableCard +import org.sensorhub.android.ui.components.OSHSwitchCard +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme @Composable -fun SettingsScreen() { - Box (modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ){ - Text( - text = "Settings Screen", - style = MaterialTheme.typography.headlineLarge - ) +fun SettingsScreen(navController: NavController = rememberNavController()) { + var sosEnabled by remember { mutableStateOf(false) } + var csApiEnabled by remember { mutableStateOf(false) } + var discoveryEnabled by remember { mutableStateOf(false) } + + Scaffold( + topBar = { OSHTopAppBarWithLogo("Settings") }, + bottomBar = { Navbar(navController = navController) } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + OSHClickableCard( + onCardClick = {}, + title = "Manage Servers", + subtitle = "0 of 0 server(s) enabled" + ) + OSHSwitchCard( + title = "SOS-T Service", + subtitle = "OGC SOS Transactional service", + checked = sosEnabled, + onCheckedChange = { sosEnabled = it } + ) + OSHSwitchCard( + title = "Connected Systems Service", + subtitle = "OGC Connected Systems API service", + checked = csApiEnabled, + onCheckedChange = { csApiEnabled = it } + ) + OSHSwitchCard( + title = "Discovery Service", + subtitle = "Sensor discovery based on rulesets", + checked = discoveryEnabled, + onCheckedChange = { discoveryEnabled = it } + ) + OSHClickableCard( + onCardClick = { navController.navigate(Screen.HelpFaq.route) }, + title = "Help / FAQ", + subtitle = "" + ) + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SettingsScreenPreview() { + OSHTheme { + SettingsScreen() } -} \ No newline at end of file +} From d65ddd16ad79a53dab5df614788bf0e1dac2e06c Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:43:19 -0500 Subject: [PATCH 13/63] added dialog components --- .../sensorhub/android/ui/components/Dialog.kt | 283 ++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt new file mode 100644 index 00000000..d15ed1ae --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt @@ -0,0 +1,283 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.selectable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.ui.theme.Primary + +@Composable +fun OSHAlertDialog( + onDismissRequest: () -> Unit, + onConfirmation: () -> Unit, + dialogTitle: String, + dialogText: String, + icon: ImageVector, +) { + AlertDialog( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(icon, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = dialogTitle, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + } + }, + text = { + Text(text = dialogText) + }, + onDismissRequest = { onDismissRequest() }, + confirmButton = { + OSHButton( + onClick = { onConfirmation() }, + text = "OK" + ) + }, + dismissButton = { + TextButton(onClick = { onDismissRequest() }) { + Text("Cancel") + } + } + ) +} + +@Composable +fun OSHAlertDialogWithoutDismiss( + onConfirmation: () -> Unit, + dialogTitle: String, + dialogText: String, + icon: ImageVector, +) { + AlertDialog( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(icon, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = dialogTitle, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + } + }, + text = { + Text(text = dialogText) + }, + onDismissRequest = {}, + confirmButton = { + OSHButton( + onClick = { onConfirmation() }, + text = "OK" + ) + } + ) +} + +@Composable +fun OSHTextInputDialog( + onDismissRequest: () -> Unit, + onConfirmation: (String) -> Unit, + dialogTitle: String, + dialogText: String, + icon: ImageVector, + initialValue: String = "", + placeholder: String = "" +) { + var textValue by remember { mutableStateOf(initialValue) } + + AlertDialog( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(icon, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = dialogTitle, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + } + }, + text = { + Column { + Text(text = dialogText) + Spacer(modifier = Modifier.height(12.dp)) + OutlinedTextField( + value = textValue, + onValueChange = { textValue = it }, + placeholder = { Text(placeholder) }, + singleLine = true, + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Primary, + cursorColor = Primary + ) + ) + } + }, + onDismissRequest = { onDismissRequest() }, + confirmButton = { + OSHButton( + onClick = { onConfirmation(textValue) }, + text = "OK" + ) + }, + dismissButton = { + TextButton(onClick = { onDismissRequest() }) { + Text("Cancel") + } + } + ) +} + +@Composable +fun OSHSingleChoiceDialog( + onDismissRequest: () -> Unit, + dialogTitle: String, + icon: ImageVector, + options: List, + selectedIndex: Int, + onOptionSelected: (Int) -> Unit +) { + AlertDialog( + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(icon, contentDescription = null) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = dialogTitle, + textAlign = TextAlign.Start, + modifier = Modifier.fillMaxWidth() + ) + } + }, + text = { + Column { + options.forEachIndexed { index, label -> + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = index == selectedIndex, + onClick = { + onOptionSelected(index) + onDismissRequest() + } + ) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = index == selectedIndex, + onClick = { + onOptionSelected(index) + onDismissRequest() + }, + colors = RadioButtonDefaults.colors( + selectedColor = Primary + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = label) + } + } + } + }, + onDismissRequest = { onDismissRequest() }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = { onDismissRequest() }) { + Text("Cancel") + } + } + ) +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun DialogPreview() { + OSHTheme { + OSHAlertDialog( + onConfirmation = {}, + onDismissRequest = {}, + dialogText = "Please enter a run name", + dialogTitle = "Run Name", + icon = Icons.Filled.PlayArrow + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun DialogNoDismissPreview() { + OSHTheme { + OSHAlertDialogWithoutDismiss( + onConfirmation = {}, + dialogText = "A software platform for building smart sensor networks and the internet of things", + dialogTitle = "OpenSensorHub", + icon = Icons.Filled.Info + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun TextInputDialogPreview() { + OSHTheme { + OSHTextInputDialog( + onDismissRequest = {}, + onConfirmation = {}, + dialogTitle = "Run Name", + dialogText = "Please enter the name for this run", + icon = Icons.Filled.PlayArrow, + initialValue = "Run-20260708-120000", + placeholder = "Enter run name" + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) +@Composable +private fun SingleChoiceDialogPreview() { + OSHTheme { + OSHSingleChoiceDialog( + onDismissRequest = {}, + dialogTitle = "Language", + icon = Icons.Filled.Language, + options = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch"), + selectedIndex = 0, + onOptionSelected = {} + ) + } +} + From 474b68075eb541ebbd3eb0f0b884a13658145270 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:49:57 -0500 Subject: [PATCH 14/63] added shape and typography to theme --- .../org/sensorhub/android/ui/theme/Shape.kt | 12 ++++ .../org/sensorhub/android/ui/theme/Type.kt | 69 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/theme/Shape.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/theme/Type.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Shape.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Shape.kt new file mode 100644 index 00000000..bbb366ae --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Shape.kt @@ -0,0 +1,12 @@ +package org.sensorhub.android.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +val OSHShapes = Shapes( + small = RoundedCornerShape(8.dp), + medium = RoundedCornerShape(12.dp), + large = RoundedCornerShape(16.dp), + extraLarge = RoundedCornerShape(24.dp) +) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Type.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Type.kt new file mode 100644 index 00000000..ba811d5c --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/theme/Type.kt @@ -0,0 +1,69 @@ +package org.sensorhub.android.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +val OSHTypography = Typography( + // Toolbar title + titleLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = TextPrimary + ), + // Card/section titles + titleMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + color = TextPrimary + ), + // Subtitle + titleSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = TextPrimary + ), + // Body text + bodyLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + color = TextPrimary + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + color = TextSecondary + ), + bodySmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + color = TextMuted + ), + // Labels / status + labelLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 14.sp, + color = TextPrimary + ), + labelMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + color = TextSecondary + ), + labelSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 12.sp, + color = TextMuted + ) +) From 7e0f783a87d461d9610c42e2131c1dc884366010 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:55:27 -0500 Subject: [PATCH 15/63] fixed --- .../org/sensorhub/android/ui/OSHNavHost.kt | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index f83b4736..120e4848 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -2,13 +2,9 @@ package org.sensorhub.android.ui import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import org.sensorhub.android.R -import org.sensorhub.android.ui.screens.FaqItem -import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.SettingsScreen @@ -32,36 +28,6 @@ fun OSHNavHost( composable(Screen.Settings.route) { SettingsScreen(navController = navController) } - composable(Screen.HelpFaq.route) { - val context = LocalContext.current - val questions = context.resources.getStringArray(R.array.faq_questions) - val answers = context.resources.getStringArray(R.array.faq_answers) - val categories = context.resources.getStringArray(R.array.faq_categories) - - val faqItems = buildFaqItems(questions, answers, categories) - - HelpFaqScreen( - items = faqItems, - onBackClick = { navController.popBackStack() } - ) - } } } -private fun buildFaqItems( - questions: Array, - answers: Array, - categories: Array -): List { - val items = mutableListOf() - var currentCategory = "" - for (i in questions.indices) { - val category = categories[i] - if (category != currentCategory) { - items.add(FaqItem.Header(category)) - currentCategory = category - } - items.add(FaqItem.Entry(questions[i], answers[i])) - } - return items -} From 2b3b1b7d6d307ae4cfa5289f20d9df3a56f3220c Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 09:26:59 -0500 Subject: [PATCH 16/63] refactor settings to servers --- .../src/org/sensorhub/android/ui/Navbar.kt | 7 +++-- .../org/sensorhub/android/ui/OSHNavHost.kt | 6 ++-- .../src/org/sensorhub/android/ui/Screen.kt | 2 +- .../{SettingsScreen.kt => ServersScreen.kt} | 29 ++++++++++++------- 4 files changed, 27 insertions(+), 17 deletions(-) rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{SettingsScreen.kt => ServersScreen.kt} (75%) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt index 1c342e24..3398c630 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Navbar.kt @@ -1,6 +1,7 @@ package org.sensorhub.android.ui import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Dns import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Sensors import androidx.compose.material.icons.filled.Settings @@ -38,9 +39,9 @@ fun Navbar( route = Screen.Sensors.route ), NavigationItem( - title = "Settings", - icon = Icons.Default.Settings, - route = Screen.Settings.route + title = "Servers", + icon = Icons.Default.Dns, + route = Screen.Servers.route ) ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 120e4848..073b35f4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen -import org.sensorhub.android.ui.screens.SettingsScreen +import org.sensorhub.android.ui.screens.ServersScreen @Composable fun OSHNavHost( @@ -25,8 +25,8 @@ fun OSHNavHost( composable(Screen.Sensors.route) { SensorsScreen() } - composable(Screen.Settings.route) { - SettingsScreen(navController = navController) + composable(Screen.Servers.route) { + ServersScreen(navController = navController) } } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index 5d2e8dbe..965e0f14 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -2,6 +2,6 @@ package org.sensorhub.android.ui sealed class Screen(val route: String) { object Dashboard : Screen("dashboard_screen") - object Settings : Screen("settings_screen") + object Servers : Screen("servers_screen") object Sensors : Screen("sensors_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt similarity index 75% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 0e8fb957..e8cc8e41 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SettingsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -2,6 +2,10 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -13,20 +17,30 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.ui.Navbar -import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHClickableCard import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme @Composable -fun SettingsScreen(navController: NavController = rememberNavController()) { +fun ServersScreen(navController: NavController = rememberNavController()) { var sosEnabled by remember { mutableStateOf(false) } var csApiEnabled by remember { mutableStateOf(false) } var discoveryEnabled by remember { mutableStateOf(false) } Scaffold( - topBar = { OSHTopAppBarWithLogo("Settings") }, + topBar = { + OSHTopAppBarWithLogo( + "Servers", + actions = { + IconButton(onClick = { /* do something */ }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "App preferences" + ) + } + }, + ) }, bottomBar = { Navbar(navController = navController) } ) { innerPadding -> Column( @@ -55,19 +69,14 @@ fun SettingsScreen(navController: NavController = rememberNavController()) { checked = discoveryEnabled, onCheckedChange = { discoveryEnabled = it } ) - OSHClickableCard( - onCardClick = { navController.navigate(Screen.HelpFaq.route) }, - title = "Help / FAQ", - subtitle = "" - ) } } } @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable -private fun SettingsScreenPreview() { +private fun ServersScreenPreview() { OSHTheme { - SettingsScreen() + ServersScreen() } } From 807f30a2b95126e777fbea8862c005bec9fea0db Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 10:57:45 -0500 Subject: [PATCH 17/63] updated servers screen with string resources --- sensorhub-android-app/res/values/strings.xml | 2 ++ .../android/ui/screens/ServersScreen.kt | 20 ++++++++++--------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 2525a6bc..46dd69c2 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -308,7 +308,9 @@ Sensors Home Settings + Servers App Preferences + summ Enter a message! Enter a message! diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index e8cc8e41..54bb6cd4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -21,6 +21,8 @@ import org.sensorhub.android.ui.components.OSHClickableCard import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme +import androidx.compose.ui.res.stringResource +import org.sensorhub.android.R @Composable fun ServersScreen(navController: NavController = rememberNavController()) { @@ -31,12 +33,12 @@ fun ServersScreen(navController: NavController = rememberNavController()) { Scaffold( topBar = { OSHTopAppBarWithLogo( - "Servers", + title = stringResource(R.string.tab_servers), actions = { IconButton(onClick = { /* do something */ }) { Icon( imageVector = Icons.Filled.MoreVert, - contentDescription = "App preferences" + contentDescription = "" ) } }, @@ -48,24 +50,24 @@ fun ServersScreen(navController: NavController = rememberNavController()) { ) { OSHClickableCard( onCardClick = {}, - title = "Manage Servers", + title = stringResource(R.string.manage_servers), subtitle = "0 of 0 server(s) enabled" ) OSHSwitchCard( - title = "SOS-T Service", - subtitle = "OGC SOS Transactional service", + title = stringResource(R.string.enable_sos_service), + subtitle = stringResource(R.string.summary_sos), checked = sosEnabled, onCheckedChange = { sosEnabled = it } ) OSHSwitchCard( - title = "Connected Systems Service", - subtitle = "OGC Connected Systems API service", + title = stringResource(R.string.enable_csapi_service), + subtitle = stringResource(R.string.summary_csapi), checked = csApiEnabled, onCheckedChange = { csApiEnabled = it } ) OSHSwitchCard( - title = "Discovery Service", - subtitle = "Sensor discovery based on rulesets", + title = stringResource(R.string.enable_discovery_service), + subtitle = stringResource(R.string.summary_discovery), checked = discoveryEnabled, onCheckedChange = { discoveryEnabled = it } ) From ab8a6b9d7b1db63303562753145e43efc1fbb8a2 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:03:37 -0500 Subject: [PATCH 18/63] update the home/sensors --- .../org/sensorhub/android/ui/OSHNavHost.kt | 4 +- .../android/ui/screens/HomeScreen.kt | 50 +++++++++++++------ .../android/ui/screens/SensorsScreen.kt | 49 ++++++++++++------ .../android/ui/screens/ServersScreen.kt | 2 +- 4 files changed, 74 insertions(+), 31 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 073b35f4..30cdc8ce 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -20,10 +20,10 @@ fun OSHNavHost( modifier = modifier ) { composable(Screen.Dashboard.route) { - HomeScreen() + HomeScreen(navController = navController) } composable(Screen.Sensors.route) { - SensorsScreen() + SensorsScreen(navController = navController) } composable(Screen.Servers.route) { ServersScreen(navController = navController) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt index 842febd5..8cddaf0a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -1,22 +1,44 @@ package org.sensorhub.android.ui.screens -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.R +import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo + @Composable -fun HomeScreen() { - Box (modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ){ - Text( - text = "Home Screen", - style = MaterialTheme.typography.headlineLarge - ) +fun HomeScreen(navController: NavController = rememberNavController()) { + Scaffold( + topBar = { + OSHTopAppBarWithLogo( + title = stringResource(R.string.tab_dashboard), + actions = { + IconButton(onClick = { /* open app preferences */ }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "" + ) + } + }, + ) }, + bottomBar = { Navbar(navController = navController) } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + + } + } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index cdd46e48..89b42913 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -1,23 +1,44 @@ package org.sensorhub.android.ui.screens -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.R +import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @Composable -fun SensorsScreen() { - Box (modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.Center - ){ - Text( - text = "Sensors Screen", - style = MaterialTheme.typography.headlineLarge - ) +fun SensorsScreen(navController: NavController = rememberNavController()) { + Scaffold( + topBar = { + OSHTopAppBarWithLogo( + title = stringResource(R.string.tab_sensors), + actions = { + IconButton(onClick = { /* open app preferences */ }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "" + ) + } + }, + ) }, + bottomBar = { Navbar(navController = navController) } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + + } + } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 54bb6cd4..ea3ba813 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -35,7 +35,7 @@ fun ServersScreen(navController: NavController = rememberNavController()) { OSHTopAppBarWithLogo( title = stringResource(R.string.tab_servers), actions = { - IconButton(onClick = { /* do something */ }) { + IconButton(onClick = { /* open app preferences */ }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" From e7d177344597c27f8025da81e367b9d357d92ef7 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:17:41 -0500 Subject: [PATCH 19/63] removed bottom navbar from each of the screens and made composemainactivity the launch point --- sensorhub-android-app/AndroidManifest.xml | 15 ++++++++------- .../sensorhub/android/ui/screens/HomeScreen.kt | 2 -- .../sensorhub/android/ui/screens/SensorsScreen.kt | 2 -- .../sensorhub/android/ui/screens/ServersScreen.kt | 2 -- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index f32781fc..c72c5e4b 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -46,13 +46,8 @@ android:configChanges="orientation|screenSize" android:label="@string/app_name" android:screenOrientation="portrait" - android:exported="true"> - - + android:exported="false" /> - - - + android:theme="@style/AppTheme"> + + + + + diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt index 8cddaf0a..1b7cb178 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R -import org.sensorhub.android.ui.Navbar import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -32,7 +31,6 @@ fun HomeScreen(navController: NavController = rememberNavController()) { } }, ) }, - bottomBar = { Navbar(navController = navController) } ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index 89b42913..170c79ae 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R -import org.sensorhub.android.ui.Navbar import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -32,7 +31,6 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { } }, ) }, - bottomBar = { Navbar(navController = navController) } ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index ea3ba813..a5caaf41 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import org.sensorhub.android.ui.Navbar import org.sensorhub.android.ui.components.OSHClickableCard import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -43,7 +42,6 @@ fun ServersScreen(navController: NavController = rememberNavController()) { } }, ) }, - bottomBar = { Navbar(navController = navController) } ) { innerPadding -> Column( modifier = Modifier.padding(innerPadding) From 427d927001baaa835842d5ff1091bc5f9cb98c15 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:19:11 -0500 Subject: [PATCH 20/63] changed dashboard title to osh --- .../src/org/sensorhub/android/ui/screens/HomeScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt index 1b7cb178..b6297a07 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -21,7 +21,7 @@ fun HomeScreen(navController: NavController = rememberNavController()) { Scaffold( topBar = { OSHTopAppBarWithLogo( - title = stringResource(R.string.tab_dashboard), + title = stringResource(R.string.app_name), actions = { IconButton(onClick = { /* open app preferences */ }) { Icon( From 6bd09dbaead8bc7ec4432fef6b05c789db733379 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:52:22 -0500 Subject: [PATCH 21/63] help faq screen using components. need to wire it up to pull from the res xml file --- .../src/org/sensorhub/android/ui/Screen.kt | 1 + .../android/ui/screens/HelpFaqScreen.kt | 99 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index 965e0f14..a2b6859f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -4,4 +4,5 @@ sealed class Screen(val route: String) { object Dashboard : Screen("dashboard_screen") object Servers : Screen("servers_screen") object Sensors : Screen("sensors_screen") + object HelpFaq : Screen("help_faq_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt new file mode 100644 index 00000000..85daf6f2 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt @@ -0,0 +1,99 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHExpandableCard +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background +import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.ui.theme.Primary +import org.sensorhub.android.ui.theme.TextSecondary + +@Composable +fun HelpFaqScreen( + items: List, + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + OSHTopAppBarWithBack( + title = "Help / FAQ", + onBackClick = onBackClick + ) + }, + containerColor = Background + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(16.dp) + ) { + items(items) { item -> + when (item) { + is FaqItem.Header -> { + Text( + text = item.title.uppercase(), + style = MaterialTheme.typography.labelLarge, + color = Primary, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + } + is FaqItem.Entry -> { + OSHExpandableCard( + title = item.question, + expandedContent = { + Text( + text = item.answer, + style = MaterialTheme.typography.bodyMedium, + color = TextSecondary + ) + } + ) + } + } + } + } + } +} + +sealed class FaqItem { + data class Header(val title: String) : FaqItem() + data class Entry(val question: String, val answer: String) : FaqItem() +} + +private fun buildSampleFaqItems(): List = listOf( + FaqItem.Header("Getting Started"), + FaqItem.Entry( + "What is OpenSensorHub?", + "OpenSensorHub is a sensor aggregation and streaming app. It collects data from your device\u2019s built-in sensors and connected Bluetooth peripherals, then streams that data to an OpenSensorHub server in real time." + ), + FaqItem.Entry( + "How do I connect to a server?", + "Go to the Settings tab, tap \"Manage Servers\", then tap the add button. Enter a name for the profile, the server host address, port, and endpoint path." + ), +) + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun HelpFaqScreenPreview() { + OSHTheme { + HelpFaqScreen( + items = buildSampleFaqItems(), + onBackClick = {} + ) + } +} From 27362d0143263b07ef8bc940a4aa8369e935c528 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:59:05 -0500 Subject: [PATCH 22/63] added expandable card for the help/faq --- .../android/ui/components/ExpandableCard.kt | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt new file mode 100644 index 00000000..440c85e1 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt @@ -0,0 +1,150 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.theme.* + +@Composable +fun OSHExpandableCard( + title: String, + modifier: Modifier = Modifier, + expanded: Boolean? = null, + onExpandChange: ((Boolean) -> Unit)? = null, + collapsedContent: @Composable ColumnScope.() -> Unit = {}, + expandedContent: @Composable ColumnScope.() -> Unit +) { + var internalExpanded by remember { mutableStateOf(false) } + val isExpanded = expanded ?: internalExpanded + val toggleExpanded = { + val newValue = !isExpanded + if (onExpandChange != null) { + onExpandChange(newValue) + } else { + internalExpanded = newValue + } + } + + Card( + onClick = { toggleExpanded() }, + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + containerColor = Secondary, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = { toggleExpanded() }) { + Icon( + imageVector = if (isExpanded) + Icons.Filled.KeyboardArrowUp + else + Icons.Filled.KeyboardArrowDown, + contentDescription = if (isExpanded) "Collapse" else "Expand" + ) + } + } + + // Always-visible collapsed content + collapsedContent() + + // Animated expandable content + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + expandedContent() + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ExpandableCardCollapsedPreview() { + OSHTheme { + OSHExpandableCard( + title = "GPS Sensor", + collapsedContent = { + Text( + text = "Streaming location data", + style = MaterialTheme.typography.bodyMedium, + color = TextSecondary + ) + }, + expandedContent = { + Text("Latitude: 34.0522") + Text("Longitude: -118.2437") + Text("Altitude: 71m") + } + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ExpandableCardExpandedPreview() { + OSHTheme { + OSHExpandableCard( + title = "Accelerometer", + expanded = true, + collapsedContent = { + Text( + text = "Motion sensor active", + style = MaterialTheme.typography.bodyMedium, + color = TextSecondary + ) + }, + expandedContent = { + Text("X: 0.023 m/s²") + Text("Y: 9.81 m/s²") + Text("Z: -0.15 m/s²") + } + ) + } +} From adea9b9c4049def7eec3320982bdeae6cb562a28 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:11:44 -0500 Subject: [PATCH 23/63] added string resource title --- .../org/sensorhub/android/ui/OSHNavHost.kt | 34 +++++++++++++++++++ .../android/ui/screens/HelpFaqScreen.kt | 5 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 30cdc8ce..3900f91a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -2,9 +2,13 @@ package org.sensorhub.android.ui import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import org.sensorhub.android.R +import org.sensorhub.android.ui.screens.FaqItem +import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.ServersScreen @@ -28,6 +32,36 @@ fun OSHNavHost( composable(Screen.Servers.route) { ServersScreen(navController = navController) } + composable(Screen.HelpFaq.route) { + val context = LocalContext.current + val questions = context.resources.getStringArray(R.array.faq_questions) + val answers = context.resources.getStringArray(R.array.faq_answers) + val categories = context.resources.getStringArray(R.array.faq_categories) + + val faqItems = buildFaqItems(questions, answers, categories) + + HelpFaqScreen( + items = faqItems, + onBackClick = { navController.popBackStack() } + ) + } } } +private fun buildFaqItems( + questions: Array, + answers: Array, + categories: Array +): List { + val items = mutableListOf() + var currentCategory = "" + for (i in questions.indices) { + val category = categories[i] + if (category != currentCategory) { + items.add(FaqItem.Header(category)) + currentCategory = category + } + items.add(FaqItem.Entry(questions[i], answers[i])) + } + return items +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt index 85daf6f2..251197b9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt @@ -19,6 +19,9 @@ import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme import org.sensorhub.android.ui.theme.Primary import org.sensorhub.android.ui.theme.TextSecondary +import org.sensorhub.android.R +import androidx.compose.ui.res.stringResource + @Composable fun HelpFaqScreen( @@ -28,7 +31,7 @@ fun HelpFaqScreen( Scaffold( topBar = { OSHTopAppBarWithBack( - title = "Help / FAQ", + title = stringResource(R.string.title_help_faq), onBackClick = onBackClick ) }, From 0d453cfbba2b0e8311018e8490ac75c89ee56a14 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:13:18 -0500 Subject: [PATCH 24/63] updated addons --- submodules/osh-addons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/osh-addons b/submodules/osh-addons index a7bc58af..db0f785e 160000 --- a/submodules/osh-addons +++ b/submodules/osh-addons @@ -1 +1 @@ -Subproject commit a7bc58af4afce56385f117597e302ad37220f2ee +Subproject commit db0f785e2ef50c899e76f358ad1662843b4e983f From 369fcadcefdd16c3efca87dd63b87f54c1c1e94a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:54:40 -0500 Subject: [PATCH 25/63] init for app status --- .../src/org/sensorhub/android/ui/Screen.kt | 1 + .../android/ui/screens/AppStatusScreen.kt | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index 965e0f14..00aa45de 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -4,4 +4,5 @@ sealed class Screen(val route: String) { object Dashboard : Screen("dashboard_screen") object Servers : Screen("servers_screen") object Sensors : Screen("sensors_screen") + object AppStatus : Screen("app_status_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt new file mode 100644 index 00000000..9e6fd146 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -0,0 +1,22 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun AppStatusScreen() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "App Status Screen", + style = MaterialTheme.typography.headlineLarge + ) + } +} From bac2b247adeb354b9095703889ad3fe7336e583f Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 21:58:43 -0500 Subject: [PATCH 26/63] Added initial scaffolding for app status # Conflicts: # sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt --- .../org/sensorhub/android/ui/OSHNavHost.kt | 8 +++ .../android/ui/screens/AppStatusScreen.kt | 53 ++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 30cdc8ce..27159ad0 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -5,6 +5,8 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import org.sensorhub.android.R +import org.sensorhub.android.ui.screens.AppStatusScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.ServersScreen @@ -28,6 +30,12 @@ fun OSHNavHost( composable(Screen.Servers.route) { ServersScreen(navController = navController) } + composable(Screen.AppStatus.route) { + AppStatusScreen( + onBackClick = { navController.popBackStack() } + ) + } + } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt index 9e6fd146..ec0d29ea 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -1,22 +1,49 @@ package org.sensorhub.android.ui.screens -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background +import org.sensorhub.android.ui.theme.OSHTheme @Composable -fun AppStatusScreen() { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = "App Status Screen", - style = MaterialTheme.typography.headlineLarge - ) +fun AppStatusScreen( + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + OSHTopAppBarWithBack( + title = "App Status", + onBackClick = onBackClick + ) + }, + containerColor = Background + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(16.dp) + ) { + + } } } + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun AppStatusScreenPreview() { + OSHTheme { + AppStatusScreen( + onBackClick = {} + ) + } +} \ No newline at end of file From 5d648addc445ff79b1e5000f129829b0eb7de633 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 22:01:56 -0500 Subject: [PATCH 27/63] fixed imports --- sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 27159ad0..76885f1a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable -import org.sensorhub.android.R import org.sensorhub.android.ui.screens.AppStatusScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen From becbbcc2653c7851a3d3289e9e6a5daf6e027d5e Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 10:51:37 -0500 Subject: [PATCH 28/63] update title --- .../src/org/sensorhub/android/ui/screens/AppStatusScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt index ec0d29ea..76fa9f64 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.unit.dp import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.R +import androidx.compose.ui.res.stringResource @Composable fun AppStatusScreen( @@ -20,7 +22,7 @@ fun AppStatusScreen( Scaffold( topBar = { OSHTopAppBarWithBack( - title = "App Status", + title = stringResource(R.string.app_status_main_fragment), onBackClick = onBackClick ) }, From b2286a29dad5ee7c92473bd62d80aa9f001dcfc9 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 22:05:06 -0500 Subject: [PATCH 29/63] initial commit of server profiles screen # Conflicts: # sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt # Conflicts: # sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt # sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt --- .../org/sensorhub/android/ui/OSHNavHost.kt | 8 +++ .../src/org/sensorhub/android/ui/Screen.kt | 1 + .../ui/screens/ServerProfilesScreen.kt | 49 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 45924b6e..8761de77 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -13,6 +13,8 @@ import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.ServersScreen +import org.sensorhub.android.ui.screens.ServerProfilesScreen +import org.sensorhub.android.ui.screens.SettingsScreen @Composable fun OSHNavHost( @@ -51,6 +53,12 @@ fun OSHNavHost( onBackClick = { navController.popBackStack() } ) } + + composable(Screen.ServerProfiles.route) { + ServerProfilesScreen( + onBackClick = { navController.popBackStack() } + ) + } } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index af60f056..2e165daf 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -4,6 +4,7 @@ sealed class Screen(val route: String) { object Dashboard : Screen("dashboard_screen") object Servers : Screen("servers_screen") object Sensors : Screen("sensors_screen") + object ServerProfiles : Screen("server_profiles_screen") object AppStatus : Screen("app_status_screen") object HelpFaq : Screen("help_faq_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt new file mode 100644 index 00000000..c4c8d3e5 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -0,0 +1,49 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background +import org.sensorhub.android.ui.theme.OSHTheme + +@Composable +fun ServerProfilesScreen( + onBackClick: () -> Unit +) { + Scaffold( + topBar = { + OSHTopAppBarWithBack( + title = "Server Profile", + onBackClick = onBackClick + ) + }, + containerColor = Background + ) { padding -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(16.dp) + ) { + + } + } +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun AppStatusScreenPreview() { + OSHTheme { + ServerProfilesScreen( + onBackClick = {} + ) + } +} \ No newline at end of file From 67ad922f82f59c1a059abfe8d1ee07367982145d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Jul 2026 22:24:45 -0500 Subject: [PATCH 30/63] Added initial server profile cards and fab to screen. working on getting the server profiles from sharedprefs --- .../org/sensorhub/android/ui/OSHNavHost.kt | 18 ++++- .../sensorhub/android/ui/components/Fab.kt | 24 ++++++ .../ui/screens/ServerProfilesScreen.kt | 73 ++++++++++++++++++- 3 files changed, 109 insertions(+), 6 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 8761de77..390595cf 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -14,7 +14,8 @@ import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.ServersScreen import org.sensorhub.android.ui.screens.ServerProfilesScreen -import org.sensorhub.android.ui.screens.SettingsScreen +import org.sensorhub.android.server.ServerProfile +import org.sensorhub.android.ui.screens.ServerProfileItems @Composable fun OSHNavHost( @@ -55,8 +56,13 @@ fun OSHNavHost( } composable(Screen.ServerProfiles.route) { + val context = LocalContext.current + val servers = context.resources.getStringArray(R.id.server_profiles_toolbar) + + val serverProfileItems = buildServerProfileItems() ServerProfilesScreen( - onBackClick = { navController.popBackStack() } + onBackClick = { navController.popBackStack() }, + items = serverProfileItems ) } } @@ -79,3 +85,11 @@ private fun buildFaqItems( } return items } + +private fun buildServerProfileItems( + +): List { + val items = mutableListOf() + + return items; +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt index d42b56a3..7933b112 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt @@ -1,7 +1,10 @@ package org.sensorhub.android.ui.components import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.FloatingActionButton @@ -11,8 +14,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.sensorhub.android.ui.theme.OSHTheme import org.sensorhub.android.ui.theme.OnPrimary import org.sensorhub.android.ui.theme.Secondary @@ -53,12 +58,31 @@ fun OSHFAB( } } +@Composable +fun OSHAddFAB( + onClick: ((Boolean) -> Unit)? = null, +) { + FloatingActionButton( + onClick = { onClick }, + containerColor = SecondaryContainer, + contentColor = OnPrimary + + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add new item" + ) + } +} + @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun ServerItemsCard() { OSHTheme { Column { OSHFAB() + Spacer(modifier = Modifier.width(10.dp)) + OSHAddFAB() } } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt index c4c8d3e5..d290d55c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -4,26 +4,39 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHActionCard +import org.sensorhub.android.ui.components.OSHAddFAB import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServerProfilesScreen( + items: List, onBackClick: () -> Unit ) { Scaffold( topBar = { OSHTopAppBarWithBack( - title = "Server Profile", + title = "Server Profiles", onBackClick = onBackClick ) }, + floatingActionButton = { + OSHAddFAB(onClick = {}) + }, containerColor = Background ) { padding -> LazyColumn( @@ -32,18 +45,70 @@ fun ServerProfilesScreen( .padding(padding), contentPadding = PaddingValues(16.dp) ) { + items(items) { item -> + var enabled by remember { mutableStateOf(item.enabled) } + OSHActionCard( + title = item.name, + subtitle = "${item.host}:${item.port}${item.endpointPath}", + checked = enabled, + onCheckedChange = { enabled = it }, + onClicked = { /* navigate to edit */ }, + imageVector = Icons.Filled.Edit, + contentDescription = "Edit ${item.name}" + ) + } } } } +data class ServerProfileItem( + val id: String = "", + val name: String = "", + val host: String = "", + val port: Int = 8181, + val endpointPath: String = "/sensorhub/api", + val username: String = "", + val password: String = "", + val enableTls: Boolean = false, + val disableSslCheck: Boolean = false, + val useConSysClient: Boolean = false, + val oAuthEnabled: Boolean = false, + val enabled: Boolean = false, + val clientId: String = "", + val clientSecret: String = "", + val tokenEndpoint: String = "" +) + +private fun buildSampleServerProfilesItems(): List = listOf( + ServerProfileItem( + id = "1", + name = "Local Server", + host = "127.0.0.1", + port = 8181, + endpointPath = "/sensorhub/api", + useConSysClient = true, + enabled = true + ), + ServerProfileItem( + id = "2", + name = "Cloud Server", + host = "osh.example.com", + port = 443, + endpointPath = "/sensorhub/api", + enableTls = true, + useConSysClient = true, + enabled = false + ) +) @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable -private fun AppStatusScreenPreview() { +private fun ServerProfilesScreenPreview() { OSHTheme { ServerProfilesScreen( - onBackClick = {} + onBackClick = {}, + items = buildSampleServerProfilesItems() ) } -} \ No newline at end of file +} From 71a667cef28a32da69cd0a44eedfbd5d4742d55c Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 11:27:04 -0500 Subject: [PATCH 31/63] fixed issues --- .../src/org/sensorhub/android/ui/OSHNavHost.kt | 5 ++--- submodules/osh-addons | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 390595cf..a09d4482 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -12,10 +12,9 @@ import org.sensorhub.android.ui.screens.FaqItem import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen +import org.sensorhub.android.ui.screens.ServerProfileItem import org.sensorhub.android.ui.screens.ServersScreen import org.sensorhub.android.ui.screens.ServerProfilesScreen -import org.sensorhub.android.server.ServerProfile -import org.sensorhub.android.ui.screens.ServerProfileItems @Composable fun OSHNavHost( @@ -57,7 +56,7 @@ fun OSHNavHost( composable(Screen.ServerProfiles.route) { val context = LocalContext.current - val servers = context.resources.getStringArray(R.id.server_profiles_toolbar) +// val servers = context.resources.getStringArray(R.id.server_profiles_toolbar) val serverProfileItems = buildServerProfileItems() ServerProfilesScreen( diff --git a/submodules/osh-addons b/submodules/osh-addons index db0f785e..a7bc58af 160000 --- a/submodules/osh-addons +++ b/submodules/osh-addons @@ -1 +1 @@ -Subproject commit db0f785e2ef50c899e76f358ad1662843b4e983f +Subproject commit a7bc58af4afce56385f117597e302ad37220f2ee From 350849f636978770ec9a00729381d46e05ba405d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 09:43:39 -0500 Subject: [PATCH 32/63] Added initial app preferences screen --- .../org/sensorhub/android/ui/OSHNavHost.kt | 6 +- .../src/org/sensorhub/android/ui/Screen.kt | 1 + .../sensorhub/android/ui/components/Card.kt | 81 ++++++++++++++++- .../ui/screens/AppPreferencesScreen.kt | 86 +++++++++++++++++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index a09d4482..befa7a75 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import org.sensorhub.android.ui.screens.AppPreferencesScreen import org.sensorhub.android.ui.screens.AppStatusScreen import org.sensorhub.android.R import org.sensorhub.android.ui.screens.FaqItem @@ -53,7 +54,9 @@ fun OSHNavHost( onBackClick = { navController.popBackStack() } ) } - + composable(Screen.AppPreferences.route) { + AppPreferencesScreen() + } composable(Screen.ServerProfiles.route) { val context = LocalContext.current // val servers = context.resources.getStringArray(R.id.server_profiles_toolbar) @@ -67,6 +70,7 @@ fun OSHNavHost( } } + private fun buildFaqItems( questions: Array, answers: Array, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index 2e165daf..19dc3d83 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -7,4 +7,5 @@ sealed class Screen(val route: String) { object ServerProfiles : Screen("server_profiles_screen") object AppStatus : Screen("app_status_screen") object HelpFaq : Screen("help_faq_screen") + object AppPreferences : Screen("app_prefs_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index b9424cf3..92a0ee34 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -1,5 +1,6 @@ package org.sensorhub.android.ui.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -34,10 +35,10 @@ fun OSHCard( content: @Composable ColumnScope.() -> Unit ) { Card( - modifier = modifier.fillMaxWidth().padding(16.dp), - shape = RoundedCornerShape(24.dp), + modifier = modifier.fillMaxWidth().padding(20.dp), + shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = Secondary, + containerColor = SecondaryContainer, contentColor = TextPrimary ), elevation = CardDefaults.cardElevation( @@ -199,6 +200,80 @@ fun OSHActionCard( } } +@Composable +fun OSHInfoRow( + label: String, + value: String, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +fun OSHEditableRow( + label: String, + value: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable { onClick() } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun DeviceInfoCardPreview() { + OSHTheme { + OSHCard { + OSHEditableRow( + label = "Device Name", + value = "MyDevice", + onClick = {} + ) + OSHInfoRow( + label = "Device IP Address", + value = "192.168.1.42" + ) + OSHInfoRow( + label = "Version", + value = "1.0.0" + ) + } + } +} + @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun CardPreview() { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt new file mode 100644 index 00000000..115f62e0 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -0,0 +1,86 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHClickableCard +import org.sensorhub.android.ui.components.OSHEditableRow +import org.sensorhub.android.ui.components.OSHInfoRow +import org.sensorhub.android.ui.components.OSHSwitchCard +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.ui.theme.Secondary + +@Composable +fun AppPreferencesScreen() { + + Scaffold( + topBar = { + OSHTopAppBarWithBack( + "App Preferences", + onBackClick = {} + ) + }, + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding) + ) { + + OSHCard { + OSHEditableRow( + label = "Device Name", + value = "MyDevice", + onClick = {} + ) +// HorizontalDivider( +// thickness = 1.dp, +// color = Secondary +// ) + OSHInfoRow( + label = "Device IP Address", + value = "192.168.1.42" + ) +// HorizontalDivider( +// thickness = 1.dp, +// color = Secondary +// ) + OSHInfoRow( + label = "Version", + value = "1.0.0" + ) + } + + OSHCard { + + } + + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun AppPreferencesScreenPreview() { + OSHTheme { + AppPreferencesScreen() + } +} From 480e9afbdf49a5e2413524c5ce7f151035397a97 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 10:32:43 -0500 Subject: [PATCH 33/63] added cards and dialogs to app prefs screen --- .../org/sensorhub/android/ui/OSHNavHost.kt | 4 +- .../sensorhub/android/ui/components/Card.kt | 108 +++++++++++++++++- .../ui/screens/AppPreferencesScreen.kt | 95 ++++++++++++--- 3 files changed, 191 insertions(+), 16 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index befa7a75..b9f085c4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -55,7 +55,9 @@ fun OSHNavHost( ) } composable(Screen.AppPreferences.route) { - AppPreferencesScreen() + AppPreferencesScreen( + onBackClick = { navController.popBackStack() } + ) } composable(Screen.ServerProfiles.route) { val context = LocalContext.current diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index 92a0ee34..86926c04 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -15,6 +15,7 @@ import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Language import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -48,13 +49,105 @@ fun OSHCard( ) } +@Composable +fun OSHRowWithIcon( + title: String, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Icon(imageVector, contentDescription = contentDescription) + Spacer(modifier = Modifier.width(15.dp)) + + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + Icon(Icons.Filled.ChevronRight, contentDescription = null) + } +} + +@Composable +fun OSHClickableRowWithIcon( + title: String, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String, + onClick: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Icon(imageVector, contentDescription = contentDescription) + Spacer(modifier = Modifier.width(15.dp)) + + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + Icon(Icons.Filled.ChevronRight, contentDescription = null) + } +} + +@Composable +fun OSHClickableCardWithIcon( + onCardClick: () -> Unit, + title: String, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String +) { + Card( + onClick = onCardClick, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Icon(imageVector, contentDescription = contentDescription) + Spacer(modifier = Modifier.width(15.dp)) + + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + Icon(Icons.Filled.ChevronRight, contentDescription = null) + + } + } +} @Composable fun OSHClickableCard( onCardClick: () -> Unit, title: String, subtitle: String, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { Card( onClick = onCardClick, @@ -68,7 +161,7 @@ fun OSHClickableCard( ), elevation = CardDefaults.cardElevation( defaultElevation = 8.dp, - ) + ), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -270,6 +363,11 @@ private fun DeviceInfoCardPreview() { label = "Version", value = "1.0.0" ) + OSHRowWithIcon( + title = "Language", + imageVector = Icons.Default.Language, + contentDescription = "Select in app language" + ) } } } @@ -332,6 +430,12 @@ private fun SettingsItemPreview() { title = "Manage Sensors", subtitle = "1 of 2 server(s) enabled", ) + OSHClickableCardWithIcon ( + onCardClick = {}, + title = "Language", + imageVector = Icons.Default.Language, + contentDescription = "Select in app language" + ) } } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 115f62e0..94e60b43 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -3,7 +3,12 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Help +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Lan +import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -20,10 +25,15 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHClickableCard +import org.sensorhub.android.ui.components.OSHClickableCardWithIcon +import org.sensorhub.android.ui.components.OSHClickableRowWithIcon import org.sensorhub.android.ui.components.OSHEditableRow import org.sensorhub.android.ui.components.OSHInfoRow +import org.sensorhub.android.ui.components.OSHRowWithIcon +import org.sensorhub.android.ui.components.OSHSingleChoiceDialog import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -31,13 +41,50 @@ import org.sensorhub.android.ui.theme.OSHTheme import org.sensorhub.android.ui.theme.Secondary @Composable -fun AppPreferencesScreen() { +fun AppPreferencesScreen( + onBackClick: () -> Unit +) { +//TODO need to pull hardcoded names/titles/ labels/ from resources !!!!! + var deviceName by remember { mutableStateOf("") } + var deviceHost by remember { mutableStateOf("") } + var appVersion by remember { mutableStateOf("") } + + var showLanguageDialog by remember { mutableStateOf(false) } + var showNotificationDialog by remember { mutableStateOf(false) } + var showDeviceNameEdit by remember { mutableStateOf(false) } + var showAboutAppDialog by remember { mutableStateOf(false) } + + if(showLanguageDialog) { + OSHSingleChoiceDialog( + onDismissRequest = { showLanguageDialog = false }, + dialogTitle = "Language", + icon = Icons.Filled.Language, + options = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch"), + selectedIndex = 0, + onOptionSelected = {} + ) + } + + if (showNotificationDialog) { + + } + + if (showAboutAppDialog) { + OSHAlertDialogWithoutDismiss( + onConfirmation = { showLanguageDialog = false }, + dialogText = "A software platform for building smart sensor networks and the internet of things", + dialogTitle = "OpenSensorHub", + icon = Icons.Filled.Info + ) + } Scaffold( topBar = { OSHTopAppBarWithBack( "App Preferences", - onBackClick = {} + onBackClick = { + + } ) }, ) { innerPadding -> @@ -51,26 +98,48 @@ fun AppPreferencesScreen() { value = "MyDevice", onClick = {} ) -// HorizontalDivider( -// thickness = 1.dp, -// color = Secondary -// ) OSHInfoRow( label = "Device IP Address", - value = "192.168.1.42" + value = deviceHost ) -// HorizontalDivider( -// thickness = 1.dp, -// color = Secondary -// ) OSHInfoRow( label = "Version", - value = "1.0.0" + value = appVersion ) } OSHCard { + OSHClickableRowWithIcon( + title = "Notifications", + imageVector = Icons.Default.Notifications, + contentDescription = "Select in app language", + onClick = { showNotificationDialog = true } + ) + OSHClickableRowWithIcon( + title = "Language", + imageVector = Icons.Default.Language, + contentDescription = "Select in app language", + onClick = {showLanguageDialog = true } + + ) + } + OSHCard { + OSHClickableRowWithIcon( + title = "About application", + imageVector = Icons.Default.Info, + contentDescription = "Select in app language", + onClick = { showAboutAppDialog = true } + + ) + + OSHClickableRowWithIcon( + title = "Help/FAQ", + imageVector = Icons.Default.Help, + contentDescription = "Select in app language", + onClick = { /* navigate to help/faq screen*/} + + ) } } @@ -81,6 +150,6 @@ fun AppPreferencesScreen() { @Composable private fun AppPreferencesScreenPreview() { OSHTheme { - AppPreferencesScreen() + AppPreferencesScreen({}) } } From ec0d2ffe02e5a0299f1defba0171a7c376a61a85 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 10:45:00 -0500 Subject: [PATCH 34/63] update to use string resources rather than hardcoding --- .../org/sensorhub/android/ui/OSHNavHost.kt | 2 + .../ui/screens/AppPreferencesScreen.kt | 46 +++++++++---------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index b9f085c4..e2c88130 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -55,6 +55,8 @@ fun OSHNavHost( ) } composable(Screen.AppPreferences.route) { + val context = LocalContext.current + AppPreferencesScreen( onBackClick = { navController.popBackStack() } ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 94e60b43..cc225481 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -21,9 +21,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.R import org.sensorhub.android.ui.Navbar import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard @@ -44,7 +46,6 @@ import org.sensorhub.android.ui.theme.Secondary fun AppPreferencesScreen( onBackClick: () -> Unit ) { -//TODO need to pull hardcoded names/titles/ labels/ from resources !!!!! var deviceName by remember { mutableStateOf("") } var deviceHost by remember { mutableStateOf("") } var appVersion by remember { mutableStateOf("") } @@ -57,7 +58,7 @@ fun AppPreferencesScreen( if(showLanguageDialog) { OSHSingleChoiceDialog( onDismissRequest = { showLanguageDialog = false }, - dialogTitle = "Language", + dialogTitle = stringResource(R.string.title_language_dialog), icon = Icons.Filled.Language, options = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch"), selectedIndex = 0, @@ -72,8 +73,8 @@ fun AppPreferencesScreen( if (showAboutAppDialog) { OSHAlertDialogWithoutDismiss( onConfirmation = { showLanguageDialog = false }, - dialogText = "A software platform for building smart sensor networks and the internet of things", - dialogTitle = "OpenSensorHub", + dialogText = stringResource(R.string.about_description), + dialogTitle = stringResource(R.string.app_name), icon = Icons.Filled.Info ) } @@ -81,7 +82,7 @@ fun AppPreferencesScreen( Scaffold( topBar = { OSHTopAppBarWithBack( - "App Preferences", + stringResource(R.string.app_preferences), onBackClick = { } @@ -94,51 +95,46 @@ fun AppPreferencesScreen( OSHCard { OSHEditableRow( - label = "Device Name", - value = "MyDevice", - onClick = {} + label = stringResource(R.string.device_name), + value = deviceName.ifEmpty { "MyDevice" }, + onClick = { showDeviceNameEdit = true } ) OSHInfoRow( - label = "Device IP Address", + label = stringResource(R.string.device_ip_address), value = deviceHost ) OSHInfoRow( - label = "Version", + label = stringResource(R.string.title_version), value = appVersion ) } OSHCard { OSHClickableRowWithIcon( - title = "Notifications", + title = stringResource(R.string.title_notifications), imageVector = Icons.Default.Notifications, - contentDescription = "Select in app language", + contentDescription = stringResource(R.string.title_notifications), onClick = { showNotificationDialog = true } - ) OSHClickableRowWithIcon( - title = "Language", + title = stringResource(R.string.title_language), imageVector = Icons.Default.Language, - contentDescription = "Select in app language", - onClick = {showLanguageDialog = true } - + contentDescription = stringResource(R.string.title_language), + onClick = { showLanguageDialog = true } ) } OSHCard { OSHClickableRowWithIcon( - title = "About application", + title = stringResource(R.string.title_about), imageVector = Icons.Default.Info, - contentDescription = "Select in app language", + contentDescription = stringResource(R.string.title_about), onClick = { showAboutAppDialog = true } - ) - OSHClickableRowWithIcon( - title = "Help/FAQ", + title = stringResource(R.string.title_help_faq), imageVector = Icons.Default.Help, - contentDescription = "Select in app language", - onClick = { /* navigate to help/faq screen*/} - + contentDescription = stringResource(R.string.title_help_faq), + onClick = { /* navigate to help/faq screen */ } ) } From 573f08f884a6640da68f80369f5a653daf303cf4 Mon Sep 17 00:00:00 2001 From: Kalyn Stallings <134666642+kalynstricklin@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:47:43 -0500 Subject: [PATCH 35/63] Fix formatting in AppPreferencesScreen.kt --- .../org/sensorhub/android/ui/screens/AppPreferencesScreen.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index cc225481..5c9e930f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -82,10 +82,9 @@ fun AppPreferencesScreen( Scaffold( topBar = { OSHTopAppBarWithBack( - stringResource(R.string.app_preferences), - onBackClick = { + stringResource(R.string.app_preferences), + onBackClick = onBackClick - } ) }, ) { innerPadding -> From 238438b9654b0d58221efc2a6cb92330f2626ec5 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 13:34:36 -0500 Subject: [PATCH 36/63] Move rows to their own file, added navcontroller to navigate between screens. cleaned out the cards rows and reduced duplicated code --- .../org/sensorhub/android/ui/OSHNavHost.kt | 4 +- .../sensorhub/android/ui/components/Card.kt | 270 +++--------------- .../android/ui/components/ExpandableCard.kt | 7 +- .../android/ui/components/OSHClickableRow.kt | 63 ++++ .../ui/components/OSHClickableRowWithIcon.kt | 50 ++++ .../android/ui/components/OSHInfoRow.kt | 39 +++ .../ui/screens/AppPreferencesScreen.kt | 33 ++- .../android/ui/screens/HelpFaqScreen.kt | 5 +- .../android/ui/screens/HomeScreen.kt | 3 +- .../android/ui/screens/SensorsScreen.kt | 3 +- .../android/ui/screens/ServersScreen.kt | 61 ++-- 11 files changed, 266 insertions(+), 272 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRow.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInfoRow.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index e2c88130..47751b4a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -54,11 +54,13 @@ fun OSHNavHost( onBackClick = { navController.popBackStack() } ) } + composable(Screen.AppPreferences.route) { val context = LocalContext.current AppPreferencesScreen( - onBackClick = { navController.popBackStack() } + onBackClick = { navController.popBackStack() }, + navController = navController ) } composable(Screen.ServerProfiles.route) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index 86926c04..528e55e1 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -13,8 +13,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.Language import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -28,7 +26,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import org.sensorhub.android.ui.theme.* +import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.android.ui.theme.SecondaryContainer +import org.sensorhub.android.ui.theme.TextPrimary @Composable fun OSHCard( @@ -36,8 +36,10 @@ fun OSHCard( content: @Composable ColumnScope.() -> Unit ) { Card( - modifier = modifier.fillMaxWidth().padding(20.dp), - shape = RoundedCornerShape(16.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(12.dp), colors = CardDefaults.cardColors( containerColor = SecondaryContainer, contentColor = TextPrimary @@ -49,56 +51,6 @@ fun OSHCard( ) } -@Composable -fun OSHRowWithIcon( - title: String, - modifier: Modifier = Modifier, - imageVector: ImageVector, - contentDescription: String, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Icon(imageVector, contentDescription = contentDescription) - Spacer(modifier = Modifier.width(15.dp)) - - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - Icon(Icons.Filled.ChevronRight, contentDescription = null) - } -} - -@Composable -fun OSHClickableRowWithIcon( - title: String, - modifier: Modifier = Modifier, - imageVector: ImageVector, - contentDescription: String, - onClick: () -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Icon(imageVector, contentDescription = contentDescription) - Spacer(modifier = Modifier.width(15.dp)) - - Text( - text = title, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - Icon(Icons.Filled.ChevronRight, contentDescription = null) - } -} @Composable fun OSHClickableCardWithIcon( @@ -108,37 +60,15 @@ fun OSHClickableCardWithIcon( imageVector: ImageVector, contentDescription: String ) { - Card( - onClick = onCardClick, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = SecondaryContainer, - contentColor = TextPrimary - ), - elevation = CardDefaults.cardElevation( - defaultElevation = 8.dp, - ), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Icon(imageVector, contentDescription = contentDescription) - Spacer(modifier = Modifier.width(15.dp)) - - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.weight(1f) - ) - Icon(Icons.Filled.ChevronRight, contentDescription = null) - - } + OSHCard() + { + OSHClickableRowWithIcon( + title = title, + imageVector = imageVector, + contentDescription = contentDescription, + modifier = modifier, + onClick = onCardClick + ) } } @@ -149,20 +79,7 @@ fun OSHClickableCard( subtitle: String, modifier: Modifier = Modifier, ) { - Card( - onClick = onCardClick, - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = SecondaryContainer, - contentColor = TextPrimary - ), - elevation = CardDefaults.cardElevation( - defaultElevation = 8.dp, - ), - ) { + OSHCard(modifier = modifier.clickable { onCardClick() }) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -182,7 +99,6 @@ fun OSHClickableCard( Spacer(modifier = Modifier.width(10.dp)) Icon(Icons.Filled.ChevronRight, contentDescription = null) - } } } @@ -196,43 +112,13 @@ fun OSHSwitchCard( onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier ) { - Card( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = SecondaryContainer, - contentColor = TextPrimary - ), - elevation = CardDefaults.cardElevation( - defaultElevation = 8.dp, + OSHCard(modifier = modifier) { + OSHSwitchRow( + title = title, + subtitle = subtitle, + checked = checked, + onCheckedChange = onCheckedChange ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - OSHSwitch( - checked = checked, - onCheckedChange = onCheckedChange - ) - } } } @@ -247,100 +133,20 @@ fun OSHActionCard( imageVector: ImageVector, contentDescription: String ) { - Card( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(12.dp), - colors = CardDefaults.cardColors( - containerColor = SecondaryContainer, - contentColor = TextPrimary - ), - elevation = CardDefaults.cardElevation( - defaultElevation = 8.dp, - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + OSHCard(modifier = modifier) { + OSHSwitchRow( + title = title, + subtitle = subtitle, + checked = checked, + onCheckedChange = onCheckedChange, + trailingContent = { + IconButton(onClick = { onClicked() }) { + Icon( + imageVector = imageVector, + contentDescription = contentDescription + ) + } } - - OSHSwitch( - checked = checked, - onCheckedChange = onCheckedChange - ) - IconButton(onClick = { onClicked() }) { - Icon( - imageVector = imageVector, - contentDescription = contentDescription - ) - } - } - } -} - -@Composable -fun OSHInfoRow( - label: String, - value: String, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - Text( - text = value, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} - -@Composable -fun OSHEditableRow( - label: String, - value: String, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { onClick() } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.weight(1f) - ) - Text( - text = value, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant ) } } @@ -350,7 +156,7 @@ fun OSHEditableRow( private fun DeviceInfoCardPreview() { OSHTheme { OSHCard { - OSHEditableRow( + OSHInfoRow( label = "Device Name", value = "MyDevice", onClick = {} @@ -363,7 +169,7 @@ private fun DeviceInfoCardPreview() { label = "Version", value = "1.0.0" ) - OSHRowWithIcon( + OSHClickableRowWithIcon( title = "Language", imageVector = Icons.Default.Language, contentDescription = "Select in app language" diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt index 440c85e1..cde8ce86 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt @@ -54,11 +54,10 @@ fun OSHExpandableCard( Card( onClick = { toggleExpanded() }, modifier = modifier - .fillMaxWidth() - .padding(16.dp), - shape = RoundedCornerShape(24.dp), + .fillMaxWidth(), + shape = RoundedCornerShape(16.dp), colors = CardDefaults.cardColors( - containerColor = Secondary, + containerColor = SecondaryContainer, contentColor = TextPrimary ), elevation = CardDefaults.cardElevation( diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRow.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRow.kt new file mode 100644 index 00000000..d5bf3563 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRow.kt @@ -0,0 +1,63 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun OSHClickableRowWithIcon( + title: String, + modifier: Modifier = Modifier, + imageVector: ImageVector, + contentDescription: String, + subtitle: String? = null, + onClick: (() -> Unit)? = null +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable { onClick() } else Modifier) + .padding(16.dp) + ) { + Icon(imageVector, contentDescription = contentDescription) + Spacer(modifier = Modifier.width(15.dp)) + + if (subtitle != null) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + } + Icon(Icons.Filled.ChevronRight, contentDescription = null) + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt new file mode 100644 index 00000000..17b0f31b --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt @@ -0,0 +1,50 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun OSHSwitchRow ( + title: String, + subtitle: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + trailingContent: @Composable () -> Unit = {} +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + OSHSwitch( + checked = checked, + onCheckedChange = onCheckedChange + ) + trailingContent() + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInfoRow.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInfoRow.kt new file mode 100644 index 00000000..cf0662e4 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInfoRow.kt @@ -0,0 +1,39 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun OSHInfoRow( + label: String, + value: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null +) { + Row( + modifier = modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable { onClick() } else Modifier) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.weight(1f) + ) + Text( + text = value, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 5c9e930f..a8b2f08e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -27,15 +27,15 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R import org.sensorhub.android.ui.Navbar +import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHClickableCard import org.sensorhub.android.ui.components.OSHClickableCardWithIcon import org.sensorhub.android.ui.components.OSHClickableRowWithIcon -import org.sensorhub.android.ui.components.OSHEditableRow import org.sensorhub.android.ui.components.OSHInfoRow -import org.sensorhub.android.ui.components.OSHRowWithIcon import org.sensorhub.android.ui.components.OSHSingleChoiceDialog +import org.sensorhub.android.ui.components.OSHTextInputDialog import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -44,7 +44,8 @@ import org.sensorhub.android.ui.theme.Secondary @Composable fun AppPreferencesScreen( - onBackClick: () -> Unit + onBackClick: () -> Unit, + navController: NavController = rememberNavController() ) { var deviceName by remember { mutableStateOf("") } var deviceHost by remember { mutableStateOf("") } @@ -67,12 +68,31 @@ fun AppPreferencesScreen( } if (showNotificationDialog) { + OSHAlertDialogWithoutDismiss( + onConfirmation = { showNotificationDialog = false }, + dialogText = "Not yet implemented", + dialogTitle = "Notifications", + icon = Icons.Filled.Info + ) + } + if (showDeviceNameEdit) { + OSHTextInputDialog( + onDismissRequest = { showDeviceNameEdit = false }, + onConfirmation = { newName -> + deviceName = newName + showDeviceNameEdit = false + }, + dialogTitle = stringResource(R.string.device_name), + dialogText = stringResource(R.string.device_name), + icon = Icons.Filled.Info, + initialValue = deviceName.ifEmpty { "MyDevice" } + ) } if (showAboutAppDialog) { OSHAlertDialogWithoutDismiss( - onConfirmation = { showLanguageDialog = false }, + onConfirmation = { showAboutAppDialog = false }, dialogText = stringResource(R.string.about_description), dialogTitle = stringResource(R.string.app_name), icon = Icons.Filled.Info @@ -84,7 +104,6 @@ fun AppPreferencesScreen( OSHTopAppBarWithBack( stringResource(R.string.app_preferences), onBackClick = onBackClick - ) }, ) { innerPadding -> @@ -93,7 +112,7 @@ fun AppPreferencesScreen( ) { OSHCard { - OSHEditableRow( + OSHInfoRow( label = stringResource(R.string.device_name), value = deviceName.ifEmpty { "MyDevice" }, onClick = { showDeviceNameEdit = true } @@ -133,7 +152,7 @@ fun AppPreferencesScreen( title = stringResource(R.string.title_help_faq), imageVector = Icons.Default.Help, contentDescription = stringResource(R.string.title_help_faq), - onClick = { /* navigate to help/faq screen */ } + onClick = { navController.navigate(Screen.HelpFaq.route)} ) } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt index 251197b9..40a6fc50 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt @@ -41,7 +41,7 @@ fun HelpFaqScreen( modifier = Modifier .fillMaxSize() .padding(padding), - contentPadding = PaddingValues(16.dp) + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) ) { items(items) { item -> when (item) { @@ -52,12 +52,13 @@ fun HelpFaqScreen( color = Primary, modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(horizontal = 4.dp, vertical = 12.dp) ) } is FaqItem.Entry -> { OSHExpandableCard( title = item.question, + modifier = Modifier.padding(vertical = 4.dp), expandedContent = { Text( text = item.answer, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt index b6297a07..94eae7b4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R +import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -23,7 +24,7 @@ fun HomeScreen(navController: NavController = rememberNavController()) { OSHTopAppBarWithLogo( title = stringResource(R.string.app_name), actions = { - IconButton(onClick = { /* open app preferences */ }) { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index 170c79ae..18760811 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R +import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo @@ -23,7 +24,7 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { OSHTopAppBarWithLogo( title = stringResource(R.string.tab_sensors), actions = { - IconButton(onClick = { /* open app preferences */ }) { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index a5caaf41..649169f7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -3,7 +3,9 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold @@ -22,6 +24,10 @@ import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme import androidx.compose.ui.res.stringResource import org.sensorhub.android.R +import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHClickableRowWithIcon +import org.sensorhub.android.ui.components.OSHSwitchRow @Composable fun ServersScreen(navController: NavController = rememberNavController()) { @@ -34,7 +40,7 @@ fun ServersScreen(navController: NavController = rememberNavController()) { OSHTopAppBarWithLogo( title = stringResource(R.string.tab_servers), actions = { - IconButton(onClick = { /* open app preferences */ }) { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" @@ -46,29 +52,36 @@ fun ServersScreen(navController: NavController = rememberNavController()) { Column( modifier = Modifier.padding(innerPadding) ) { - OSHClickableCard( - onCardClick = {}, - title = stringResource(R.string.manage_servers), - subtitle = "0 of 0 server(s) enabled" - ) - OSHSwitchCard( - title = stringResource(R.string.enable_sos_service), - subtitle = stringResource(R.string.summary_sos), - checked = sosEnabled, - onCheckedChange = { sosEnabled = it } - ) - OSHSwitchCard( - title = stringResource(R.string.enable_csapi_service), - subtitle = stringResource(R.string.summary_csapi), - checked = csApiEnabled, - onCheckedChange = { csApiEnabled = it } - ) - OSHSwitchCard( - title = stringResource(R.string.enable_discovery_service), - subtitle = stringResource(R.string.summary_discovery), - checked = discoveryEnabled, - onCheckedChange = { discoveryEnabled = it } - ) + OSHCard { + OSHClickableRowWithIcon( + title = stringResource(R.string.manage_servers), +// subtitle = "0 of 0 server(s) enabled", + imageVector = Icons.Default.Cloud, + contentDescription = stringResource(R.string.manage_servers), + onClick = { navController.navigate(Screen.ServerProfiles.route)} + ) + } + + OSHCard { + OSHSwitchRow( + title = stringResource(R.string.enable_sos_service), + subtitle = stringResource(R.string.summary_sos), + checked = sosEnabled, + onCheckedChange = { sosEnabled = it } + ) + OSHSwitchRow( + title = stringResource(R.string.enable_csapi_service), + subtitle = stringResource(R.string.summary_csapi), + checked = csApiEnabled, + onCheckedChange = { csApiEnabled = it } + ) + OSHSwitchRow( + title = stringResource(R.string.enable_discovery_service), + subtitle = stringResource(R.string.summary_discovery), + checked = discoveryEnabled, + onCheckedChange = { discoveryEnabled = it } + ) + } } } } From 4271a1566c0e181170b591b182a7f19e30e8c9d3 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 13:40:08 -0500 Subject: [PATCH 37/63] changed onClick functions, and updated to use OSHClickableCard --- .../sensorhub/android/ui/components/Card.kt | 20 ++++++++++--------- .../ui/screens/ServerProfilesScreen.kt | 2 +- .../android/ui/screens/ServersScreen.kt | 16 +++++++-------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index 528e55e1..4329c055 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -54,7 +54,7 @@ fun OSHCard( @Composable fun OSHClickableCardWithIcon( - onCardClick: () -> Unit, + onClick: () -> Unit, title: String, modifier: Modifier = Modifier, imageVector: ImageVector, @@ -67,19 +67,21 @@ fun OSHClickableCardWithIcon( imageVector = imageVector, contentDescription = contentDescription, modifier = modifier, - onClick = onCardClick + onClick = onClick ) } } @Composable fun OSHClickableCard( - onCardClick: () -> Unit, + onClick: () -> Unit, title: String, subtitle: String, modifier: Modifier = Modifier, ) { - OSHCard(modifier = modifier.clickable { onCardClick() }) { + OSHCard( + modifier = modifier.clickable { onClick() } + ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -128,7 +130,7 @@ fun OSHActionCard( subtitle: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, - onClicked: () -> Unit, + onClick: () -> Unit, modifier: Modifier = Modifier, imageVector: ImageVector, contentDescription: String @@ -140,7 +142,7 @@ fun OSHActionCard( checked = checked, onCheckedChange = onCheckedChange, trailingContent = { - IconButton(onClick = { onClicked() }) { + IconButton(onClick = { onClick() }) { Icon( imageVector = imageVector, contentDescription = contentDescription @@ -232,12 +234,12 @@ private fun SettingsItemPreview() { OSHTheme { Column { OSHClickableCard( - onCardClick = {}, + onClick = {}, title = "Manage Sensors", subtitle = "1 of 2 server(s) enabled", ) OSHClickableCardWithIcon ( - onCardClick = {}, + onClick = {}, title = "Language", imageVector = Icons.Default.Language, contentDescription = "Select in app language" @@ -256,7 +258,7 @@ private fun ServerItemsCard() { subtitle = "http:localhost:8080/sensorhub/api", checked = true, onCheckedChange = {}, - onClicked = {}, + onClick = {}, imageVector = Icons.Filled.Edit, contentDescription = "Edit Server" ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt index d290d55c..f1b76ab2 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -53,7 +53,7 @@ fun ServerProfilesScreen( subtitle = "${item.host}:${item.port}${item.endpointPath}", checked = enabled, onCheckedChange = { enabled = it }, - onClicked = { /* navigate to edit */ }, + onClick = { /* navigate to edit */ }, imageVector = Icons.Filled.Edit, contentDescription = "Edit ${item.name}" ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 649169f7..7c5bd5c7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.res.stringResource import org.sensorhub.android.R import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHClickableCardWithIcon import org.sensorhub.android.ui.components.OSHClickableRowWithIcon import org.sensorhub.android.ui.components.OSHSwitchRow @@ -52,15 +53,14 @@ fun ServersScreen(navController: NavController = rememberNavController()) { Column( modifier = Modifier.padding(innerPadding) ) { - OSHCard { - OSHClickableRowWithIcon( - title = stringResource(R.string.manage_servers), + OSHClickableCardWithIcon ( + title = stringResource(R.string.manage_servers), // subtitle = "0 of 0 server(s) enabled", - imageVector = Icons.Default.Cloud, - contentDescription = stringResource(R.string.manage_servers), - onClick = { navController.navigate(Screen.ServerProfiles.route)} - ) - } + imageVector = Icons.Default.Cloud, + contentDescription = stringResource(R.string.manage_servers), + onClick = { navController.navigate(Screen.ServerProfiles.route)} + ) + OSHCard { OSHSwitchRow( From a873217b38f8e89e5f1266707ff434a87f89c084 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 15:46:11 -0500 Subject: [PATCH 38/63] adding server form --- .../org/sensorhub/android/ui/OSHNavHost.kt | 12 +- .../src/org/sensorhub/android/ui/Screen.kt | 1 + .../android/ui/components/OSHInputField.kt | 136 +++++++++++++ .../ui/screens/AppPreferencesScreen.kt | 3 +- .../android/ui/screens/ServerFormScreen.kt | 183 ++++++++++++++++++ .../ui/screens/ServerProfilesScreen.kt | 12 +- .../android/ui/screens/ServersScreen.kt | 3 +- 7 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 47751b4a..e47bcbf8 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -13,6 +13,7 @@ import org.sensorhub.android.ui.screens.FaqItem import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen +import org.sensorhub.android.ui.screens.ServerFormScreen import org.sensorhub.android.ui.screens.ServerProfileItem import org.sensorhub.android.ui.screens.ServersScreen import org.sensorhub.android.ui.screens.ServerProfilesScreen @@ -70,7 +71,16 @@ fun OSHNavHost( val serverProfileItems = buildServerProfileItems() ServerProfilesScreen( onBackClick = { navController.popBackStack() }, - items = serverProfileItems + items = serverProfileItems, + navController = navController + ) + } + + composable(Screen.ServerForm.route) { + val context = LocalContext.current + + ServerFormScreen( + onBackClick = { navController.popBackStack() }, ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index 19dc3d83..f93a6fcd 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -8,4 +8,5 @@ sealed class Screen(val route: String) { object AppStatus : Screen("app_status_screen") object HelpFaq : Screen("help_faq_screen") object AppPreferences : Screen("app_prefs_screen") + object ServerForm : Screen("server_form_screen") } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt new file mode 100644 index 00000000..391922e5 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt @@ -0,0 +1,136 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.theme.OSHTheme + +@Composable +fun OSHInputField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier, + error: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailingIcon: @Composable (() -> Unit)? = null +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + leadingIcon = leadingIcon +// { +// if (error != null) { +// Icon(Icons.Default.Warning, contentDescription = "Error", tint = Error) +// } } + , + trailingIcon = trailingIcon, + isError = error != null, + keyboardOptions = keyboardOptions, + visualTransformation = visualTransformation, + singleLine = true, + shape = RoundedCornerShape(16.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + errorBorderColor = MaterialTheme.colorScheme.error + ), + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) + + AnimatedVisibility( + visible = error != null, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + error?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(start = 16.dp, top = 4.dp) + ) + } + } + } +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ServerProfilesScreenPreview() { + var value = "Local Server" + var isPasswordVisible = true + + OSHTheme { + Column { + OSHInputField( + value = value, + onValueChange = { value = it }, + label = "Server Name", + ) + Spacer(modifier = Modifier.width(8.dp)) + + OSHInputField( + value = value, + onValueChange = { value = it }, + label = "Server Name", + error = "Please enter a server name" + ) + Spacer(modifier = Modifier.width(8.dp)) + + OSHInputField( + value = value, + onValueChange = { value = it }, + label = "Password", + visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { + Icon( + imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle password visibility" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index a8b2f08e..6e7fb2ea 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -108,7 +108,8 @@ fun AppPreferencesScreen( }, ) { innerPadding -> Column( - modifier = Modifier.padding(innerPadding) + modifier = Modifier + .padding(innerPadding) ) { OSHCard { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt new file mode 100644 index 00000000..db37542a --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt @@ -0,0 +1,183 @@ +package org.sensorhub.android.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHButton +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHInputField +import org.sensorhub.android.ui.components.OSHSegmentedButton +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background +import org.sensorhub.android.ui.theme.OSHTheme + +@Composable +fun ServerFormScreen( + onBackClick: () -> Unit +) { + + var id by rememberSaveable { mutableStateOf("") } + var serverName by rememberSaveable { mutableStateOf("") } + var host by rememberSaveable { mutableStateOf("") } + var port by rememberSaveable { mutableStateOf("") } + var endpointPath by rememberSaveable { mutableStateOf("") } + var username by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + var enableTls by rememberSaveable { mutableStateOf("") } + var disableSslCheck by rememberSaveable { mutableStateOf("") } + var useConSysClient by rememberSaveable { mutableStateOf("") } + var oAuthEnabled by rememberSaveable { mutableStateOf("") } + var enabled by rememberSaveable { mutableStateOf("") } + var clientId by rememberSaveable { mutableStateOf("") } + var clientSecret by rememberSaveable { mutableStateOf("") } + var tokenEndpoint by rememberSaveable { mutableStateOf("") } + + // validation + var portError by rememberSaveable { mutableStateOf(null) } + + + var isPasswordVisible by remember { mutableStateOf(false) } + var isClientSecretVisible by remember { mutableStateOf(false) } + + + Scaffold( + topBar = { + OSHTopAppBarWithBack( + title = "Server Form", + onBackClick = onBackClick + ) + }, + containerColor = Background + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(padding) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + OSHCard( + modifier = Modifier + .fillMaxSize() + ) { + OSHSegmentedButton( + options = listOf("CS API CLient", "SOS-T Client") + ) + } + + OSHCard( + modifier = Modifier + .fillMaxSize() + ) { + OSHInputField( + value = serverName, + onValueChange = { serverName = it }, + label = "Server Name", + ) + OSHInputField( + value = host, + onValueChange = { host = it }, + label = "Host/ IP Address", + ) + OSHInputField( + value = endpointPath, + onValueChange = { endpointPath = it }, + label = "Endpoint Path", + ) + } + + + OSHInputField( + value = username, + onValueChange = { username = it }, + label = "Username", + ) + OSHInputField( + value = password, + onValueChange = { password = it }, + label = "Password", + visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { + Icon( + imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle password visibility" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + + + OSHButton( + onClick = {}, + text = "Add Server", + modifier = Modifier + .fillMaxSize() + ) + } + } +} + + +data class ServerProfile( + val id: String = "", + val name: String = "", + val host: String = "", + val port: Int = 8181, + val endpointPath: String = "/sensorhub/api", + val username: String = "", + val password: String = "", + val enableTls: Boolean = false, + val disableSslCheck: Boolean = false, + val useConSysClient: Boolean = false, + val oAuthEnabled: Boolean = false, + val enabled: Boolean = false, + val clientId: String = "", + val clientSecret: String = "", + val tokenEndpoint: String = "" +) { + val isValid: Boolean get() = name.isNotBlank() && host.isNotBlank() && ( port >= 0 && port <= 65513) //name == null && host == null && port == null && +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ServerProfilesScreenPreview() { + OSHTheme { + ServerFormScreen( + onBackClick = {}, + ) + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt index f1b76ab2..ce83abf4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -16,6 +16,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHActionCard import org.sensorhub.android.ui.components.OSHAddFAB import org.sensorhub.android.ui.components.OSHTopAppBarWithBack @@ -25,7 +28,8 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServerProfilesScreen( items: List, - onBackClick: () -> Unit + onBackClick: () -> Unit, + navController: NavController = rememberNavController() ) { Scaffold( topBar = { @@ -35,7 +39,9 @@ fun ServerProfilesScreen( ) }, floatingActionButton = { - OSHAddFAB(onClick = {}) + OSHAddFAB( + onClick = { navController.navigate(Screen.ServerForm.route)} + ) }, containerColor = Background ) { padding -> @@ -53,7 +59,7 @@ fun ServerProfilesScreen( subtitle = "${item.host}:${item.port}${item.endpointPath}", checked = enabled, onCheckedChange = { enabled = it }, - onClick = { /* navigate to edit */ }, + onClick = { navController.navigate(Screen.ServerForm.route)}, imageVector = Icons.Filled.Edit, contentDescription = "Edit ${item.name}" ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 7c5bd5c7..c707283a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -51,7 +51,8 @@ fun ServersScreen(navController: NavController = rememberNavController()) { ) }, ) { innerPadding -> Column( - modifier = Modifier.padding(innerPadding) + modifier = Modifier + .padding(innerPadding) ) { OSHClickableCardWithIcon ( title = stringResource(R.string.manage_servers), From 2b8d29d3ee2b6a030d7fabdf771cc414b66a4f57 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 9 Jul 2026 15:47:28 -0500 Subject: [PATCH 39/63] fixed broken click function for fab --- .../src/org/sensorhub/android/ui/components/Fab.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt index 7933b112..387d10a2 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Fab.kt @@ -60,10 +60,10 @@ fun OSHFAB( @Composable fun OSHAddFAB( - onClick: ((Boolean) -> Unit)? = null, + onClick: () -> Unit = {}, ) { FloatingActionButton( - onClick = { onClick }, + onClick = onClick, containerColor = SecondaryContainer, contentColor = OnPrimary From ce88e1af31e26c40e9ac4a6b49c2487fdaddffc5 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 13 Jul 2026 08:39:13 -0500 Subject: [PATCH 40/63] Added optional subtitle, and updated server profiles screen --- .../ui/components/OSHClickableRowWithIcon.kt | 36 +++++- .../android/ui/components/OSHInputField.kt | 1 - .../android/ui/screens/HomeScreen.kt | 5 + .../android/ui/screens/SensorsScreen.kt | 5 + .../android/ui/screens/ServerFormScreen.kt | 117 +++++++++++++----- .../android/ui/screens/ServersScreen.kt | 11 +- 6 files changed, 135 insertions(+), 40 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt index 17b0f31b..9e4c66e7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHClickableRowWithIcon.kt @@ -6,17 +6,21 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.theme.OSHTheme @Composable fun OSHSwitchRow ( title: String, - subtitle: String, + subtitle: String = "", checked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, @@ -34,11 +38,14 @@ fun OSHSwitchRow ( style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.height(4.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (subtitle.isNotBlank()) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } OSHSwitch( @@ -47,4 +54,21 @@ fun OSHSwitchRow ( ) trailingContent() } +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ServerItemsCard() { + OSHTheme { + Column { + OSHSwitchRow( + "Enable TLS", + subtitle = "", + checked = true, + onCheckedChange = {}, + + ) + } + } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt index 391922e5..a1aebe68 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHInputField.kt @@ -69,7 +69,6 @@ fun OSHInputField( ), modifier = Modifier .fillMaxWidth() - .padding(12.dp) ) AnimatedVisibility( diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt index 94eae7b4..85fe6698 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt @@ -1,6 +1,8 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -10,6 +12,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R @@ -37,6 +40,8 @@ fun HomeScreen(navController: NavController = rememberNavController()) { modifier = Modifier.padding(innerPadding) ) { + Spacer(modifier = Modifier.height(16.dp)) + } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index 18760811..32a066af 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -1,6 +1,8 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert @@ -10,6 +12,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R @@ -37,6 +40,8 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { modifier = Modifier.padding(innerPadding) ) { + Spacer(modifier = Modifier.height(16.dp)) + } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt index db37542a..57481967 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt @@ -2,8 +2,11 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions @@ -30,9 +33,9 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sensorhub.android.ui.components.OSHButton -import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHInputField import org.sensorhub.android.ui.components.OSHSegmentedButton +import org.sensorhub.android.ui.components.OSHSwitchRow import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme @@ -49,11 +52,10 @@ fun ServerFormScreen( var endpointPath by rememberSaveable { mutableStateOf("") } var username by rememberSaveable { mutableStateOf("") } var password by rememberSaveable { mutableStateOf("") } - var enableTls by rememberSaveable { mutableStateOf("") } + var enableTls by rememberSaveable { mutableStateOf(false) } + var enableOAuth by rememberSaveable { mutableStateOf(false) } var disableSslCheck by rememberSaveable { mutableStateOf("") } var useConSysClient by rememberSaveable { mutableStateOf("") } - var oAuthEnabled by rememberSaveable { mutableStateOf("") } - var enabled by rememberSaveable { mutableStateOf("") } var clientId by rememberSaveable { mutableStateOf("") } var clientSecret by rememberSaveable { mutableStateOf("") } var tokenEndpoint by rememberSaveable { mutableStateOf("") } @@ -66,6 +68,37 @@ fun ServerFormScreen( var isClientSecretVisible by remember { mutableStateOf(false) } + if (enableOAuth) { + OSHInputField( + value = clientId, + onValueChange = { clientId = it }, + label = "Client ID", + ) + OSHInputField( + value = tokenEndpoint, + onValueChange = { tokenEndpoint = it }, + label = "Token Endpoint", + ) + OSHInputField( + value = clientSecret, + onValueChange = { clientSecret = it }, + label = "ClientSecret", + visualTransformation = if (isClientSecretVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { isClientSecretVisible = !isClientSecretVisible }) { + Icon( + imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle secret visibility" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + } + Scaffold( topBar = { OSHTopAppBarWithBack( @@ -80,44 +113,52 @@ fun ServerFormScreen( .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(padding) - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { + Spacer(modifier = Modifier.height(16.dp)) + + OSHSegmentedButton( + options = listOf("CS API Client", "SOS-T Client") + ) - OSHCard( - modifier = Modifier - .fillMaxSize() - ) { - OSHSegmentedButton( - options = listOf("CS API CLient", "SOS-T Client") - ) - } - - OSHCard( - modifier = Modifier - .fillMaxSize() + OSHInputField( + value = serverName, + onValueChange = { serverName = it }, + label = "Server name", + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - OSHInputField( - value = serverName, - onValueChange = { serverName = it }, - label = "Server Name", - ) OSHInputField( value = host, - onValueChange = { host = it }, - label = "Host/ IP Address", + onValueChange = { host = it }, + label = "Host / IP", + modifier = Modifier.weight(1f), ) OSHInputField( - value = endpointPath, - onValueChange = { endpointPath = it }, - label = "Endpoint Path", + value = port, + onValueChange = { port = it }, + label = "Port", + modifier = Modifier.weight(0.4f), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), ) } - + OSHInputField( + value = endpointPath, + onValueChange = { endpointPath = it }, + label = "Endpoint path", + ) + OSHSwitchRow( + title = "Enable TLS", + checked = enableTls, + onCheckedChange = { enableTls = it }, + ) OSHInputField( value = username, - onValueChange = { username = it }, + onValueChange = { username = it }, label = "Username", ) OSHInputField( @@ -140,6 +181,12 @@ fun ServerFormScreen( ) + OSHSwitchRow( + title = "Enable OAuth", + checked = enableOAuth, + onCheckedChange = { enableOAuth = it }, + ) + OSHButton( onClick = {}, text = "Add Server", @@ -151,6 +198,14 @@ fun ServerFormScreen( } +//fun toggleServerClient() { +// if(client == "api") { +// endpoint = "/sensorhub/api" +// } else { +// endpoint = "/sensorhub/sos" +// } +//} + data class ServerProfile( val id: String = "", val name: String = "", @@ -168,7 +223,7 @@ data class ServerProfile( val clientSecret: String = "", val tokenEndpoint: String = "" ) { - val isValid: Boolean get() = name.isNotBlank() && host.isNotBlank() && ( port >= 0 && port <= 65513) //name == null && host == null && port == null && + val isValid: Boolean get() = name.isNotBlank() && host.isNotBlank() && ( port >= 0 && port <= 65513) } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index c707283a..30b762e7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -1,7 +1,11 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.MoreVert @@ -23,6 +27,7 @@ import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import org.sensorhub.android.R import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHCard @@ -54,15 +59,17 @@ fun ServersScreen(navController: NavController = rememberNavController()) { modifier = Modifier .padding(innerPadding) ) { + + Spacer(modifier = Modifier.height(16.dp)) + OSHClickableCardWithIcon ( title = stringResource(R.string.manage_servers), // subtitle = "0 of 0 server(s) enabled", imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.manage_servers), - onClick = { navController.navigate(Screen.ServerProfiles.route)} + onClick = { navController.navigate(Screen.ServerProfiles.route)}, ) - OSHCard { OSHSwitchRow( title = stringResource(R.string.enable_sos_service), From fa9252e03f620c97757d4f93f62740537f91131c Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 13 Jul 2026 09:21:58 -0500 Subject: [PATCH 41/63] updated buttons --- sensorhub-android-app/res/values/strings.xml | 1 + .../sensorhub/android/ui/components/Button.kt | 48 +++++++++++-------- .../android/ui/screens/ServerFormScreen.kt | 4 +- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 46dd69c2..f707f9e2 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -310,6 +310,7 @@ Settings Servers App Preferences + Add Server summ Enter a message! diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt index 5cfc490d..a532229b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -29,7 +29,7 @@ fun OSHButton( modifier: Modifier = Modifier, enabled: Boolean = true ) { - Button( + ElevatedButton( onClick = onClick, modifier = modifier, enabled = enabled, @@ -38,11 +38,7 @@ fun OSHButton( containerColor = Primary, contentColor = OnPrimary ), - elevation = ButtonDefaults.buttonElevation( - defaultElevation = 8.dp, - pressedElevation = 12.dp, - disabledElevation = 0.dp - ) + ) { Text(text) } @@ -55,15 +51,11 @@ fun OSHTonalButton( modifier: Modifier = Modifier, enabled: Boolean = true ) { - Button( + FilledTonalButton( onClick = onClick, modifier = modifier, enabled = enabled, shape = PillShape, - colors = ButtonDefaults.buttonColors( - containerColor = SecondaryContainer, - contentColor = OnPrimary - ), elevation = ButtonDefaults.buttonElevation( defaultElevation = 8.dp, pressedElevation = 12.dp, @@ -86,15 +78,27 @@ fun OSHOutlinedButton( modifier = modifier, enabled = enabled, shape = PillShape, - border = BorderStroke(1.dp, Outline), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = TextPrimary + ) { + Text(text) + } +} + +@Composable +fun OSHElevatedButton( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + ElevatedButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + shape = PillShape, + colors = ButtonDefaults.buttonColors( + containerColor = Primary, + contentColor = OnPrimary ), - elevation = ButtonDefaults.buttonElevation( - defaultElevation = 8.dp, - pressedElevation = 12.dp, - disabledElevation = 0.dp - ) ) { Text(text) } @@ -135,7 +139,8 @@ fun OSHSegmentedButton( modifier = Modifier.weight(1f), shape = SegmentedButtonDefaults.itemShape( index = index, - count = options.size + count = options.size, + baseShape = RoundedCornerShape(25) ), onClick = { selectedIndex = index }, selected = index == selectedIndex, @@ -162,10 +167,13 @@ private fun ButtonPreview() { OSHOutlinedButton(onClick = {}, text = "Outlined Button") Spacer(modifier = Modifier.height(8.dp)) OSHTextButton(onClick = {}, text = "Text Button") + Spacer(modifier = Modifier.height(8.dp)) + OSHElevatedButton(onClick = {}, text = "Elevated Button") } } } + @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun SegmentedButtonPreview() { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt index 57481967..acdd0310 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt @@ -26,12 +26,14 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.sensorhub.android.R import org.sensorhub.android.ui.components.OSHButton import org.sensorhub.android.ui.components.OSHInputField import org.sensorhub.android.ui.components.OSHSegmentedButton @@ -102,7 +104,7 @@ fun ServerFormScreen( Scaffold( topBar = { OSHTopAppBarWithBack( - title = "Server Form", + title = stringResource(R.string.add_server), onBackClick = onBackClick ) }, From a1d374bd074266feb24abb38975e2ad65fd48713 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 13 Jul 2026 10:15:08 -0500 Subject: [PATCH 42/63] removed unusued imports and added filter chip --- .../org/sensorhub/android/ui/OSHNavHost.kt | 7 +-- .../sensorhub/android/ui/components/Chip.kt | 61 +++++++++++++++++++ .../ui/screens/AppPreferencesScreen.kt | 15 +---- .../android/ui/screens/ServersScreen.kt | 14 ++--- 4 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index e47bcbf8..04c738c1 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -105,10 +105,9 @@ private fun buildFaqItems( return items } -private fun buildServerProfileItems( - -): List { +private fun buildServerProfileItems(): List { val items = mutableListOf() return items; -} \ No newline at end of file +} + diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt new file mode 100644 index 00000000..a6e20049 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt @@ -0,0 +1,61 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.tooling.preview.Preview +import org.sensorhub.android.ui.theme.OSHTheme + + +@Composable +fun OSHFilterChip( + onClick: () -> Unit, + text: String, + modifier: Modifier = Modifier, + selected: Boolean = true, + imageVector: ImageVector, + contentDescription: String +) { + FilterChip( + onClick = onClick, + label = { + Text(text) + }, + selected = selected, + leadingIcon = if (selected) { + { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + } + } else { + null + } + ) +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ChipPreview() { + OSHTheme { + Column { + OSHFilterChip( + onClick = {}, + text = "Bluetooth", + selected = true, + imageVector = Icons.Default.Bluetooth, + contentDescription = "Blueooth" + ) + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 6e7fb2ea..e3aaba15 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -5,13 +5,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Lan import androidx.compose.material.icons.filled.Language -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,28 +14,20 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R -import org.sensorhub.android.ui.Navbar import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard -import org.sensorhub.android.ui.components.OSHClickableCard -import org.sensorhub.android.ui.components.OSHClickableCardWithIcon import org.sensorhub.android.ui.components.OSHClickableRowWithIcon import org.sensorhub.android.ui.components.OSHInfoRow import org.sensorhub.android.ui.components.OSHSingleChoiceDialog import org.sensorhub.android.ui.components.OSHTextInputDialog -import org.sensorhub.android.ui.components.OSHSwitchCard import org.sensorhub.android.ui.components.OSHTopAppBarWithBack -import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme -import org.sensorhub.android.ui.theme.Secondary @Composable fun AppPreferencesScreen( diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 30b762e7..7846c179 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -1,15 +1,12 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold @@ -19,21 +16,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import org.sensorhub.android.ui.components.OSHClickableCard -import org.sensorhub.android.ui.components.OSHSwitchCard -import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo -import org.sensorhub.android.ui.theme.OSHTheme -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import org.sensorhub.android.R import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHClickableCardWithIcon -import org.sensorhub.android.ui.components.OSHClickableRowWithIcon import org.sensorhub.android.ui.components.OSHSwitchRow +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServersScreen(navController: NavController = rememberNavController()) { From 2f0708d4fdf12e0cf651e20641238d26aa8ca63d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 13 Jul 2026 10:16:19 -0500 Subject: [PATCH 43/63] added filter chips to sensors page --- .../android/ui/screens/SensorsScreen.kt | 78 +++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index 32a066af..9b968c2b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -1,23 +1,47 @@ package org.sensorhub.android.ui.screens +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.DevicesOther +import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHFilterChip +import org.sensorhub.android.ui.components.OSHSwitchRow import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme @Composable @@ -27,22 +51,64 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { OSHTopAppBarWithLogo( title = stringResource(R.string.tab_sensors), actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" ) } }, - ) }, - ) { innerPadding -> + ) + }, + ) { padding -> + val filteredItems = items.filter { it.category in selectedCategories } + val grouped = filteredItems.groupBy { it.category } + Column( - modifier = Modifier.padding(innerPadding) + modifier = Modifier + .fillMaxSize() + .padding(padding) ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OSHFilterChip( + onClick = { + }, + text = stringResource(R.string.category_on_device), + selected = SensorCategory.ON_DEVICE in selectedCategories, + imageVector = Icons.Default.PhoneAndroid, + contentDescription = stringResource(R.string.category_on_device) + ) + OSHFilterChip( + onClick = { + }, + text = stringResource(R.string.category_bluetooth), + selected = SensorCategory.BLUETOOTH in selectedCategories, + imageVector = Icons.Default.Bluetooth, + contentDescription = stringResource(R.string.category_bluetooth) + ) + OSHFilterChip( + onClick = { + }, + text = stringResource(R.string.category_others), + selected = SensorCategory.OTHERS in selectedCategories, + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.category_others) + ) + } Spacer(modifier = Modifier.height(16.dp)) } +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SensorsScreenPreview() { + OSHTheme { + SensorsScreen() } } \ No newline at end of file From f6c043f94b0441f5dae561494c051a8472b4e211 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 13 Jul 2026 16:48:06 -0500 Subject: [PATCH 44/63] create chips, cards for sensors page --- sensorhub-android-app/res/values/strings.xml | 6 +- .../sensorhub/android/ui/components/Card.kt | 20 +++ .../sensorhub/android/ui/components/Chip.kt | 11 +- .../android/ui/components/ExpandableCard.kt | 130 +++++++++++++++++- 4 files changed, 156 insertions(+), 11 deletions(-) diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index f707f9e2..94cf9ae8 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -327,9 +327,11 @@ Language A software platform for building smart sensor networks and the Internet of Things + Tap to configure + - ON-DEVICE - Bluetooth Peripherals + On Device + Bluetooth Others Codec Frame Rate diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index 4329c055..8dfa54d9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -52,6 +52,26 @@ fun OSHCard( } +@Composable +fun OSHSensorCard( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp, + ), + content = content + ) +} @Composable fun OSHClickableCardWithIcon( onClick: () -> Unit, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt index a6e20049..29b398eb 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Chip.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon @@ -21,8 +22,6 @@ fun OSHFilterChip( text: String, modifier: Modifier = Modifier, selected: Boolean = true, - imageVector: ImageVector, - contentDescription: String ) { FilterChip( onClick = onClick, @@ -33,8 +32,8 @@ fun OSHFilterChip( leadingIcon = if (selected) { { Icon( - imageVector = imageVector, - contentDescription = contentDescription, + imageVector = Icons.Default.Check, + contentDescription = "Selcted filter", modifier = Modifier.size(FilterChipDefaults.IconSize) ) } @@ -52,9 +51,7 @@ private fun ChipPreview() { OSHFilterChip( onClick = {}, text = "Bluetooth", - selected = true, - imageVector = Icons.Default.Bluetooth, - contentDescription = "Blueooth" + selected = true ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt index cde8ce86..6d0343da 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/ExpandableCard.kt @@ -3,6 +3,7 @@ package org.sensorhub.android.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -10,10 +11,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -29,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.sensorhub.android.ui.components.OSHExpandableSwitchCard import org.sensorhub.android.ui.theme.* @Composable @@ -85,10 +89,8 @@ fun OSHExpandableCard( } } - // Always-visible collapsed content collapsedContent() - // Animated expandable content AnimatedVisibility( visible = isExpanded, enter = expandVertically(), @@ -103,6 +105,130 @@ fun OSHExpandableCard( } } +@Composable +fun OSHExpandableSwitchCard( + title: String, + subtitle: String = "", + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + configHint: String = "", + expanded: Boolean? = null, + onExpandChange: ((Boolean) -> Unit)? = null, + expandedContent: @Composable ColumnScope.() -> Unit +) { + var internalExpanded by remember { mutableStateOf(false) } + val isExpanded = expanded ?: internalExpanded + val toggleExpanded = { + val newValue = !isExpanded + if (onExpandChange != null) { + onExpandChange(newValue) + } else { + internalExpanded = newValue + } + } + + Card( + onClick = { toggleExpanded() }, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + shape = RoundedCornerShape(12.dp), + colors = CardDefaults.cardColors( + containerColor = SecondaryContainer, + contentColor = TextPrimary + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column(modifier = Modifier.padding(16.dp)) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + if (subtitle.isNotBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = TextSecondary + ) + } + } + OSHSwitch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } + + if (configHint.isNotBlank() && !isExpanded) { + Spacer(modifier = Modifier.height(6.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = configHint, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary + ) + } + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + expandedContent() + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun ExpandableSwitchCardPreview() { + OSHTheme { + Column { + OSHExpandableSwitchCard( + title = "Polar Heart Monitor", + subtitle = "Connect to a Polar heart rate sensor over Bluetooth LE", + checked = true, + onCheckedChange = {}, + expanded = true, + expandedContent = { + } + + ) + Spacer(modifier = Modifier.height(8.dp)) + OSHExpandableSwitchCard( + title = "Accelerometer", + subtitle = "Stream real-time accelerometer motion data", + checked = false, + configHint = "Tap to configure", + onCheckedChange = {}, + expandedContent = {} + ) + } + } +} + @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun ExpandableCardCollapsedPreview() { From 17c3177313516a1e283834f610b02b0dececa846 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 14 Jul 2026 10:48:52 -0500 Subject: [PATCH 45/63] update sensors screen to include expansion cards for configurable sensors (ble/ video/ audio) and added additional filter chip --- sensorhub-android-app/res/values/strings.xml | 1 + .../android/ui/screens/SensorsScreen.kt | 270 ++++++++++++++++-- 2 files changed, 255 insertions(+), 16 deletions(-) diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 94cf9ae8..c36a1915 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -332,6 +332,7 @@ On Device Bluetooth + USB Others Codec Frame Rate diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt index 9b968c2b..884c2728 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt @@ -1,6 +1,7 @@ package org.sensorhub.android.ui.screens import androidx.annotation.StringRes +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -8,17 +9,14 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.DevicesOther -import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.PhoneAndroid -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -37,15 +35,88 @@ import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R import org.sensorhub.android.ui.Screen -import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHClickableRowWithIcon +import org.sensorhub.android.ui.components.OSHExpandableSwitchCard import org.sensorhub.android.ui.components.OSHFilterChip +import org.sensorhub.android.ui.components.OSHSensorCard import org.sensorhub.android.ui.components.OSHSwitchRow import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme +enum class SensorCategory(@StringRes val labelRes: Int) { + ON_DEVICE(R.string.category_on_device), + BLUETOOTH(R.string.category_bluetooth), + USB(R.string.category_usb), + OTHERS(R.string.category_others), +} + +sealed class SensorConfig { + data class BluetoothDevice(@StringRes val selectLabelRes: Int) : SensorConfig() + + data object Video : SensorConfig() + + data object Audio : SensorConfig() + + data object TruPulse : SensorConfig() +} + +data class SensorItem( + val id: String, + @StringRes val nameRes: Int, + @StringRes val summaryRes: Int, + val category: SensorCategory, + val enabled: Boolean = false, + val config: SensorConfig? = null, +) + +val ALL_SENSORS: List = listOf( + // On device sensors + SensorItem("accelerometer", R.string.sensor_accelerometer, R.string.summary_accel, SensorCategory.ON_DEVICE), + SensorItem("gyroscope", R.string.sensor_gyroscope, R.string.summary_gyro, SensorCategory.ON_DEVICE), + SensorItem("magnetometer", R.string.sensor_magnetometer, R.string.summary_mag, SensorCategory.ON_DEVICE), + SensorItem("orientation_q", R.string.sensor_orientation_quat, R.string.summary_orientation, SensorCategory.ON_DEVICE), + SensorItem("orientation_e", R.string.sensor_orientation_euler, R.string.summary_orientation, SensorCategory.ON_DEVICE), + SensorItem("gps", R.string.sensor_gps, R.string.summary_gps, SensorCategory.ON_DEVICE), + SensorItem("network_loc", R.string.sensor_network_location, R.string.summary_netloc, SensorCategory.ON_DEVICE), + + // On device sensors (have additional config) + SensorItem("video", R.string.sensor_video, R.string.summary_video, SensorCategory.ON_DEVICE, config = SensorConfig.Video), + SensorItem("video_roll", R.string.sensor_video_roll, R.string.summary_video_roll, SensorCategory.ON_DEVICE), + SensorItem("audio", R.string.sensor_audio, R.string.summary_audio, SensorCategory.ON_DEVICE, config = SensorConfig.Audio), + + // Bluetooth sensors (have additional config) + SensorItem("meshtastic", R.string.sensor_meshtastic, R.string.summary_meshtastic, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_meshtastic)), + SensorItem("polar", R.string.sensor_polar, R.string.summary_polar, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_polar)), + SensorItem("kestrel", R.string.sensor_kestrel, R.string.summary_kestrel, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_kestrel)), + SensorItem("garmin", R.string.sensor_garmin, R.string.summary_garmin, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_garmin)), + SensorItem("ste", R.string.sensor_ste, R.string.summary_ste, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_device)), + SensorItem("trupulse", R.string.sensor_trupulse, R.string.summary_trupulse, SensorCategory.BLUETOOTH, config = SensorConfig.TruPulse), + + // USB sensors + SensorItem("controller", R.string.sensor_controller, R.string.summary_controller, SensorCategory.USB), + SensorItem("flirone", R.string.sensor_flirone, R.string.summary_flirone, SensorCategory.USB), + + // Others + SensorItem("wardriving", R.string.sensor_wardriving, R.string.summary_wardriving, SensorCategory.OTHERS), +) @Composable -fun SensorsScreen(navController: NavController = rememberNavController()) { +fun SensorsScreen( + items: List = ALL_SENSORS, + onSensorToggled: (SensorItem, Boolean) -> Unit = { _, _ -> }, + onConfigAction: (SensorItem) -> Unit = {}, + navController: NavController = rememberNavController() +) { + var selectedCategories by remember { + mutableStateOf(SensorCategory.entries.toSet()) + } + var sensorStates by remember { + mutableStateOf(items.associate { it.id to it.enabled }) + } + + val scrollState = rememberScrollState() + + Scaffold( topBar = { OSHTopAppBarWithLogo( @@ -61,7 +132,9 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { ) }, ) { padding -> - val filteredItems = items.filter { it.category in selectedCategories } + val resolvedItems = items.map { it.copy(enabled = sensorStates[it.id] ?: it.enabled) } + val filteredItems = if (selectedCategories.isEmpty()) resolvedItems + else resolvedItems.filter { it.category in selectedCategories } val grouped = filteredItems.groupBy { it.category } Column( @@ -71,39 +144,204 @@ fun SensorsScreen(navController: NavController = rememberNavController()) { ) { Row( modifier = Modifier + .horizontalScroll(scrollState) .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { OSHFilterChip( onClick = { + selectedCategories = if (SensorCategory.ON_DEVICE in selectedCategories) + selectedCategories - SensorCategory.ON_DEVICE + else + selectedCategories + SensorCategory.ON_DEVICE }, text = stringResource(R.string.category_on_device), selected = SensorCategory.ON_DEVICE in selectedCategories, - imageVector = Icons.Default.PhoneAndroid, - contentDescription = stringResource(R.string.category_on_device) ) OSHFilterChip( onClick = { + selectedCategories = if (SensorCategory.BLUETOOTH in selectedCategories) + selectedCategories - SensorCategory.BLUETOOTH + else + selectedCategories + SensorCategory.BLUETOOTH }, text = stringResource(R.string.category_bluetooth), selected = SensorCategory.BLUETOOTH in selectedCategories, - imageVector = Icons.Default.Bluetooth, - contentDescription = stringResource(R.string.category_bluetooth) ) OSHFilterChip( onClick = { + selectedCategories = if (SensorCategory.USB in selectedCategories) + selectedCategories - SensorCategory.USB + else + selectedCategories + SensorCategory.USB + }, + text = stringResource(R.string.category_usb), + selected = SensorCategory.USB in selectedCategories, + ) + OSHFilterChip( + onClick = { + selectedCategories = if (SensorCategory.OTHERS in selectedCategories) + selectedCategories - SensorCategory.OTHERS + else + selectedCategories + SensorCategory.OTHERS }, text = stringResource(R.string.category_others), selected = SensorCategory.OTHERS in selectedCategories, - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.category_others) ) } - Spacer(modifier = Modifier.height(16.dp)) + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + grouped.forEach { (category, sensors) -> + item { + Text( + text = stringResource(category.labelRes), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp) + ) + } + items(sensors, key = { it.id }) { sensor -> + val toggleSensor = { checked: Boolean -> + sensorStates = sensorStates + (sensor.id to checked) + onSensorToggled(sensor, checked) + } + if (sensor.config != null) { + SensorExpandableCard( + sensor = sensor, + onCheckedChange = toggleSensor, + onConfigAction = { onConfigAction(sensor) } + ) + } else { + OSHSensorCard { + OSHSwitchRow( + title = stringResource(sensor.nameRes), + subtitle = stringResource(sensor.summaryRes), + checked = sensor.enabled, + onCheckedChange = toggleSensor + ) + } + } + } + } + } + } + } +} +@Composable +private fun SensorExpandableCard( + sensor: SensorItem, + onCheckedChange: (Boolean) -> Unit, + onConfigAction: () -> Unit +) { + OSHExpandableSwitchCard( + title = stringResource(sensor.nameRes), + subtitle = stringResource(sensor.summaryRes), + checked = sensor.enabled, + onCheckedChange = onCheckedChange, + configHint = stringResource(R.string.hint_tap_to_configure), + expandedContent = { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + when (sensor.config) { + is SensorConfig.BluetoothDevice -> { + BluetoothDeviceConfig( + selectLabelRes = sensor.config.selectLabelRes, + onSelect = onConfigAction + ) + } + is SensorConfig.Video -> VideoConfig(onConfigAction) + is SensorConfig.Audio -> AudioConfig(onConfigAction) + is SensorConfig.TruPulse -> TruPulseConfig(onConfigAction) + null -> {} + } } + ) +} + +@Composable +private fun BluetoothDeviceConfig( + @StringRes selectLabelRes: Int, + onSelect: () -> Unit +) { + OSHClickableRowWithIcon( + title = stringResource(selectLabelRes), + subtitle = stringResource(R.string.summary_select_device), + imageVector = Icons.Default.Bluetooth, + contentDescription = stringResource(selectLabelRes), + onClick = onSelect + ) +} + +@Composable +private fun VideoConfig(onConfigAction: () -> Unit) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_codec), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_codec), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_resolution), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_resolution), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_frame_rate), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_frame_rate), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_camera), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_camera), + onClick = onConfigAction + ) +} + +@Composable +private fun AudioConfig(onConfigAction: () -> Unit) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_codec), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_codec), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_sample_rate), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_sample_rate), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_bitrate), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_bitrate), + onClick = onConfigAction + ) +} + +@Composable +private fun TruPulseConfig(onConfigAction: () -> Unit) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_trupulse_datasource), + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_trupulse_datasource), + onClick = onConfigAction + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_select_trupulse), + subtitle = stringResource(R.string.summary_select_device), + imageVector = Icons.Default.Bluetooth, + contentDescription = stringResource(R.string.title_select_trupulse), + onClick = onConfigAction + ) +} @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable From f3b2e7f4cba233766b93fed22f00cc2f1f9cb9e2 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 14 Jul 2026 11:03:14 -0500 Subject: [PATCH 46/63] Added app status page with status dot and status row --- .../sensorhub/android/ui/components/Card.kt | 5 ++ .../android/ui/components/OSHStatusRow.kt | 54 +++++++++++++++++++ .../android/ui/components/StatusDot.kt | 27 ++++++++++ .../ui/screens/AppPreferencesScreen.kt | 5 ++ .../android/ui/screens/AppStatusScreen.kt | 54 ++++++++++++++++--- 5 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt index 8dfa54d9..3b5e768b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Card.kt @@ -196,6 +196,11 @@ private fun DeviceInfoCardPreview() { imageVector = Icons.Default.Language, contentDescription = "Select in app language" ) + + OSHStatusRow( + title = "SOS Service Status", + subtitle = "Connected" + ) } } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt new file mode 100644 index 00000000..948836bc --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt @@ -0,0 +1,54 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Circle +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun OSHStatusRow( + title: String, + subtitle: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null +) { + Row( + modifier = modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable { onClick() } else Modifier) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + StatusDot(isOnline = true,) + Spacer(modifier = Modifier.width(15.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.height(4.dp)) + if (subtitle.isNotBlank()) { + Text( + text = subtitle, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt new file mode 100644 index 00000000..ebbad5ba --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt @@ -0,0 +1,27 @@ +package org.sensorhub.android.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun StatusDot( + isOnline: Boolean, + modifier: Modifier = Modifier, + dotSize: Dp = 10.dp +) { + Box( + modifier = modifier + .size(dotSize) + .background( + color = if (isOnline) Color.Green else Color.Gray, + shape = CircleShape + ) + ) +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index e3aaba15..cb4ee93c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -1,6 +1,8 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Help @@ -16,6 +18,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R @@ -99,6 +102,8 @@ fun AppPreferencesScreen( .padding(innerPadding) ) { + Spacer(modifier = Modifier.height(16.dp)) + OSHCard { OSHInfoRow( label = stringResource(R.string.device_name), diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt index 76fa9f64..d4bc9b58 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -1,19 +1,19 @@ package org.sensorhub.android.ui.screens -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import org.sensorhub.android.R +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHStatusRow import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme -import org.sensorhub.android.R -import androidx.compose.ui.res.stringResource @Composable fun AppStatusScreen( @@ -28,13 +28,53 @@ fun AppStatusScreen( }, containerColor = Background ) { padding -> - LazyColumn( + Column( modifier = Modifier .fillMaxSize() .padding(padding), - contentPadding = PaddingValues(16.dp) ) { + OSHCard { + OSHStatusRow( + title = stringResource(R.string.httpServerStatusLabel), + subtitle = "Connected" + ) + } + + OSHCard { + OSHStatusRow( + title = stringResource(R.string.sosServiceStatusLabel), + subtitle = "Connected" + ) + } + OSHCard { + OSHStatusRow( + title = stringResource(R.string.conSysServiceStatusLabel), + subtitle = "Connected" + ) + } + + OSHCard { + OSHStatusRow( + title = stringResource(R.string.discoveryServiceStatusLabel), + subtitle = "Connected" + ) + } + + OSHCard { + OSHStatusRow( + title = stringResource(R.string.sensorServiceStatusLabel), + subtitle = "Connected" + ) + } + + OSHCard { + OSHStatusRow( + title = stringResource(R.string.storageServiceStatusLabel), + subtitle = "Connected" + ) + } + } } } From ddce63c48b4beac07c4a6eca504f3e6cea18aab5 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 15 Jul 2026 12:22:16 -0500 Subject: [PATCH 47/63] Update AppStatusScreen.kt --- .../android/ui/screens/AppStatusScreen.kt | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt index d4bc9b58..d75a4236 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -3,6 +3,8 @@ package org.sensorhub.android.ui.screens import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -39,42 +41,42 @@ fun AppStatusScreen( title = stringResource(R.string.httpServerStatusLabel), subtitle = "Connected" ) - } + HorizontalDivider( - OSHCard { + ) OSHStatusRow( title = stringResource(R.string.sosServiceStatusLabel), subtitle = "Connected" ) - } - OSHCard { + HorizontalDivider( + + ) OSHStatusRow( title = stringResource(R.string.conSysServiceStatusLabel), subtitle = "Connected" ) - } + HorizontalDivider( - OSHCard { + ) OSHStatusRow( title = stringResource(R.string.discoveryServiceStatusLabel), subtitle = "Connected" ) - } + HorizontalDivider( - OSHCard { + ) OSHStatusRow( title = stringResource(R.string.sensorServiceStatusLabel), subtitle = "Connected" ) - } + HorizontalDivider( - OSHCard { + ) OSHStatusRow( title = stringResource(R.string.storageServiceStatusLabel), subtitle = "Connected" ) } - } } } From d8b3853158f622448081d41fb79bf566527041cc Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 20 Jul 2026 15:42:42 -0500 Subject: [PATCH 48/63] Update AppPreferencesScreen.kt --- .../android/ui/screens/AppPreferencesScreen.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index cb4ee93c..9beea46a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -101,9 +101,7 @@ fun AppPreferencesScreen( modifier = Modifier .padding(innerPadding) ) { - Spacer(modifier = Modifier.height(16.dp)) - OSHCard { OSHInfoRow( label = stringResource(R.string.device_name), @@ -119,7 +117,6 @@ fun AppPreferencesScreen( value = appVersion ) } - OSHCard { OSHClickableRowWithIcon( title = stringResource(R.string.title_notifications), @@ -141,6 +138,14 @@ fun AppPreferencesScreen( contentDescription = stringResource(R.string.title_about), onClick = { showAboutAppDialog = true } ) + + OSHClickableRowWithIcon( + title = stringResource(R.string.app_status_main_fragment), + imageVector = Icons.Default.Info, + contentDescription = stringResource(R.string.app_status_main_fragment), + onClick = { navController.navigate(Screen.AppStatus.route) } + ) + OSHClickableRowWithIcon( title = stringResource(R.string.title_help_faq), imageVector = Icons.Default.Help, @@ -148,7 +153,6 @@ fun AppPreferencesScreen( onClick = { navController.navigate(Screen.HelpFaq.route)} ) } - } } } From ba5ce0e7a0dde4f7b749a9c00f174cf015d9c5c2 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 20 Jul 2026 16:13:39 -0500 Subject: [PATCH 49/63] adding status info for app status screen --- .../sensorhub/android/ui/components/OSHStatusRow.kt | 5 +++-- .../org/sensorhub/android/ui/components/StatusDot.kt | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt index 948836bc..57828163 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt @@ -23,7 +23,8 @@ fun OSHStatusRow( title: String, subtitle: String, modifier: Modifier = Modifier, - onClick: (() -> Unit)? = null + onClick: (() -> Unit)? = null, + status: String ) { Row( modifier = modifier @@ -32,7 +33,7 @@ fun OSHStatusRow( .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - StatusDot(isOnline = true,) + StatusDot(status = status) Spacer(modifier = Modifier.width(15.dp)) Column(modifier = Modifier.weight(1f)) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt index ebbad5ba..ba047020 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt @@ -12,15 +12,23 @@ import androidx.compose.ui.unit.dp @Composable fun StatusDot( - isOnline: Boolean, + status: String, modifier: Modifier = Modifier, dotSize: Dp = 10.dp ) { + + val color = when (status) { + "started" -> Color.Green + "initialized", "starting" -> Color.Yellow + "stopped" -> Color.Red + "unknown" -> Color.Gray + else -> Color.Gray + } Box( modifier = modifier .size(dotSize) .background( - color = if (isOnline) Color.Green else Color.Gray, + color = color, shape = CircleShape ) ) From 3f4f5078378c22c62b9ddeb01b93b0cbb1408a20 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 28 Jul 2026 17:18:35 -0500 Subject: [PATCH 50/63] migrated app status to compose removed app status activity and xml fragments. --- sensorhub-android-app/AndroidManifest.xml | 19 +- .../res/layout/activity_app_status.xml | 341 ------------------ sensorhub-android-app/res/xml/pref_app.xml | 5 + .../android/AppPreferencesActivity.java | 51 +-- .../android/AppPreferencesFragment.java | 8 + .../sensorhub/android/AppStatusActivity.java | 77 ---- .../sensorhub/android/AppStatusActivity.kt | 20 + .../org/sensorhub/android/ui/OSHNavHost.kt | 1 + .../android/ui/components/OSHStatusRow.kt | 2 +- .../android/ui/components/StatusDot.kt | 13 +- .../ui/screens/AppPreferencesScreen.kt | 3 +- .../android/ui/screens/AppStatusScreen.kt | 128 ++++++- .../sensorhub/android/SensorHubService.java | 2 +- 13 files changed, 176 insertions(+), 494 deletions(-) delete mode 100644 sensorhub-android-app/res/layout/activity_app_status.xml delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index c72c5e4b..790a8e52 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -27,6 +27,9 @@ + + + @@ -46,7 +49,12 @@ android:configChanges="orientation|screenSize" android:label="@string/app_name" android:screenOrientation="portrait" - android:exported="false" /> + android:exported="true"> + + + + + - - - - - + android:exported="false" + android:theme="@style/AppTheme" /> diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml deleted file mode 100644 index c4a64d8a..00000000 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ /dev/null @@ -1,341 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/xml/pref_app.xml b/sensorhub-android-app/res/xml/pref_app.xml index a767bbdc..a141187a 100644 --- a/sensorhub-android-app/res/xml/pref_app.xml +++ b/sensorhub-android-app/res/xml/pref_app.xml @@ -48,6 +48,11 @@ android:title="@string/title_about" android:icon="@drawable/ic_info" android:layout="@layout/pref_icon_top" /> + > modules = moduleRegistry.getLoadedModules(); - - for (IModule module : modules) { - var moduleConf = module.getConfiguration(); - - if (moduleConf instanceof ModuleConfig) { - String status = module.getCurrentState().name(); - String moduleId = ((ModuleConfig) moduleConf).id; - - switch (moduleId) { - case "HTTP_SERVER_0": - statusIntent.putExtra("httpStatus", status); - break; - case "SOS_SERVICE": - statusIntent.putExtra("sosService", status); - break; - case "CON_SYS_SERVICE": - statusIntent.putExtra("conSysService", status); - break; - case "DISCOVERY_SERVICE": - statusIntent.putExtra("discoveryService", status); - break; - case "ANDROID_SENSORS": - statusIntent.putExtra("androidSensorStatus", status); - break; - case "ANDROID_SENSORS#storage": - statusIntent.putExtra("sensorStorageStatus", status); - break; - } - } - } - } else { - statusIntent.putExtra("sosService", "N/A"); - statusIntent.putExtra("conSysService", "N/A"); - statusIntent.putExtra("httpStatus", "N/A"); - statusIntent.putExtra("androidSensorStatus", "N/A"); - statusIntent.putExtra("sensorStorageStatus", "N/A"); - } - - startActivity(statusIntent); + startActivity(new Intent(this, AppStatusActivity.class)); } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java index 6c0e483c..dc5c777b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java @@ -51,6 +51,14 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); } + Preference statusPref = findPreference("app_status"); + if (statusPref != null) { + statusPref.setOnPreferenceClickListener(preference -> { + startActivity(new Intent(requireContext(), AppStatusActivity.class)); + return true; + }); + } + Preference helpPref = findPreference("app_help"); if (helpPref != null) { helpPref.setOnPreferenceClickListener(preference -> { diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java deleted file mode 100644 index 4e02d47c..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.sensorhub.android; - -import android.content.Context; -import android.content.Intent; -import android.graphics.drawable.GradientDrawable; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; - -import com.google.android.material.appbar.MaterialToolbar; - -public class AppStatusActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_app_status); - - MaterialToolbar toolbar = findViewById(R.id.status_toolbar); - setSupportActionBar(toolbar); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); - - Intent intent = getIntent(); - - String sosStatus = intent.getStringExtra("sosService"); - String consSysStatus = intent.getStringExtra("conSysService"); - String discoveryStatus = intent.getStringExtra("discoveryService"); - String httpStatus = intent.getStringExtra("httpStatus"); - String sensorStatus = intent.getStringExtra("androidSensorStatus"); - String sensorStorageStatus = intent.getStringExtra("sensorStorageStatus"); - - TextView sosStatusView = findViewById(R.id.sos_service_state); - TextView conSysStatusView = findViewById(R.id.consys_service_state); - TextView discoveryStatusView = findViewById(R.id.discovery_service_state); - TextView httpStatusView = findViewById(R.id.http_service_state); - TextView sensorStatusView = findViewById(R.id.sensor_service_state); - TextView storageStatusView = findViewById(R.id.storage_service_state); - - sosStatusView.setText(sosStatus); - conSysStatusView.setText(consSysStatus); - discoveryStatusView.setText(discoveryStatus); - httpStatusView.setText(httpStatus); - sensorStatusView.setText(sensorStatus); - storageStatusView.setText(sensorStorageStatus); - - setStatusDotColor(findViewById(R.id.sos_status_dot), sosStatus); - setStatusDotColor(findViewById(R.id.consys_status_dot), consSysStatus); - setStatusDotColor(findViewById(R.id.discovery_status_dot), discoveryStatus); - setStatusDotColor(findViewById(R.id.http_status_dot), httpStatus); - setStatusDotColor(findViewById(R.id.sensor_status_dot), sensorStatus); - setStatusDotColor(findViewById(R.id.storage_status_dot), sensorStorageStatus); - } - - private void setStatusDotColor(View dot, String status) { - int colorRes; - if (status == null) { - colorRes = R.color.status_unknown; - } else { - String lower = status.toLowerCase(); - if (lower.contains("started")) { - colorRes = R.color.status_started; - } else if (lower.contains("stopped")) { - colorRes = R.color.status_stopped; - } else if (lower.contains("starting") || lower.contains("initializ")) { - colorRes = R.color.status_initializing; - } else { - colorRes = R.color.status_unknown; - } - } - - GradientDrawable background = (GradientDrawable) dot.getBackground(); - background.setColor(ContextCompat.getColor(this, colorRes)); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt new file mode 100644 index 00000000..42ad82de --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt @@ -0,0 +1,20 @@ +package org.sensorhub.android + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import org.sensorhub.android.ui.screens.AppStatusScreen +import org.sensorhub.android.ui.theme.OSHTheme + +class AppStatusActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + OSHTheme { + AppStatusScreen( + onBackClick = { finish() } + ) + } + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index 04c738c1..dcd4f5bc 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -9,6 +9,7 @@ import androidx.navigation.compose.composable import org.sensorhub.android.ui.screens.AppPreferencesScreen import org.sensorhub.android.ui.screens.AppStatusScreen import org.sensorhub.android.R +import org.sensorhub.android.ui.screens.AppStatusViewModel import org.sensorhub.android.ui.screens.FaqItem import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt index 57828163..6e1ea047 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/OSHStatusRow.kt @@ -24,7 +24,7 @@ fun OSHStatusRow( subtitle: String, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, - status: String + status: String = subtitle ) { Row( modifier = modifier diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt index ba047020..d1c1ddc9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/StatusDot.kt @@ -16,13 +16,12 @@ fun StatusDot( modifier: Modifier = Modifier, dotSize: Dp = 10.dp ) { - - val color = when (status) { - "started" -> Color.Green - "initialized", "starting" -> Color.Yellow - "stopped" -> Color.Red - "unknown" -> Color.Gray - else -> Color.Gray + val _status = status.lowercase() + val color = when { + _status.contains("started") -> Color(0xFF4CAF50) + _status.contains("stopped") -> Color(0xFFEF5350) + _status.contains("initializ" )|| _status.contains("starting") -> Color(0xFFFF9800) + else -> Color(0xFF757575) } Box( modifier = modifier diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 9beea46a..8414aa80 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Language @@ -141,7 +142,7 @@ fun AppPreferencesScreen( OSHClickableRowWithIcon( title = stringResource(R.string.app_status_main_fragment), - imageVector = Icons.Default.Info, + imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.app_status_main_fragment), onClick = { navController.navigate(Screen.AppStatus.route) } ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt index d75a4236..f9124a42 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt @@ -1,26 +1,138 @@ package org.sensorhub.android.ui.screens +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import org.sensorhub.android.R +import org.sensorhub.android.SensorHubService import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHStatusRow import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.api.module.ModuleConfig + +data class AppStatusState ( + val httpStatus: String = "Unknown", + val sosStatus: String = "Unknown", + val conSysStatus: String = "Unknown", + val discoveryStatus: String = "Unknown", + val sensorStatus: String = "Unknown", + val storageStatus: String = "Unknown", +) + +class AppStatusViewModel(application: Application): AndroidViewModel(application) { + private val _state = MutableStateFlow(AppStatusState()) + val state: StateFlow = _state.asStateFlow() + private var boundService: SensorHubService? = null + + private val sConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + boundService = (service as SensorHubService.LocalBinder).service + refreshStatus() + } + + override fun onServiceDisconnected(name: ComponentName?) { + boundService = null + } + } + + init { + val context = getApplication() + context.bindService( + Intent(context, SensorHubService::class.java), + sConn, + Context.BIND_AUTO_CREATE + ) + viewModelScope.launch { + while (isActive) { + delay(3000) + refreshStatus() + } + } + } + + private fun refreshStatus() { + val service = boundService ?: return + val sensorhub = service.sensorHub ?: return + val modules = sensorhub.moduleRegistry.loadedModules + + var http = "Unknown" + var sos = "Unknown" + var conSys = "Unknown" + var discovery = "Unknown" + var sensor = "Unknown" + var storage = "Unknown" + + for (module in modules) { + val moduleConf = module.configuration + if (moduleConf is ModuleConfig) { + val status = module.currentState.name + val moduleId = moduleConf.id + + when (moduleId) { + "HTTP_SERVER_0" -> http = status + "SOS_SERVICE" -> sos = status + "CON_SYS_SERVICE" -> conSys = status + "DISCOVERY_SERVICE" -> discovery = status + "ANDROID_SENSORS" -> sensor = status + "ANDROID_SENSORS#storage" -> storage = status + } + } + } + + _state.value = AppStatusState( + httpStatus = http, + sosStatus = sos, + conSysStatus = conSys, + discoveryStatus = discovery, + sensorStatus = sensor, + storageStatus = storage + ) + + } + + override fun onCleared() { + super.onCleared() + try { + getApplication().unbindService(sConn) + } catch (_: IllegalArgumentException) { + + } + boundService = null + } +} @Composable fun AppStatusScreen( - onBackClick: () -> Unit + onBackClick: () -> Unit, + viewModel: AppStatusViewModel = viewModel() ) { + val state by viewModel.state.collectAsStateWithLifecycle() + Scaffold( topBar = { OSHTopAppBarWithBack( @@ -39,42 +151,42 @@ fun AppStatusScreen( OSHCard { OSHStatusRow( title = stringResource(R.string.httpServerStatusLabel), - subtitle = "Connected" + subtitle = state.httpStatus ) HorizontalDivider( ) OSHStatusRow( title = stringResource(R.string.sosServiceStatusLabel), - subtitle = "Connected" + subtitle = state.sosStatus ) HorizontalDivider( ) OSHStatusRow( title = stringResource(R.string.conSysServiceStatusLabel), - subtitle = "Connected" + subtitle = state.conSysStatus ) HorizontalDivider( ) OSHStatusRow( title = stringResource(R.string.discoveryServiceStatusLabel), - subtitle = "Connected" + subtitle = state.discoveryStatus ) HorizontalDivider( ) OSHStatusRow( title = stringResource(R.string.sensorServiceStatusLabel), - subtitle = "Connected" + subtitle = state.sensorStatus ) HorizontalDivider( ) OSHStatusRow( title = stringResource(R.string.storageServiceStatusLabel), - subtitle = "Connected" + subtitle = state.storageStatus ) } } diff --git a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java index d1ec8520..70ce777b 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java +++ b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java @@ -55,7 +55,7 @@ public class SensorHubService extends Service private LocationListener locationListener; public class LocalBinder extends Binder { - SensorHubService getService() { + public SensorHubService getService() { return SensorHubService.this; } } From 2efa30a608cbbf238e785d8640b16b2f84fa554d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 13:54:31 -0500 Subject: [PATCH 51/63] add functions for app preferences lang/devicename/ version and ip and remove dead code removed layouts for app prefs and help/faq moved activities to dir added functionality to app prefs added component activity for help faq, app status and app prefs --- sensorhub-android-app/AndroidManifest.xml | 8 +- .../res/layout/activity_help_faq.xml | 36 ----- .../res/layout/fragment_app_preferences.xml | 34 ----- .../res/layout/item_faq_entry.xml | 56 ------- .../res/layout/item_faq_header.xml | 15 -- sensorhub-android-app/res/values/strings.xml | 2 + .../res/xml/pref_settings.xml | 9 ++ .../android/AppPreferencesActivity.java | 59 -------- .../android/AppPreferencesFragment.java | 136 ----------------- .../sensorhub/android/HelpFaqActivity.java | 142 ------------------ .../org/sensorhub/android/MainActivity.java | 3 - .../sensorhub/android/SettingsFragment.java | 12 ++ .../activities/AppPreferencesActivity.kt | 18 +++ .../{ => activities}/AppStatusActivity.kt | 4 +- .../{ => activities}/ComposeMainActivity.kt | 2 +- .../android/activities/HelpFaqActivity.kt | 40 +++++ .../ui/screens/AppPreferencesScreen.kt | 134 ++++++++++++++--- 17 files changed, 203 insertions(+), 507 deletions(-) delete mode 100644 sensorhub-android-app/res/layout/activity_help_faq.xml delete mode 100644 sensorhub-android-app/res/layout/fragment_app_preferences.xml delete mode 100644 sensorhub-android-app/res/layout/item_faq_entry.xml delete mode 100644 sensorhub-android-app/res/layout/item_faq_header.xml delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppPreferencesActivity.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/activities/AppPreferencesActivity.kt rename sensorhub-android-app/src/org/sensorhub/android/{ => activities}/AppStatusActivity.kt (92%) rename sensorhub-android-app/src/org/sensorhub/android/{ => activities}/ComposeMainActivity.kt (96%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/activities/HelpFaqActivity.kt diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 790a8e52..bd7049af 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -57,7 +57,7 @@ @@ -73,17 +73,17 @@ android:windowSoftInputMode="adjustResize" android:exported="false" /> - - - - - - - - - - - diff --git a/sensorhub-android-app/res/layout/fragment_app_preferences.xml b/sensorhub-android-app/res/layout/fragment_app_preferences.xml deleted file mode 100644 index 89698646..00000000 --- a/sensorhub-android-app/res/layout/fragment_app_preferences.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/layout/item_faq_entry.xml b/sensorhub-android-app/res/layout/item_faq_entry.xml deleted file mode 100644 index 7d88c563..00000000 --- a/sensorhub-android-app/res/layout/item_faq_entry.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/layout/item_faq_header.xml b/sensorhub-android-app/res/layout/item_faq_header.xml deleted file mode 100644 index 3e721b24..00000000 --- a/sensorhub-android-app/res/layout/item_faq_header.xml +++ /dev/null @@ -1,15 +0,0 @@ - - diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index c36a1915..aadc3a40 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -323,11 +323,13 @@ Language Version About application + Send Feedback Help / FAQ Language A software platform for building smart sensor networks and the Internet of Things Tap to configure + Send Feedback On Device diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index 6a30d18a..5b416205 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -2,6 +2,15 @@ + + + + + onBackPressed()); - - bindService(new Intent(this, SensorHubService.class), sConn, Context.BIND_AUTO_CREATE); - - if (savedInstanceState == null) { - getSupportFragmentManager().beginTransaction() - .replace(R.id.app_prefs_container, new AppPreferencesFragment()) - .commit(); - } - } - - @Override - protected void onDestroy() { - if (boundService != null) { - unbindService(sConn); - boundService = null; - } - super.onDestroy(); - } - - public void launchAppStatus() { - startActivity(new Intent(this, AppStatusActivity.class)); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java deleted file mode 100644 index dc5c777b..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.sensorhub.android; - -import static android.content.Context.WIFI_SERVICE; - -import android.app.LocaleManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.net.wifi.WifiManager; -import android.os.Bundle; -import android.os.LocaleList; -import android.preference.PreferenceManager; - -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteOrder; - - -public class AppPreferencesFragment extends PreferenceFragmentCompat { - private static final String[] LANGUAGE_LABELS = {"English", "中文 (台灣)", "Español", "Français", "Deutsch", "Italiano", "Português"}; - private static final String[] LANGUAGE_VALUES = {"en", "zh-TW", "es", "fr", "de", "it", "pt"}; - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.pref_app, rootKey); - - Preference languagePref = findPreference("app_language"); - if (languagePref != null) { - languagePref.setOnPreferenceClickListener(preference -> { - showLanguageDialog(); - return true; - }); - } - - Preference aboutPref = findPreference("app_about"); - if (aboutPref != null) { - aboutPref.setOnPreferenceClickListener(preference -> { - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.app_name) - .setMessage(R.string.about_description) - .setIcon(R.drawable.ic_launcher) - .setPositiveButton(R.string.btn_ok, null) - .show(); - return true; - }); - } - - Preference statusPref = findPreference("app_status"); - if (statusPref != null) { - statusPref.setOnPreferenceClickListener(preference -> { - startActivity(new Intent(requireContext(), AppStatusActivity.class)); - return true; - }); - } - - Preference helpPref = findPreference("app_help"); - if (helpPref != null) { - helpPref.setOnPreferenceClickListener(preference -> { - startActivity(new Intent(requireContext(), HelpFaqActivity.class)); - return true; - }); - } - - Preference versionPref = findPreference("app_version"); - if (versionPref != null) { - String version = getString(R.string.title_version); - try { - PackageInfo pInfo = requireContext().getPackageManager() - .getPackageInfo(requireContext().getPackageName(), 0); - version = pInfo.versionName; - } catch (PackageManager.NameNotFoundException ignored) { - } - versionPref.setSummary(version); - } - - //DEVICE IP ADDRESS - getDeviceIpAddress(); - } - - private void getDeviceIpAddress() { - WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); - int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); - - if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { - ipAddress = Integer.reverseBytes(ipAddress); - } - - byte[] ipByteArray = BigInteger.valueOf(ipAddress).toByteArray(); - - String ipAddressString; - try { - ipAddressString = InetAddress.getByAddress(ipByteArray).getHostAddress(); - } catch (UnknownHostException ex) { - ipAddressString = getString(R.string.unable_to_get_ip); - } - - Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); - ipAddressLabel.setSummary(ipAddressString); - } - - - private void showLanguageDialog() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - String currentLanguage = prefs.getString("app_language", "en"); - - int checkedIndex = 0; - for (int i = 0; i < LANGUAGE_VALUES.length; i++) { - if (LANGUAGE_VALUES[i].equals(currentLanguage)) { - checkedIndex = i; - break; - } - } - - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.title_language_dialog) - .setSingleChoiceItems(LANGUAGE_LABELS, checkedIndex, (dialog, which) -> { - String selected = LANGUAGE_VALUES[which]; - prefs.edit().putString("app_language", selected).apply(); - applyLanguage(selected); - dialog.dismiss(); - }) - .setNegativeButton(R.string.btn_cancel, null) - .show(); - } - - private void applyLanguage(String localeTag) { - LocaleManager localeManager = requireContext().getSystemService(LocaleManager.class); - localeManager.setApplicationLocales(LocaleList.forLanguageTags(localeTag)); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java b/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java deleted file mode 100644 index 05828eb2..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java +++ /dev/null @@ -1,142 +0,0 @@ -package org.sensorhub.android; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.appbar.MaterialToolbar; - -import java.util.ArrayList; -import java.util.List; - -public class HelpFaqActivity extends AppCompatActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_help_faq); - - MaterialToolbar toolbar = findViewById(R.id.help_faq_toolbar); - setSupportActionBar(toolbar); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); - - RecyclerView recyclerView = findViewById(R.id.faq_recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setAdapter(new FaqAdapter(buildFaqItems())); - } - - private List buildFaqItems() { - List items = new ArrayList<>(); - String[] questions = getResources().getStringArray(R.array.faq_questions); - String[] answers = getResources().getStringArray(R.array.faq_answers); - String[] categories = getResources().getStringArray(R.array.faq_categories); - - String currentCategory = ""; - for (int i = 0; i < questions.length; i++) { - String category = categories[i]; - if (!category.equals(currentCategory)) { - items.add(new FaqItem(category, null, true)); - currentCategory = category; - } - items.add(new FaqItem(questions[i], answers[i], false)); - } - return items; - } - - private static class FaqItem { - final String text; - final String answer; - final boolean isHeader; - boolean expanded; - - FaqItem(String text, String answer, boolean isHeader) { - this.text = text; - this.answer = answer; - this.isHeader = isHeader; - this.expanded = false; - } - } - - private static class FaqAdapter extends RecyclerView.Adapter { - private static final int TYPE_HEADER = 0; - private static final int TYPE_ITEM = 1; - - private final List items; - - FaqAdapter(List items) { - this.items = items; - } - - @Override - public int getItemViewType(int position) { - return items.get(position).isHeader ? TYPE_HEADER : TYPE_ITEM; - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - if (viewType == TYPE_HEADER) { - View view = inflater.inflate(R.layout.item_faq_header, parent, false); - return new HeaderViewHolder(view); - } else { - View view = inflater.inflate(R.layout.item_faq_entry, parent, false); - return new ItemViewHolder(view); - } - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - FaqItem item = items.get(position); - if (holder instanceof HeaderViewHolder) { - ((HeaderViewHolder) holder).title.setText(item.text); - } else if (holder instanceof ItemViewHolder) { - ItemViewHolder itemHolder = (ItemViewHolder) holder; - itemHolder.question.setText(item.text); - itemHolder.answer.setText(item.answer); - itemHolder.answer.setVisibility(item.expanded ? View.VISIBLE : View.GONE); - itemHolder.expandIcon.setImageResource( - item.expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); - itemHolder.itemView.setOnClickListener(v -> { - item.expanded = !item.expanded; - notifyItemChanged(position); - }); - } - } - - @Override - public int getItemCount() { - return items.size(); - } - - static class HeaderViewHolder extends RecyclerView.ViewHolder { - final TextView title; - - HeaderViewHolder(View itemView) { - super(itemView); - title = itemView.findViewById(R.id.faq_header_title); - } - } - - static class ItemViewHolder extends RecyclerView.ViewHolder { - final TextView question; - final TextView answer; - final ImageView expandIcon; - - ItemViewHolder(View itemView) { - super(itemView); - question = itemView.findViewById(R.id.faq_question); - answer = itemView.findViewById(R.id.faq_answer); - expandIcon = itemView.findViewById(R.id.faq_expand_icon); - } - } - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 0e6133fc..671be06a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -589,9 +589,6 @@ protected void onCreate(Bundle savedInstanceState) setSupportActionBar(toolbar); toolbarTitle = findViewById(R.id.toolbar_title); - findViewById(R.id.btn_app_preferences).setOnClickListener(v -> - startActivity(new Intent(this, AppPreferencesActivity.class))); - if (getSupportActionBar() != null) { getSupportActionBar().setDisplayShowTitleEnabled(false); } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index 7b6ae471..97a5a76a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -9,6 +9,7 @@ import org.sensorhub.android.server.ServerProfileRepository; import org.sensorhub.android.server.ServerProfilesActivity; +import org.sensorhub.android.activities.AppPreferencesActivity; /* @@ -20,10 +21,21 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref_settings, rootKey); + setupAppPreferences(); manageServerProfiles(); setupDiscoveryToggle(); } + private void setupAppPreferences() { + Preference appPrefsPref = findPreference("app_preferences"); + if (appPrefsPref != null) { + appPrefsPref.setOnPreferenceClickListener(p -> { + startActivity(new Intent(requireContext(), AppPreferencesActivity.class)); + return true; + }); + } + } + @Override public void onResume() { super.onResume(); diff --git a/sensorhub-android-app/src/org/sensorhub/android/activities/AppPreferencesActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/activities/AppPreferencesActivity.kt new file mode 100644 index 00000000..efacbfc1 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/activities/AppPreferencesActivity.kt @@ -0,0 +1,18 @@ +package org.sensorhub.android.activities + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import org.sensorhub.android.ui.screens.AppPreferencesScreen +import org.sensorhub.android.ui.theme.OSHTheme + +class AppPreferencesActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + OSHTheme { + AppPreferencesScreen(onBackClick = { finish() }) + } + } + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/activities/AppStatusActivity.kt similarity index 92% rename from sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt rename to sensorhub-android-app/src/org/sensorhub/android/activities/AppStatusActivity.kt index 42ad82de..2275d1c4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/activities/AppStatusActivity.kt @@ -1,4 +1,4 @@ -package org.sensorhub.android +package org.sensorhub.android.activities import android.os.Bundle import androidx.activity.ComponentActivity @@ -17,4 +17,4 @@ class AppStatusActivity : ComponentActivity() { } } } -} +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/activities/ComposeMainActivity.kt similarity index 96% rename from sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt rename to sensorhub-android-app/src/org/sensorhub/android/activities/ComposeMainActivity.kt index 02b9ddf4..bd73c3f2 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/activities/ComposeMainActivity.kt @@ -1,4 +1,4 @@ -package org.sensorhub.android +package org.sensorhub.android.activities import android.os.Bundle import androidx.activity.ComponentActivity diff --git a/sensorhub-android-app/src/org/sensorhub/android/activities/HelpFaqActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/activities/HelpFaqActivity.kt new file mode 100644 index 00000000..4d97dfd7 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/activities/HelpFaqActivity.kt @@ -0,0 +1,40 @@ +package org.sensorhub.android.activities + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import org.sensorhub.android.R +import org.sensorhub.android.ui.screens.FaqItem +import org.sensorhub.android.ui.screens.HelpFaqScreen +import org.sensorhub.android.ui.theme.OSHTheme + +class HelpFaqActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val questions = resources.getStringArray(R.array.faq_questions) + val answers = resources.getStringArray(R.array.faq_answers) + val categories = resources.getStringArray(R.array.faq_categories) + + val items = buildList { + var currentCategory = "" + for (i in questions.indices) { + val category = categories[i] + if (category != currentCategory) { + add(FaqItem.Header(category)) + currentCategory = category + } + add(FaqItem.Entry(questions[i], answers[i])) + } + } + + setContent { + OSHTheme { + HelpFaqScreen( + items = items, + onBackClick = { finish() } + ) + } + } + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 8414aa80..4f91e2e3 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -1,5 +1,12 @@ package org.sensorhub.android.ui.screens +import android.app.Application +import android.app.LocaleManager +import android.content.Intent +import android.content.pm.PackageManager +import android.net.wifi.WifiManager +import android.os.LocaleList +import android.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -10,6 +17,7 @@ import androidx.compose.material.icons.filled.Help import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Send import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -17,13 +25,19 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.sensorhub.android.activities.AppStatusActivity +import org.sensorhub.android.activities.HelpFaqActivity import org.sensorhub.android.R -import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHClickableRowWithIcon @@ -31,30 +45,104 @@ import org.sensorhub.android.ui.components.OSHInfoRow import org.sensorhub.android.ui.components.OSHSingleChoiceDialog import org.sensorhub.android.ui.components.OSHTextInputDialog import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme +import java.math.BigInteger +import java.net.InetAddress +import java.nio.ByteOrder + +private val LANGUAGE_LABELS = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch", "Italiano", "Português") +private val LANGUAGE_VALUES = listOf("en", "zh-TW", "es", "fr", "de", "it", "pt") + +data class AppPreferencesState( + val deviceName: String = "MyDevice", + val deviceIpAddress: String = "", + val appVersion: String = "", + val selectedLanguageIndex: Int = 0 +) + +class AppPreferencesViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + private val _state = MutableStateFlow(AppPreferencesState()) + val state: StateFlow = _state.asStateFlow() + + init { + val deviceName = prefs.getString("device_name", "MyDevice") ?: "MyDevice" + val ipAddress = getDeviceIpAddress() + val version = getAppVersion() + val currentLang = prefs.getString("app_language", "en") ?: "en" + val langIndex = LANGUAGE_VALUES.indexOf(currentLang).coerceAtLeast(0) + + _state.value = AppPreferencesState( + deviceName = deviceName, + deviceIpAddress = ipAddress, + appVersion = version, + selectedLanguageIndex = langIndex + ) + } + + fun updateDeviceName(name: String) { + prefs.edit().putString("device_name", name).apply() + _state.value = _state.value.copy(deviceName = name) + } + + fun selectLanguage(index: Int) { + val localeTag = LANGUAGE_VALUES[index] + prefs.edit().putString("app_language", localeTag).apply() + _state.value = _state.value.copy(selectedLanguageIndex = index) + + val localeManager = getApplication().getSystemService(LocaleManager::class.java) + localeManager.applicationLocales = LocaleList.forLanguageTags(localeTag) + } + + private fun getDeviceIpAddress(): String { + return try { + val wifiManager = getApplication().getSystemService(WifiManager::class.java) + var ipAddress = wifiManager.connectionInfo.ipAddress + + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + ipAddress = Integer.reverseBytes(ipAddress) + } + + val ipByteArray = BigInteger.valueOf(ipAddress.toLong()).toByteArray() + InetAddress.getByAddress(ipByteArray).hostAddress ?: "" + } catch (e: Exception) { + "" + } + } + + private fun getAppVersion(): String { + return try { + val context = getApplication() + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + pInfo.versionName ?: "" + } catch (e: PackageManager.NameNotFoundException) { + "" + } + } +} @Composable fun AppPreferencesScreen( onBackClick: () -> Unit, - navController: NavController = rememberNavController() + viewModel: AppPreferencesViewModel = viewModel() ) { - var deviceName by remember { mutableStateOf("") } - var deviceHost by remember { mutableStateOf("") } - var appVersion by remember { mutableStateOf("") } + val context = LocalContext.current + val state by viewModel.state.collectAsStateWithLifecycle() var showLanguageDialog by remember { mutableStateOf(false) } var showNotificationDialog by remember { mutableStateOf(false) } var showDeviceNameEdit by remember { mutableStateOf(false) } var showAboutAppDialog by remember { mutableStateOf(false) } - if(showLanguageDialog) { + if (showLanguageDialog) { OSHSingleChoiceDialog( onDismissRequest = { showLanguageDialog = false }, dialogTitle = stringResource(R.string.title_language_dialog), icon = Icons.Filled.Language, - options = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch"), - selectedIndex = 0, - onOptionSelected = {} + options = LANGUAGE_LABELS, + selectedIndex = state.selectedLanguageIndex, + onOptionSelected = { index -> viewModel.selectLanguage(index) } ) } @@ -62,7 +150,7 @@ fun AppPreferencesScreen( OSHAlertDialogWithoutDismiss( onConfirmation = { showNotificationDialog = false }, dialogText = "Not yet implemented", - dialogTitle = "Notifications", + dialogTitle = stringResource(R.string.title_notifications), icon = Icons.Filled.Info ) } @@ -71,13 +159,13 @@ fun AppPreferencesScreen( OSHTextInputDialog( onDismissRequest = { showDeviceNameEdit = false }, onConfirmation = { newName -> - deviceName = newName + viewModel.updateDeviceName(newName) showDeviceNameEdit = false }, dialogTitle = stringResource(R.string.device_name), dialogText = stringResource(R.string.device_name), icon = Icons.Filled.Info, - initialValue = deviceName.ifEmpty { "MyDevice" } + initialValue = state.deviceName ) } @@ -97,6 +185,7 @@ fun AppPreferencesScreen( onBackClick = onBackClick ) }, + containerColor = Background ) { innerPadding -> Column( modifier = Modifier @@ -106,16 +195,16 @@ fun AppPreferencesScreen( OSHCard { OSHInfoRow( label = stringResource(R.string.device_name), - value = deviceName.ifEmpty { "MyDevice" }, + value = state.deviceName, onClick = { showDeviceNameEdit = true } ) OSHInfoRow( label = stringResource(R.string.device_ip_address), - value = deviceHost + value = state.deviceIpAddress ) OSHInfoRow( label = stringResource(R.string.title_version), - value = appVersion + value = state.appVersion ) } OSHCard { @@ -144,14 +233,21 @@ fun AppPreferencesScreen( title = stringResource(R.string.app_status_main_fragment), imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.app_status_main_fragment), - onClick = { navController.navigate(Screen.AppStatus.route) } + onClick = { context.startActivity(Intent(context, AppStatusActivity::class.java)) } + ) + + OSHClickableRowWithIcon( + title = stringResource(R.string.send_feedback), + imageVector = Icons.Default.Send, + contentDescription = stringResource(R.string.send_feedback_desc), + onClick = { } ) OSHClickableRowWithIcon( title = stringResource(R.string.title_help_faq), imageVector = Icons.Default.Help, contentDescription = stringResource(R.string.title_help_faq), - onClick = { navController.navigate(Screen.HelpFaq.route)} + onClick = { context.startActivity(Intent(context, HelpFaqActivity::class.java)) } ) } } From c5a0db5afc4e22314efb095c0e74bf4698912548 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 14:00:12 -0500 Subject: [PATCH 52/63] updated server profiles and forms added validation check and save profiles function removed sample test server code added server profiles view model --- .../org/sensorhub/android/ui/OSHNavHost.kt | 30 +- .../src/org/sensorhub/android/ui/Screen.kt | 8 +- .../sensorhub/android/ui/components/Button.kt | 8 +- .../android/ui/screens/ServerFormScreen.kt | 284 +++++++++++------- .../ui/screens/ServerProfilesScreen.kt | 175 +++++++---- .../android/ui/screens/ServersScreen.kt | 77 +++-- 6 files changed, 363 insertions(+), 219 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt index dcd4f5bc..3e0b5948 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/OSHNavHost.kt @@ -4,18 +4,18 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavHostController +import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.navArgument import org.sensorhub.android.ui.screens.AppPreferencesScreen import org.sensorhub.android.ui.screens.AppStatusScreen import org.sensorhub.android.R -import org.sensorhub.android.ui.screens.AppStatusViewModel import org.sensorhub.android.ui.screens.FaqItem import org.sensorhub.android.ui.screens.HelpFaqScreen import org.sensorhub.android.ui.screens.HomeScreen import org.sensorhub.android.ui.screens.SensorsScreen import org.sensorhub.android.ui.screens.ServerFormScreen -import org.sensorhub.android.ui.screens.ServerProfileItem import org.sensorhub.android.ui.screens.ServersScreen import org.sensorhub.android.ui.screens.ServerProfilesScreen @@ -58,30 +58,25 @@ fun OSHNavHost( } composable(Screen.AppPreferences.route) { - val context = LocalContext.current - AppPreferencesScreen( - onBackClick = { navController.popBackStack() }, - navController = navController + onBackClick = { navController.popBackStack() } ) } composable(Screen.ServerProfiles.route) { - val context = LocalContext.current -// val servers = context.resources.getStringArray(R.id.server_profiles_toolbar) - - val serverProfileItems = buildServerProfileItems() ServerProfilesScreen( onBackClick = { navController.popBackStack() }, - items = serverProfileItems, navController = navController ) } - composable(Screen.ServerForm.route) { - val context = LocalContext.current - + composable( + route = Screen.ServerForm.route, + arguments = listOf(navArgument("profileId") { type = NavType.StringType }) + ) { backStackEntry -> + val profileId = backStackEntry.arguments?.getString("profileId") ServerFormScreen( onBackClick = { navController.popBackStack() }, + profileId = profileId ) } } @@ -105,10 +100,3 @@ private fun buildFaqItems( } return items } - -private fun buildServerProfileItems(): List { - val items = mutableListOf() - - return items; -} - diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt index f93a6fcd..a3977ade 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/Screen.kt @@ -8,5 +8,9 @@ sealed class Screen(val route: String) { object AppStatus : Screen("app_status_screen") object HelpFaq : Screen("help_faq_screen") object AppPreferences : Screen("app_prefs_screen") - object ServerForm : Screen("server_form_screen") -} \ No newline at end of file + object ServerForm : Screen("server_form_screen/{profileId}") { + fun createRoute(profileId: String? = null): String { + return "server_form_screen/${profileId ?: "new"}" + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt index a532229b..3a1abe17 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Button.kt @@ -129,10 +129,10 @@ fun OSHTextButton( @Composable fun OSHSegmentedButton( modifier: Modifier = Modifier, - options: List + options: List, + selectedIndex: Int = 0, + onOptionSelected: (Int) -> Unit = {} ) { - var selectedIndex by remember { mutableIntStateOf(0) } - SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { options.forEachIndexed { index, label -> SegmentedButton( @@ -142,7 +142,7 @@ fun OSHSegmentedButton( count = options.size, baseShape = RoundedCornerShape(25) ), - onClick = { selectedIndex = index }, + onClick = { onOptionSelected(index) }, selected = index == selectedIndex, label = { Text(label, maxLines = 1) }, colors = SegmentedButtonDefaults.colors( diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt index acdd0310..56bc1e53 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt @@ -1,5 +1,6 @@ package org.sensorhub.android.ui.screens +import android.app.Application import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -16,9 +17,7 @@ import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -26,6 +25,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -34,6 +34,8 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sensorhub.android.R +import org.sensorhub.android.server.ServerProfile +import org.sensorhub.android.server.ServerProfileRepository import org.sensorhub.android.ui.components.OSHButton import org.sensorhub.android.ui.components.OSHInputField import org.sensorhub.android.ui.components.OSHSegmentedButton @@ -44,67 +46,104 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServerFormScreen( - onBackClick: () -> Unit + onBackClick: () -> Unit, + profileId: String? = null ) { + val context = LocalContext.current + val repo = remember { ServerProfileRepository.getInstance(context) } + val isEdit = profileId != null && profileId != "new" + val existingProfile = remember { if (isEdit) repo.getById(profileId) else null } - var id by rememberSaveable { mutableStateOf("") } - var serverName by rememberSaveable { mutableStateOf("") } - var host by rememberSaveable { mutableStateOf("") } - var port by rememberSaveable { mutableStateOf("") } - var endpointPath by rememberSaveable { mutableStateOf("") } - var username by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } - var enableTls by rememberSaveable { mutableStateOf(false) } - var enableOAuth by rememberSaveable { mutableStateOf(false) } - var disableSslCheck by rememberSaveable { mutableStateOf("") } - var useConSysClient by rememberSaveable { mutableStateOf("") } - var clientId by rememberSaveable { mutableStateOf("") } - var clientSecret by rememberSaveable { mutableStateOf("") } - var tokenEndpoint by rememberSaveable { mutableStateOf("") } - - // validation - var portError by rememberSaveable { mutableStateOf(null) } + var serverName by rememberSaveable { mutableStateOf(existingProfile?.name ?: "") } + var host by rememberSaveable { mutableStateOf(existingProfile?.host ?: "") } + var port by rememberSaveable { mutableStateOf(existingProfile?.port?.toString() ?: "") } + var endpointPath by rememberSaveable { mutableStateOf(existingProfile?.endpointPath ?: "/sensorhub/api") } + var username by rememberSaveable { mutableStateOf(existingProfile?.username ?: "") } + var password by rememberSaveable { + mutableStateOf(if (isEdit && existingProfile != null) repo.getPassword(existingProfile.id) else "") + } + var enableTls by rememberSaveable { mutableStateOf(existingProfile?.enableTls ?: false) } + var disableSslCheck by rememberSaveable { mutableStateOf(existingProfile?.disableSslCheck ?: false) } + var enableOAuth by rememberSaveable { mutableStateOf(existingProfile?.oAuthEnabled ?: false) } + var clientId by rememberSaveable { + mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthClientId(existingProfile.id) else "") + } + var clientSecret by rememberSaveable { + mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthClientSecret(existingProfile.id) else "") + } + var tokenEndpoint by rememberSaveable { + mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthTokenEndpoint(existingProfile.id) else "") + } + var clientTypeIndex by rememberSaveable { + mutableStateOf(if (existingProfile?.useConSysClient == false) 1 else 0) + } + + var nameError by rememberSaveable { mutableStateOf(null) } + var hostError by rememberSaveable { mutableStateOf(null) } + var portError by rememberSaveable { mutableStateOf(null) } var isPasswordVisible by remember { mutableStateOf(false) } var isClientSecretVisible by remember { mutableStateOf(false) } + val msgRequired = stringResource(R.string.msg_name_host_port_required) + val msgNoProtocol = stringResource(R.string.msg_no_protocol) + val msgPortNumber = stringResource(R.string.msg_port_number) + val msgPortRange = stringResource(R.string.msg_port_range) - if (enableOAuth) { - OSHInputField( - value = clientId, - onValueChange = { clientId = it }, - label = "Client ID", - ) - OSHInputField( - value = tokenEndpoint, - onValueChange = { tokenEndpoint = it }, - label = "Token Endpoint", - ) - OSHInputField( - value = clientSecret, - onValueChange = { clientSecret = it }, - label = "ClientSecret", - visualTransformation = if (isClientSecretVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { isClientSecretVisible = !isClientSecretVisible }) { - Icon( - imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = "Toggle secret visibility" - ) - } - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ) - ) + fun validate(): Boolean { + var valid = true + nameError = if (serverName.isBlank()) { valid = false; msgRequired } else null + hostError = if (host.isBlank()) { + valid = false; msgRequired + } else if (host.contains(" ") || host.contains("://")) { + valid = false; msgNoProtocol + } else null + portError = if (port.isBlank()) { + valid = false; msgRequired + } else { + val portNum = port.toIntOrNull() + if (portNum == null) { valid = false; msgPortNumber } + else if (portNum < 1 || portNum > 65535) { valid = false; msgPortRange } + else null + } + return valid + } + + fun saveProfile() { + if (!validate()) return + + val profile = existingProfile ?: ServerProfile() + profile.name = serverName.trim() + profile.host = host.trim() + profile.port = port.toInt() + var ep = endpointPath.trim() + if (ep.isNotEmpty() && !ep.startsWith("/")) ep = "/$ep" + profile.endpointPath = ep + profile.useConSysClient = clientTypeIndex == 0 + profile.enableTls = enableTls + profile.disableSslCheck = disableSslCheck + profile.oAuthEnabled = enableOAuth + profile.username = if (!enableOAuth) username.trim() else "" + + repo.save(profile) + + val pwd = if (!enableOAuth) password.trim() else "" + repo.setPassword(profile.id, pwd) + + if (enableOAuth) { + repo.setOAuthClientId(profile.id, clientId.trim()) + repo.setOAuthClientSecret(profile.id, clientSecret.trim()) + repo.setOAuthTokenEndpoint(profile.id, tokenEndpoint.trim()) + } + + onBackClick() } Scaffold( topBar = { OSHTopAppBarWithBack( - title = stringResource(R.string.add_server), + title = stringResource(if (isEdit) R.string.title_edit_server else R.string.add_server), onBackClick = onBackClick ) }, @@ -121,13 +160,19 @@ fun ServerFormScreen( Spacer(modifier = Modifier.height(16.dp)) OSHSegmentedButton( - options = listOf("CS API Client", "SOS-T Client") + options = listOf("CS API Client", "SOS-T Client"), + selectedIndex = clientTypeIndex, + onOptionSelected = { index -> + clientTypeIndex = index + endpointPath = if (index == 0) "/sensorhub/api" else "/sensorhub/sos" + } ) OSHInputField( value = serverName, - onValueChange = { serverName = it }, + onValueChange = { serverName = it; nameError = null }, label = "Server name", + error = nameError ) Row( modifier = Modifier.fillMaxWidth(), @@ -135,16 +180,18 @@ fun ServerFormScreen( ) { OSHInputField( value = host, - onValueChange = { host = it }, + onValueChange = { host = it; hostError = null }, label = "Host / IP", modifier = Modifier.weight(1f), + error = hostError ) OSHInputField( value = port, - onValueChange = { port = it }, + onValueChange = { port = it; portError = null }, label = "Port", modifier = Modifier.weight(0.4f), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + error = portError ) } OSHInputField( @@ -155,33 +202,19 @@ fun ServerFormScreen( OSHSwitchRow( title = "Enable TLS", checked = enableTls, - onCheckedChange = { enableTls = it }, - ) - - OSHInputField( - value = username, - onValueChange = { username = it }, - label = "Username", - ) - OSHInputField( - value = password, - onValueChange = { password = it }, - label = "Password", - visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { - Icon( - imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, - contentDescription = "Toggle password visibility" - ) - } + onCheckedChange = { + enableTls = it + if (!it) disableSslCheck = false }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ) ) + if (enableTls) { + OSHSwitchRow( + title = "Disable SSL Check", + checked = disableSslCheck, + onCheckedChange = { disableSslCheck = it }, + ) + } OSHSwitchRow( title = "Enable OAuth", @@ -189,49 +222,76 @@ fun ServerFormScreen( onCheckedChange = { enableOAuth = it }, ) + if (enableOAuth) { + OSHInputField( + value = tokenEndpoint, + onValueChange = { tokenEndpoint = it }, + label = "Token Endpoint", + ) + OSHInputField( + value = clientId, + onValueChange = { clientId = it }, + label = "Client ID", + ) + OSHInputField( + value = clientSecret, + onValueChange = { clientSecret = it }, + label = "Client Secret", + visualTransformation = if (isClientSecretVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { isClientSecretVisible = !isClientSecretVisible }) { + Icon( + imageVector = if (isClientSecretVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle secret visibility" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + } else { + OSHInputField( + value = username, + onValueChange = { username = it }, + label = "Username", + ) + OSHInputField( + value = password, + onValueChange = { password = it }, + label = "Password", + visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { + Icon( + imageVector = if (isPasswordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff, + contentDescription = "Toggle password visibility" + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + OSHButton( - onClick = {}, - text = "Add Server", - modifier = Modifier - .fillMaxSize() + onClick = { saveProfile() }, + text = stringResource(if (isEdit) R.string.title_edit_server else R.string.btn_add_server), + modifier = Modifier.fillMaxWidth() ) } } } -//fun toggleServerClient() { -// if(client == "api") { -// endpoint = "/sensorhub/api" -// } else { -// endpoint = "/sensorhub/sos" -// } -//} - -data class ServerProfile( - val id: String = "", - val name: String = "", - val host: String = "", - val port: Int = 8181, - val endpointPath: String = "/sensorhub/api", - val username: String = "", - val password: String = "", - val enableTls: Boolean = false, - val disableSslCheck: Boolean = false, - val useConSysClient: Boolean = false, - val oAuthEnabled: Boolean = false, - val enabled: Boolean = false, - val clientId: String = "", - val clientSecret: String = "", - val tokenEndpoint: String = "" -) { - val isValid: Boolean get() = name.isNotBlank() && host.isNotBlank() && ( port >= 0 && port <= 65513) -} - - @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable -private fun ServerProfilesScreenPreview() { +private fun ServerFormScreenPreview() { OSHTheme { ServerFormScreen( onBackClick = {}, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt index ce83abf4..3b6ac04f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -1,36 +1,115 @@ package org.sensorhub.android.ui.screens +import android.app.Application +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.sensorhub.android.R +import org.sensorhub.android.server.ServerProfileRepository import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHActionCard import org.sensorhub.android.ui.components.OSHAddFAB +import org.sensorhub.android.ui.components.OSHAlertDialog import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme +data class ServerProfileItem( + val id: String = "", + val name: String = "", + val host: String = "", + val port: Int = 8181, + val endpointPath: String = "/sensorhub/api", + val username: String = "", + val password: String = "", + val enableTls: Boolean = false, + val disableSslCheck: Boolean = false, + val useConSysClient: Boolean = false, + val oAuthEnabled: Boolean = false, + val enabled: Boolean = false, + val clientId: String = "", + val clientSecret: String = "", + val tokenEndpoint: String = "" +) + +class ServerProfilesViewModel(application: Application) : AndroidViewModel(application) { + private val repo = ServerProfileRepository.getInstance(application) + private val _profiles = MutableStateFlow>(emptyList()) + val profiles: StateFlow> = _profiles.asStateFlow() + + init { + refresh() + } + + fun refresh() { + _profiles.value = repo.all.map { p -> + ServerProfileItem( + id = p.id, + name = p.name, + host = p.host, + port = p.port, + endpointPath = p.endpointPath, + username = p.username, + enableTls = p.enableTls, + disableSslCheck = p.disableSslCheck, + useConSysClient = p.useConSysClient, + oAuthEnabled = p.oAuthEnabled, + enabled = p.enabled + ) + } + } + + fun setEnabled(id: String, enabled: Boolean) { + repo.setEnabled(id, enabled) + refresh() + } + + fun delete(id: String) { + repo.delete(id) + refresh() + } +} + @Composable fun ServerProfilesScreen( - items: List, onBackClick: () -> Unit, - navController: NavController = rememberNavController() + navController: NavController = rememberNavController(), + viewModel: ServerProfilesViewModel = viewModel() ) { + val profiles by viewModel.profiles.collectAsStateWithLifecycle() + var profileToDelete by remember { mutableStateOf(null) } Scaffold( topBar = { OSHTopAppBarWithBack( @@ -40,81 +119,53 @@ fun ServerProfilesScreen( }, floatingActionButton = { OSHAddFAB( - onClick = { navController.navigate(Screen.ServerForm.route)} + onClick = { navController.navigate(Screen.ServerForm.createRoute()) } ) }, containerColor = Background ) { padding -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(padding), - contentPadding = PaddingValues(16.dp) - ) { - items(items) { item -> - var enabled by remember { mutableStateOf(item.enabled) } - - OSHActionCard( - title = item.name, - subtitle = "${item.host}:${item.port}${item.endpointPath}", - checked = enabled, - onCheckedChange = { enabled = it }, - onClick = { navController.navigate(Screen.ServerForm.route)}, - imageVector = Icons.Filled.Edit, - contentDescription = "Edit ${item.name}" + if (profiles.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.empty_server_profiles), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding), + contentPadding = PaddingValues(16.dp) + ) { + items(profiles, key = { it.id }) { item -> + OSHActionCard( + title = item.name, + subtitle = "${if (item.enableTls) "https" else "http"}://${item.host}:${item.port}${item.endpointPath}", + checked = item.enabled, + onCheckedChange = { enabled -> viewModel.setEnabled(item.id, enabled) }, + onClick = { navController.navigate(Screen.ServerForm.createRoute(item.id)) }, + imageVector = Icons.Filled.Edit, + contentDescription = "Edit ${item.name}" + ) + } + } } } } -data class ServerProfileItem( - val id: String = "", - val name: String = "", - val host: String = "", - val port: Int = 8181, - val endpointPath: String = "/sensorhub/api", - val username: String = "", - val password: String = "", - val enableTls: Boolean = false, - val disableSslCheck: Boolean = false, - val useConSysClient: Boolean = false, - val oAuthEnabled: Boolean = false, - val enabled: Boolean = false, - val clientId: String = "", - val clientSecret: String = "", - val tokenEndpoint: String = "" -) - -private fun buildSampleServerProfilesItems(): List = listOf( - ServerProfileItem( - id = "1", - name = "Local Server", - host = "127.0.0.1", - port = 8181, - endpointPath = "/sensorhub/api", - useConSysClient = true, - enabled = true - ), - ServerProfileItem( - id = "2", - name = "Cloud Server", - host = "osh.example.com", - port = 443, - endpointPath = "/sensorhub/api", - enableTls = true, - useConSysClient = true, - enabled = false - ) -) - @Preview(showBackground = true, backgroundColor = 0xFF121212) @Composable private fun ServerProfilesScreenPreview() { OSHTheme { ServerProfilesScreen( - onBackClick = {}, - items = buildSampleServerProfilesItems() + onBackClick = {} ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 7846c179..69d8bd09 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -1,5 +1,7 @@ package org.sensorhub.android.ui.screens +import android.app.Application +import android.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -12,15 +14,18 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import org.sensorhub.android.R import org.sensorhub.android.ui.Screen import org.sensorhub.android.ui.components.OSHCard @@ -29,25 +34,62 @@ import org.sensorhub.android.ui.components.OSHSwitchRow import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme +data class ServersState( + val sosEnabled: Boolean = true, + val csApiEnabled: Boolean = true, + val discoveryEnabled: Boolean = false +) + +class ServersViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + private val _state = MutableStateFlow(ServersState()) + val state: StateFlow = _state.asStateFlow() + + init { + _state.value = ServersState( + sosEnabled = prefs.getBoolean("sos_service", true), + csApiEnabled = prefs.getBoolean("csapi_service", true), + discoveryEnabled = prefs.getBoolean("discovery_service", false) + ) + } + + fun setSosEnabled(enabled: Boolean) { + prefs.edit().putBoolean("sos_service", enabled).apply() + _state.value = _state.value.copy(sosEnabled = enabled) + } + + fun setCsApiEnabled(enabled: Boolean) { + prefs.edit().putBoolean("csapi_service", enabled).apply() + _state.value = _state.value.copy(csApiEnabled = enabled) + } + + fun setDiscoveryEnabled(enabled: Boolean) { + prefs.edit().putBoolean("discovery_service", enabled).apply() + _state.value = _state.value.copy(discoveryEnabled = enabled) + } +} + @Composable -fun ServersScreen(navController: NavController = rememberNavController()) { - var sosEnabled by remember { mutableStateOf(false) } - var csApiEnabled by remember { mutableStateOf(false) } - var discoveryEnabled by remember { mutableStateOf(false) } +fun ServersScreen( + navController: NavController = rememberNavController(), + viewModel: ServersViewModel = viewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() Scaffold( topBar = { OSHTopAppBarWithLogo( title = stringResource(R.string.tab_servers), actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" ) } }, - ) }, + ) + }, ) { innerPadding -> Column( modifier = Modifier @@ -56,32 +98,31 @@ fun ServersScreen(navController: NavController = rememberNavController()) { Spacer(modifier = Modifier.height(16.dp)) - OSHClickableCardWithIcon ( + OSHClickableCardWithIcon( title = stringResource(R.string.manage_servers), -// subtitle = "0 of 0 server(s) enabled", imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.manage_servers), - onClick = { navController.navigate(Screen.ServerProfiles.route)}, + onClick = { navController.navigate(Screen.ServerProfiles.route) }, ) OSHCard { OSHSwitchRow( title = stringResource(R.string.enable_sos_service), subtitle = stringResource(R.string.summary_sos), - checked = sosEnabled, - onCheckedChange = { sosEnabled = it } + checked = state.sosEnabled, + onCheckedChange = { viewModel.setSosEnabled(it) } ) OSHSwitchRow( title = stringResource(R.string.enable_csapi_service), subtitle = stringResource(R.string.summary_csapi), - checked = csApiEnabled, - onCheckedChange = { csApiEnabled = it } + checked = state.csApiEnabled, + onCheckedChange = { viewModel.setCsApiEnabled(it) } ) OSHSwitchRow( title = stringResource(R.string.enable_discovery_service), subtitle = stringResource(R.string.summary_discovery), - checked = discoveryEnabled, - onCheckedChange = { discoveryEnabled = it } + checked = state.discoveryEnabled, + onCheckedChange = { viewModel.setDiscoveryEnabled(it) } ) } } From 0830ba5f22b982752f5de3db6f8944e7ef322c7a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 15:11:37 -0500 Subject: [PATCH 53/63] added bluetooth scanner and dialog to delete server profile --- .../sensorhub/android/ui/BluetoothScanner.kt | 53 +++++++++++++++++++ .../ui/screens/ServerProfilesScreen.kt | 25 +++++++++ 2 files changed, 78 insertions(+) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt new file mode 100644 index 00000000..ce9aabcf --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt @@ -0,0 +1,53 @@ +package org.sensorhub.android.ui + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.compose.runtime.mutableStateListOf + +class BluetoothScanner(private val context: Context) { + private val btManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + private val btAdapter: BluetoothAdapter? = btManager.adapter + + val foundDevices = mutableStateListOf() + + private val receiver = object : BroadcastReceiver() { + @SuppressLint("MissingPermission") + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == BluetoothDevice.ACTION_FOUND) { // + val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) // + if (device != null && !foundDevices.contains(device)) { + foundDevices.add(device) + } + } + } + } + + @SuppressLint("MissingPermission") + fun startDiscovery() { + foundDevices.clear() + + // Include already paired devices first if desired + btAdapter?.bondedDevices?.let { foundDevices.addAll(it) } // + + val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) // + context.registerReceiver(receiver, filter) + btAdapter?.startDiscovery() // + } + + @SuppressLint("MissingPermission") + fun stopDiscovery() { + try { + context.unregisterReceiver(receiver) + } catch (e: IllegalArgumentException) { + // Receiver wasn't registered + } + btAdapter?.cancelDiscovery() + } + +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt index 3b6ac04f..52f6daae 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt @@ -110,6 +110,31 @@ fun ServerProfilesScreen( ) { val profiles by viewModel.profiles.collectAsStateWithLifecycle() var profileToDelete by remember { mutableStateOf(null) } + + val lifecycleOwner = LocalLifecycleOwner.current + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_RESUME) { + viewModel.refresh() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } + } + + profileToDelete?.let { profile -> + OSHAlertDialog( + onDismissRequest = { profileToDelete = null }, + onConfirmation = { + viewModel.delete(profile.id) + profileToDelete = null + }, + dialogTitle = stringResource(R.string.title_delete_server), + dialogText = stringResource(R.string.msg_delete_server, profile.name), + icon = Icons.Filled.Info + ) + } + Scaffold( topBar = { OSHTopAppBarWithBack( From cd22b23ec9d8f94c2deb9d2a6eda906567969523 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 16:08:01 -0500 Subject: [PATCH 54/63] update to use androidx PreferenceManager --- .../org/sensorhub/android/ui/screens/AppPreferencesScreen.kt | 2 +- .../src/org/sensorhub/android/ui/screens/ServersScreen.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt index 4f91e2e3..7cc5bc61 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.content.pm.PackageManager import android.net.wifi.WifiManager import android.os.LocaleList -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt index 69d8bd09..ecc3e70f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt @@ -1,7 +1,7 @@ package org.sensorhub.android.ui.screens import android.app.Application -import android.preference.PreferenceManager +import androidx.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height From 015335bb36d2d047d3077168efc8ecc29a1bc0e7 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 16:36:18 -0500 Subject: [PATCH 55/63] refactor ui arch --- sensorhub-android-app/AndroidManifest.xml | 8 +- .../{activities => }/ComposeMainActivity.kt | 6 +- .../sensorhub/android/SettingsFragment.java | 2 +- .../AppPreferencesActivity.kt | 4 +- .../{activities => ui}/AppStatusActivity.kt | 4 +- .../{activities => ui}/HelpFaqActivity.kt | 6 +- .../android/ui/{ => navigation}/Navbar.kt | 6 +- .../android/ui/{ => navigation}/OSHNavHost.kt | 20 +- .../android/ui/{ => navigation}/Screen.kt | 2 +- .../android/ui/screens/AppStatusScreen.kt | 205 --------- .../android/ui/screens/SensorsScreen.kt | 352 --------------- .../ui/screens/appstatus/AppStatusContract.kt | 10 + .../ui/screens/appstatus/AppStatusScreen.kt | 98 +++++ .../screens/appstatus/AppStatusViewModel.kt | 100 +++++ .../ui/screens/{ => dashboard}/HomeScreen.kt | 4 +- .../ui/screens/help/HelpFaqContract.kt | 6 + .../ui/screens/{ => help}/HelpFaqScreen.kt | 7 +- .../preferences/AppPreferencesContract.kt | 11 + .../{ => preferences}/AppPreferencesScreen.kt | 90 +--- .../preferences/AppPreferencesViewModel.kt | 76 ++++ .../{ => profiles}/ServerFormScreen.kt | 3 +- .../screens/profiles/ServerProfileContract.kt | 19 + .../{ => profiles}/ServerProfilesScreen.kt | 67 +-- .../profiles/ServerProfilesViewModel.kt | 46 ++ .../{ => screens/sensors}/BluetoothScanner.kt | 15 +- .../ui/screens/sensors/SensorsContract.kt | 278 ++++++++++++ .../ui/screens/sensors/SensorsScreen.kt | 402 ++++++++++++++++++ .../ui/screens/sensors/SensorsViewModel.kt | 40 ++ .../ui/screens/servers/ServersContract.kt | 7 + .../ui/screens/{ => servers}/ServersScreen.kt | 45 +- .../ui/screens/servers/ServersViewModel.kt | 37 ++ 31 files changed, 1178 insertions(+), 798 deletions(-) rename sensorhub-android-app/src/org/sensorhub/android/{activities => }/ComposeMainActivity.kt (87%) rename sensorhub-android-app/src/org/sensorhub/android/{activities => ui}/AppPreferencesActivity.kt (81%) rename sensorhub-android-app/src/org/sensorhub/android/{activities => ui}/AppStatusActivity.kt (82%) rename sensorhub-android-app/src/org/sensorhub/android/{activities => ui}/HelpFaqActivity.kt (88%) rename sensorhub-android-app/src/org/sensorhub/android/ui/{ => navigation}/Navbar.kt (95%) rename sensorhub-android-app/src/org/sensorhub/android/ui/{ => navigation}/OSHNavHost.kt (82%) rename sensorhub-android-app/src/org/sensorhub/android/ui/{ => navigation}/Screen.kt (93%) delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppStatusScreen.kt delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusContract.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusScreen.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => dashboard}/HomeScreen.kt (93%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqContract.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => help}/HelpFaqScreen.kt (94%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesContract.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => preferences}/AppPreferencesScreen.kt (67%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesViewModel.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => profiles}/ServerFormScreen.kt (99%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => profiles}/ServerProfilesScreen.kt (70%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/{ => screens/sensors}/BluetoothScanner.kt (86%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsContract.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsViewModel.kt create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersContract.kt rename sensorhub-android-app/src/org/sensorhub/android/ui/screens/{ => servers}/ServersScreen.kt (69%) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersViewModel.kt diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index bd7049af..11281d5d 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -57,7 +57,7 @@ @@ -73,17 +73,17 @@ android:windowSoftInputMode="adjustResize" android:exported="false" /> = _state.asStateFlow() - private var boundService: SensorHubService? = null - - private val sConn = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - boundService = (service as SensorHubService.LocalBinder).service - refreshStatus() - } - - override fun onServiceDisconnected(name: ComponentName?) { - boundService = null - } - } - - init { - val context = getApplication() - context.bindService( - Intent(context, SensorHubService::class.java), - sConn, - Context.BIND_AUTO_CREATE - ) - viewModelScope.launch { - while (isActive) { - delay(3000) - refreshStatus() - } - } - } - - private fun refreshStatus() { - val service = boundService ?: return - val sensorhub = service.sensorHub ?: return - val modules = sensorhub.moduleRegistry.loadedModules - - var http = "Unknown" - var sos = "Unknown" - var conSys = "Unknown" - var discovery = "Unknown" - var sensor = "Unknown" - var storage = "Unknown" - - for (module in modules) { - val moduleConf = module.configuration - if (moduleConf is ModuleConfig) { - val status = module.currentState.name - val moduleId = moduleConf.id - - when (moduleId) { - "HTTP_SERVER_0" -> http = status - "SOS_SERVICE" -> sos = status - "CON_SYS_SERVICE" -> conSys = status - "DISCOVERY_SERVICE" -> discovery = status - "ANDROID_SENSORS" -> sensor = status - "ANDROID_SENSORS#storage" -> storage = status - } - } - } - - _state.value = AppStatusState( - httpStatus = http, - sosStatus = sos, - conSysStatus = conSys, - discoveryStatus = discovery, - sensorStatus = sensor, - storageStatus = storage - ) - - } - - override fun onCleared() { - super.onCleared() - try { - getApplication().unbindService(sConn) - } catch (_: IllegalArgumentException) { - - } - boundService = null - } -} -@Composable -fun AppStatusScreen( - onBackClick: () -> Unit, - viewModel: AppStatusViewModel = viewModel() -) { - val state by viewModel.state.collectAsStateWithLifecycle() - - Scaffold( - topBar = { - OSHTopAppBarWithBack( - title = stringResource(R.string.app_status_main_fragment), - onBackClick = onBackClick - ) - }, - containerColor = Background - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding), - ) { - - OSHCard { - OSHStatusRow( - title = stringResource(R.string.httpServerStatusLabel), - subtitle = state.httpStatus - ) - HorizontalDivider( - - ) - OSHStatusRow( - title = stringResource(R.string.sosServiceStatusLabel), - subtitle = state.sosStatus - ) - HorizontalDivider( - - ) - OSHStatusRow( - title = stringResource(R.string.conSysServiceStatusLabel), - subtitle = state.conSysStatus - ) - HorizontalDivider( - - ) - OSHStatusRow( - title = stringResource(R.string.discoveryServiceStatusLabel), - subtitle = state.discoveryStatus - ) - HorizontalDivider( - - ) - OSHStatusRow( - title = stringResource(R.string.sensorServiceStatusLabel), - subtitle = state.sensorStatus - ) - HorizontalDivider( - - ) - OSHStatusRow( - title = stringResource(R.string.storageServiceStatusLabel), - subtitle = state.storageStatus - ) - } - } - } -} - - -@Preview(showBackground = true, backgroundColor = 0xFF121212) -@Composable -private fun AppStatusScreenPreview() { - OSHTheme { - AppStatusScreen( - onBackClick = {} - ) - } -} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt deleted file mode 100644 index 884c2728..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/SensorsScreen.kt +++ /dev/null @@ -1,352 +0,0 @@ -package org.sensorhub.android.ui.screens - -import androidx.annotation.StringRes -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Bluetooth -import androidx.compose.material.icons.filled.DevicesOther -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import org.sensorhub.android.R -import org.sensorhub.android.ui.Screen -import org.sensorhub.android.ui.components.OSHClickableRowWithIcon -import org.sensorhub.android.ui.components.OSHExpandableSwitchCard -import org.sensorhub.android.ui.components.OSHFilterChip -import org.sensorhub.android.ui.components.OSHSensorCard -import org.sensorhub.android.ui.components.OSHSwitchRow -import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo -import org.sensorhub.android.ui.theme.OSHTheme - -enum class SensorCategory(@StringRes val labelRes: Int) { - ON_DEVICE(R.string.category_on_device), - BLUETOOTH(R.string.category_bluetooth), - USB(R.string.category_usb), - OTHERS(R.string.category_others), -} - -sealed class SensorConfig { - data class BluetoothDevice(@StringRes val selectLabelRes: Int) : SensorConfig() - - data object Video : SensorConfig() - - data object Audio : SensorConfig() - - data object TruPulse : SensorConfig() -} - -data class SensorItem( - val id: String, - @StringRes val nameRes: Int, - @StringRes val summaryRes: Int, - val category: SensorCategory, - val enabled: Boolean = false, - val config: SensorConfig? = null, -) - -val ALL_SENSORS: List = listOf( - // On device sensors - SensorItem("accelerometer", R.string.sensor_accelerometer, R.string.summary_accel, SensorCategory.ON_DEVICE), - SensorItem("gyroscope", R.string.sensor_gyroscope, R.string.summary_gyro, SensorCategory.ON_DEVICE), - SensorItem("magnetometer", R.string.sensor_magnetometer, R.string.summary_mag, SensorCategory.ON_DEVICE), - SensorItem("orientation_q", R.string.sensor_orientation_quat, R.string.summary_orientation, SensorCategory.ON_DEVICE), - SensorItem("orientation_e", R.string.sensor_orientation_euler, R.string.summary_orientation, SensorCategory.ON_DEVICE), - SensorItem("gps", R.string.sensor_gps, R.string.summary_gps, SensorCategory.ON_DEVICE), - SensorItem("network_loc", R.string.sensor_network_location, R.string.summary_netloc, SensorCategory.ON_DEVICE), - - // On device sensors (have additional config) - SensorItem("video", R.string.sensor_video, R.string.summary_video, SensorCategory.ON_DEVICE, config = SensorConfig.Video), - SensorItem("video_roll", R.string.sensor_video_roll, R.string.summary_video_roll, SensorCategory.ON_DEVICE), - SensorItem("audio", R.string.sensor_audio, R.string.summary_audio, SensorCategory.ON_DEVICE, config = SensorConfig.Audio), - - // Bluetooth sensors (have additional config) - SensorItem("meshtastic", R.string.sensor_meshtastic, R.string.summary_meshtastic, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_meshtastic)), - SensorItem("polar", R.string.sensor_polar, R.string.summary_polar, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_polar)), - SensorItem("kestrel", R.string.sensor_kestrel, R.string.summary_kestrel, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_kestrel)), - SensorItem("garmin", R.string.sensor_garmin, R.string.summary_garmin, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_garmin)), - SensorItem("ste", R.string.sensor_ste, R.string.summary_ste, SensorCategory.BLUETOOTH, config = SensorConfig.BluetoothDevice(R.string.title_select_device)), - SensorItem("trupulse", R.string.sensor_trupulse, R.string.summary_trupulse, SensorCategory.BLUETOOTH, config = SensorConfig.TruPulse), - - // USB sensors - SensorItem("controller", R.string.sensor_controller, R.string.summary_controller, SensorCategory.USB), - SensorItem("flirone", R.string.sensor_flirone, R.string.summary_flirone, SensorCategory.USB), - - // Others - SensorItem("wardriving", R.string.sensor_wardriving, R.string.summary_wardriving, SensorCategory.OTHERS), -) - -@Composable -fun SensorsScreen( - items: List = ALL_SENSORS, - onSensorToggled: (SensorItem, Boolean) -> Unit = { _, _ -> }, - onConfigAction: (SensorItem) -> Unit = {}, - navController: NavController = rememberNavController() -) { - var selectedCategories by remember { - mutableStateOf(SensorCategory.entries.toSet()) - } - var sensorStates by remember { - mutableStateOf(items.associate { it.id to it.enabled }) - } - - val scrollState = rememberScrollState() - - - Scaffold( - topBar = { - OSHTopAppBarWithLogo( - title = stringResource(R.string.tab_sensors), - actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = "" - ) - } - }, - ) - }, - ) { padding -> - val resolvedItems = items.map { it.copy(enabled = sensorStates[it.id] ?: it.enabled) } - val filteredItems = if (selectedCategories.isEmpty()) resolvedItems - else resolvedItems.filter { it.category in selectedCategories } - val grouped = filteredItems.groupBy { it.category } - - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding) - ) { - Row( - modifier = Modifier - .horizontalScroll(scrollState) - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - OSHFilterChip( - onClick = { - selectedCategories = if (SensorCategory.ON_DEVICE in selectedCategories) - selectedCategories - SensorCategory.ON_DEVICE - else - selectedCategories + SensorCategory.ON_DEVICE - }, - text = stringResource(R.string.category_on_device), - selected = SensorCategory.ON_DEVICE in selectedCategories, - ) - OSHFilterChip( - onClick = { - selectedCategories = if (SensorCategory.BLUETOOTH in selectedCategories) - selectedCategories - SensorCategory.BLUETOOTH - else - selectedCategories + SensorCategory.BLUETOOTH - }, - text = stringResource(R.string.category_bluetooth), - selected = SensorCategory.BLUETOOTH in selectedCategories, - ) - OSHFilterChip( - onClick = { - selectedCategories = if (SensorCategory.USB in selectedCategories) - selectedCategories - SensorCategory.USB - else - selectedCategories + SensorCategory.USB - }, - text = stringResource(R.string.category_usb), - selected = SensorCategory.USB in selectedCategories, - ) - OSHFilterChip( - onClick = { - selectedCategories = if (SensorCategory.OTHERS in selectedCategories) - selectedCategories - SensorCategory.OTHERS - else - selectedCategories + SensorCategory.OTHERS - }, - text = stringResource(R.string.category_others), - selected = SensorCategory.OTHERS in selectedCategories, - ) - } - - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) - ) { - grouped.forEach { (category, sensors) -> - item { - Text( - text = stringResource(category.labelRes), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(top = 12.dp, bottom = 4.dp) - ) - } - items(sensors, key = { it.id }) { sensor -> - val toggleSensor = { checked: Boolean -> - sensorStates = sensorStates + (sensor.id to checked) - onSensorToggled(sensor, checked) - } - if (sensor.config != null) { - SensorExpandableCard( - sensor = sensor, - onCheckedChange = toggleSensor, - onConfigAction = { onConfigAction(sensor) } - ) - } else { - OSHSensorCard { - OSHSwitchRow( - title = stringResource(sensor.nameRes), - subtitle = stringResource(sensor.summaryRes), - checked = sensor.enabled, - onCheckedChange = toggleSensor - ) - } - } - } - } - } - } - } -} - -@Composable -private fun SensorExpandableCard( - sensor: SensorItem, - onCheckedChange: (Boolean) -> Unit, - onConfigAction: () -> Unit -) { - OSHExpandableSwitchCard( - title = stringResource(sensor.nameRes), - subtitle = stringResource(sensor.summaryRes), - checked = sensor.enabled, - onCheckedChange = onCheckedChange, - configHint = stringResource(R.string.hint_tap_to_configure), - expandedContent = { - HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) - when (sensor.config) { - is SensorConfig.BluetoothDevice -> { - BluetoothDeviceConfig( - selectLabelRes = sensor.config.selectLabelRes, - onSelect = onConfigAction - ) - } - is SensorConfig.Video -> VideoConfig(onConfigAction) - is SensorConfig.Audio -> AudioConfig(onConfigAction) - is SensorConfig.TruPulse -> TruPulseConfig(onConfigAction) - null -> {} - } - } - ) -} - -@Composable -private fun BluetoothDeviceConfig( - @StringRes selectLabelRes: Int, - onSelect: () -> Unit -) { - OSHClickableRowWithIcon( - title = stringResource(selectLabelRes), - subtitle = stringResource(R.string.summary_select_device), - imageVector = Icons.Default.Bluetooth, - contentDescription = stringResource(selectLabelRes), - onClick = onSelect - ) -} - -@Composable -private fun VideoConfig(onConfigAction: () -> Unit) { - OSHClickableRowWithIcon( - title = stringResource(R.string.title_codec), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_codec), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_resolution), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_resolution), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_frame_rate), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_frame_rate), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_camera), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_camera), - onClick = onConfigAction - ) -} - -@Composable -private fun AudioConfig(onConfigAction: () -> Unit) { - OSHClickableRowWithIcon( - title = stringResource(R.string.title_codec), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_codec), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_sample_rate), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_sample_rate), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_bitrate), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_bitrate), - onClick = onConfigAction - ) -} - -@Composable -private fun TruPulseConfig(onConfigAction: () -> Unit) { - OSHClickableRowWithIcon( - title = stringResource(R.string.title_trupulse_datasource), - imageVector = Icons.Default.DevicesOther, - contentDescription = stringResource(R.string.title_trupulse_datasource), - onClick = onConfigAction - ) - OSHClickableRowWithIcon( - title = stringResource(R.string.title_select_trupulse), - subtitle = stringResource(R.string.summary_select_device), - imageVector = Icons.Default.Bluetooth, - contentDescription = stringResource(R.string.title_select_trupulse), - onClick = onConfigAction - ) -} - -@Preview(showBackground = true, backgroundColor = 0xFF121212) -@Composable -private fun SensorsScreenPreview() { - OSHTheme { - SensorsScreen() - } -} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusContract.kt new file mode 100644 index 00000000..e2927f06 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusContract.kt @@ -0,0 +1,10 @@ +package org.sensorhub.android.ui.screens.appstatus + +data class AppStatusState ( + val httpStatus: String = "Unknown", + val sosStatus: String = "Unknown", + val conSysStatus: String = "Unknown", + val discoveryStatus: String = "Unknown", + val sensorStatus: String = "Unknown", + val storageStatus: String = "Unknown", +) \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusScreen.kt new file mode 100644 index 00000000..389af937 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusScreen.kt @@ -0,0 +1,98 @@ +package org.sensorhub.android.ui.screens.appstatus + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import org.sensorhub.android.R +import org.sensorhub.android.ui.components.OSHCard +import org.sensorhub.android.ui.components.OSHStatusRow +import org.sensorhub.android.ui.components.OSHTopAppBarWithBack +import org.sensorhub.android.ui.theme.Background +import org.sensorhub.android.ui.theme.OSHTheme + +@Composable +fun AppStatusScreen( + onBackClick: () -> Unit, + viewModel: AppStatusViewModel = viewModel() +) { + val state by viewModel.state.collectAsStateWithLifecycle() + + Scaffold( + topBar = { + OSHTopAppBarWithBack( + title = stringResource(R.string.app_status_main_fragment), + onBackClick = onBackClick + ) + }, + containerColor = Background + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding), + ) { + + OSHCard { + OSHStatusRow( + title = stringResource(R.string.httpServerStatusLabel), + subtitle = state.httpStatus + ) + HorizontalDivider( + + ) + OSHStatusRow( + title = stringResource(R.string.sosServiceStatusLabel), + subtitle = state.sosStatus + ) + HorizontalDivider( + + ) + OSHStatusRow( + title = stringResource(R.string.conSysServiceStatusLabel), + subtitle = state.conSysStatus + ) + HorizontalDivider( + + ) + OSHStatusRow( + title = stringResource(R.string.discoveryServiceStatusLabel), + subtitle = state.discoveryStatus + ) + HorizontalDivider( + + ) + OSHStatusRow( + title = stringResource(R.string.sensorServiceStatusLabel), + subtitle = state.sensorStatus + ) + HorizontalDivider( + + ) + OSHStatusRow( + title = stringResource(R.string.storageServiceStatusLabel), + subtitle = state.storageStatus + ) + } + } + } +} + + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun AppStatusScreenPreview() { + OSHTheme { + AppStatusScreen( + onBackClick = {} + ) + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt new file mode 100644 index 00000000..e68abdcf --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt @@ -0,0 +1,100 @@ +package org.sensorhub.android.ui.screens.appstatus + +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import org.sensorhub.android.SensorHubService +import org.sensorhub.api.module.ModuleConfig + +class AppStatusViewModel(application: Application): AndroidViewModel(application) { + private val _state = MutableStateFlow(AppStatusState()) + val state: StateFlow = _state.asStateFlow() + private var boundService: SensorHubService? = null + + private val sConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + boundService = (service as SensorHubService.LocalBinder).service + refreshStatus() + } + + override fun onServiceDisconnected(name: ComponentName?) { + boundService = null + } + } + + init { + val context = getApplication() + context.bindService( + Intent(context, SensorHubService::class.java), + sConn, + Context.BIND_AUTO_CREATE + ) + viewModelScope.launch { + while (isActive) { + delay(3000) + refreshStatus() + } + } + } + + private fun refreshStatus() { + val service = boundService ?: return + val sensorhub = service.sensorHub ?: return + val modules = sensorhub.moduleRegistry.loadedModules + + var http = "Unknown" + var sos = "Unknown" + var conSys = "Unknown" + var discovery = "Unknown" + var sensor = "Unknown" + var storage = "Unknown" + + for (module in modules) { + val moduleConf = module.configuration + if (moduleConf is ModuleConfig) { + val status = module.currentState.name + val moduleId = moduleConf.id + + when (moduleId) { + "HTTP_SERVER_0" -> http = status + "SOS_SERVICE" -> sos = status + "CON_SYS_SERVICE" -> conSys = status + "DISCOVERY_SERVICE" -> discovery = status + "ANDROID_SENSORS" -> sensor = status + "ANDROID_SENSORS#storage" -> storage = status + } + } + } + + _state.value = AppStatusState( + httpStatus = http, + sosStatus = sos, + conSysStatus = conSys, + discoveryStatus = discovery, + sensorStatus = sensor, + storageStatus = storage + ) + + } + + override fun onCleared() { + super.onCleared() + try { + getApplication().unbindService(sConn) + } catch (_: IllegalArgumentException) { + + } + boundService = null + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt similarity index 93% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt index 85fe6698..623bfa6f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt @@ -1,4 +1,4 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.dashboard import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -16,7 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController import org.sensorhub.android.R -import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.navigation.Screen import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqContract.kt new file mode 100644 index 00000000..b5961aec --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqContract.kt @@ -0,0 +1,6 @@ +package org.sensorhub.android.ui.screens.help + +sealed class FaqItem { + data class Header(val title: String) : FaqItem() + data class Entry(val question: String, val answer: String) : FaqItem() +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqScreen.kt similarity index 94% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqScreen.kt index 40a6fc50..429a5bc5 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/HelpFaqScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/help/HelpFaqScreen.kt @@ -1,4 +1,4 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.help import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -74,10 +74,7 @@ fun HelpFaqScreen( } } -sealed class FaqItem { - data class Header(val title: String) : FaqItem() - data class Entry(val question: String, val answer: String) : FaqItem() -} + private fun buildSampleFaqItems(): List = listOf( FaqItem.Header("Getting Started"), diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesContract.kt new file mode 100644 index 00000000..894dc6c8 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesContract.kt @@ -0,0 +1,11 @@ +package org.sensorhub.android.ui.screens.preferences + +public val LANGUAGE_LABELS = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch", "Italiano", "Português") +public val LANGUAGE_VALUES = listOf("en", "zh-TW", "es", "fr", "de", "it", "pt") + +data class AppPreferencesState( + val deviceName: String = "MyDevice", + val deviceIpAddress: String = "", + val appVersion: String = "", + val selectedLanguageIndex: Int = 0 +) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt similarity index 67% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt index 7cc5bc61..76963b70 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt @@ -1,12 +1,6 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.preferences -import android.app.Application -import android.app.LocaleManager import android.content.Intent -import android.content.pm.PackageManager -import android.net.wifi.WifiManager -import android.os.LocaleList -import androidx.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -29,14 +23,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.sensorhub.android.activities.AppStatusActivity -import org.sensorhub.android.activities.HelpFaqActivity +import org.sensorhub.android.ui.AppStatusActivity +import org.sensorhub.android.ui.HelpFaqActivity import org.sensorhub.android.R import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard @@ -47,80 +37,6 @@ import org.sensorhub.android.ui.components.OSHTextInputDialog import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme -import java.math.BigInteger -import java.net.InetAddress -import java.nio.ByteOrder - -private val LANGUAGE_LABELS = listOf("English", "中文 (台灣)", "Español", "Français", "Deutsch", "Italiano", "Português") -private val LANGUAGE_VALUES = listOf("en", "zh-TW", "es", "fr", "de", "it", "pt") - -data class AppPreferencesState( - val deviceName: String = "MyDevice", - val deviceIpAddress: String = "", - val appVersion: String = "", - val selectedLanguageIndex: Int = 0 -) - -class AppPreferencesViewModel(application: Application) : AndroidViewModel(application) { - private val prefs = PreferenceManager.getDefaultSharedPreferences(application) - private val _state = MutableStateFlow(AppPreferencesState()) - val state: StateFlow = _state.asStateFlow() - - init { - val deviceName = prefs.getString("device_name", "MyDevice") ?: "MyDevice" - val ipAddress = getDeviceIpAddress() - val version = getAppVersion() - val currentLang = prefs.getString("app_language", "en") ?: "en" - val langIndex = LANGUAGE_VALUES.indexOf(currentLang).coerceAtLeast(0) - - _state.value = AppPreferencesState( - deviceName = deviceName, - deviceIpAddress = ipAddress, - appVersion = version, - selectedLanguageIndex = langIndex - ) - } - - fun updateDeviceName(name: String) { - prefs.edit().putString("device_name", name).apply() - _state.value = _state.value.copy(deviceName = name) - } - - fun selectLanguage(index: Int) { - val localeTag = LANGUAGE_VALUES[index] - prefs.edit().putString("app_language", localeTag).apply() - _state.value = _state.value.copy(selectedLanguageIndex = index) - - val localeManager = getApplication().getSystemService(LocaleManager::class.java) - localeManager.applicationLocales = LocaleList.forLanguageTags(localeTag) - } - - private fun getDeviceIpAddress(): String { - return try { - val wifiManager = getApplication().getSystemService(WifiManager::class.java) - var ipAddress = wifiManager.connectionInfo.ipAddress - - if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { - ipAddress = Integer.reverseBytes(ipAddress) - } - - val ipByteArray = BigInteger.valueOf(ipAddress.toLong()).toByteArray() - InetAddress.getByAddress(ipByteArray).hostAddress ?: "" - } catch (e: Exception) { - "" - } - } - - private fun getAppVersion(): String { - return try { - val context = getApplication() - val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) - pInfo.versionName ?: "" - } catch (e: PackageManager.NameNotFoundException) { - "" - } - } -} @Composable fun AppPreferencesScreen( diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesViewModel.kt new file mode 100644 index 00000000..efc4cd3a --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesViewModel.kt @@ -0,0 +1,76 @@ +package org.sensorhub.android.ui.screens.preferences + +import android.app.Application +import android.app.LocaleManager +import android.content.pm.PackageManager +import android.net.wifi.WifiManager +import android.os.LocaleList +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import java.math.BigInteger +import java.net.InetAddress +import java.nio.ByteOrder + +class AppPreferencesViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + private val _state = MutableStateFlow(AppPreferencesState()) + val state: StateFlow = _state.asStateFlow() + + init { + val deviceName = prefs.getString("device_name", "MyDevice") ?: "MyDevice" + val ipAddress = getDeviceIpAddress() + val version = getAppVersion() + val currentLang = prefs.getString("app_language", "en") ?: "en" + val langIndex = LANGUAGE_VALUES.indexOf(currentLang).coerceAtLeast(0) + + _state.value = AppPreferencesState( + deviceName = deviceName, + deviceIpAddress = ipAddress, + appVersion = version, + selectedLanguageIndex = langIndex + ) + } + + fun updateDeviceName(name: String) { + prefs.edit().putString("device_name", name).apply() + _state.value = _state.value.copy(deviceName = name) + } + + fun selectLanguage(index: Int) { + val localeTag = LANGUAGE_VALUES[index] + prefs.edit().putString("app_language", localeTag).apply() + _state.value = _state.value.copy(selectedLanguageIndex = index) + + val localeManager = getApplication().getSystemService(LocaleManager::class.java) + localeManager.applicationLocales = LocaleList.forLanguageTags(localeTag) + } + + private fun getDeviceIpAddress(): String { + return try { + val wifiManager = getApplication().getSystemService(WifiManager::class.java) + var ipAddress = wifiManager.connectionInfo.ipAddress + + if (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) { + ipAddress = Integer.reverseBytes(ipAddress) + } + + val ipByteArray = BigInteger.valueOf(ipAddress.toLong()).toByteArray() + InetAddress.getByAddress(ipByteArray).hostAddress ?: "" + } catch (e: Exception) { + "" + } + } + + private fun getAppVersion(): String { + return try { + val context = getApplication() + val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) + pInfo.versionName ?: "" + } catch (e: PackageManager.NameNotFoundException) { + "" + } + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt similarity index 99% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt index 56bc1e53..833eb82e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerFormScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt @@ -1,6 +1,5 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.profiles -import android.app.Application import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt new file mode 100644 index 00000000..4cd2be5f --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt @@ -0,0 +1,19 @@ +package org.sensorhub.android.ui.screens.profiles + +data class ServerProfileItem( + val id: String = "", + val name: String = "", + val host: String = "", + val port: Int = 8181, + val endpointPath: String = "/sensorhub/api", + val username: String = "", + val password: String = "", + val enableTls: Boolean = false, + val disableSslCheck: Boolean = false, + val useConSysClient: Boolean = false, + val oAuthEnabled: Boolean = false, + val enabled: Boolean = false, + val clientId: String = "", + val clientSecret: String = "", + val tokenEndpoint: String = "" +) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt similarity index 70% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt index 52f6daae..c54d5592 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt @@ -1,6 +1,5 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.profiles -import android.app.Application import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -8,7 +7,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Info import androidx.compose.material3.MaterialTheme @@ -28,17 +26,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import org.sensorhub.android.R -import org.sensorhub.android.server.ServerProfileRepository -import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.navigation.Screen import org.sensorhub.android.ui.components.OSHActionCard import org.sensorhub.android.ui.components.OSHAddFAB import org.sensorhub.android.ui.components.OSHAlertDialog @@ -46,62 +39,6 @@ import org.sensorhub.android.ui.components.OSHTopAppBarWithBack import org.sensorhub.android.ui.theme.Background import org.sensorhub.android.ui.theme.OSHTheme -data class ServerProfileItem( - val id: String = "", - val name: String = "", - val host: String = "", - val port: Int = 8181, - val endpointPath: String = "/sensorhub/api", - val username: String = "", - val password: String = "", - val enableTls: Boolean = false, - val disableSslCheck: Boolean = false, - val useConSysClient: Boolean = false, - val oAuthEnabled: Boolean = false, - val enabled: Boolean = false, - val clientId: String = "", - val clientSecret: String = "", - val tokenEndpoint: String = "" -) - -class ServerProfilesViewModel(application: Application) : AndroidViewModel(application) { - private val repo = ServerProfileRepository.getInstance(application) - private val _profiles = MutableStateFlow>(emptyList()) - val profiles: StateFlow> = _profiles.asStateFlow() - - init { - refresh() - } - - fun refresh() { - _profiles.value = repo.all.map { p -> - ServerProfileItem( - id = p.id, - name = p.name, - host = p.host, - port = p.port, - endpointPath = p.endpointPath, - username = p.username, - enableTls = p.enableTls, - disableSslCheck = p.disableSslCheck, - useConSysClient = p.useConSysClient, - oAuthEnabled = p.oAuthEnabled, - enabled = p.enabled - ) - } - } - - fun setEnabled(id: String, enabled: Boolean) { - repo.setEnabled(id, enabled) - refresh() - } - - fun delete(id: String) { - repo.delete(id) - refresh() - } -} - @Composable fun ServerProfilesScreen( onBackClick: () -> Unit, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt new file mode 100644 index 00000000..0d2ddd6c --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt @@ -0,0 +1,46 @@ +package org.sensorhub.android.ui.screens.profiles + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.sensorhub.android.server.ServerProfileRepository + +class ServerProfilesViewModel(application: Application) : AndroidViewModel(application) { + private val repo = ServerProfileRepository.getInstance(application) + private val _profiles = MutableStateFlow>(emptyList()) + val profiles: StateFlow> = _profiles.asStateFlow() + + init { + refresh() + } + + fun refresh() { + _profiles.value = repo.all.map { p -> + ServerProfileItem( + id = p.id, + name = p.name, + host = p.host, + port = p.port, + endpointPath = p.endpointPath, + username = p.username, + enableTls = p.enableTls, + disableSslCheck = p.disableSslCheck, + useConSysClient = p.useConSysClient, + oAuthEnabled = p.oAuthEnabled, + enabled = p.enabled + ) + } + } + + fun setEnabled(id: String, enabled: Boolean) { + repo.setEnabled(id, enabled) + refresh() + } + + fun delete(id: String) { + repo.delete(id) + refresh() + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/BluetoothScanner.kt similarity index 86% rename from sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/BluetoothScanner.kt index ce9aabcf..dc03190d 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/BluetoothScanner.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/BluetoothScanner.kt @@ -1,4 +1,4 @@ -package org.sensorhub.android.ui +package org.sensorhub.android.ui.screens.sensors import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter @@ -19,8 +19,8 @@ class BluetoothScanner(private val context: Context) { private val receiver = object : BroadcastReceiver() { @SuppressLint("MissingPermission") override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == BluetoothDevice.ACTION_FOUND) { // - val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) // + if (intent?.action == BluetoothDevice.ACTION_FOUND) { + val device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE) if (device != null && !foundDevices.contains(device)) { foundDevices.add(device) } @@ -32,12 +32,11 @@ class BluetoothScanner(private val context: Context) { fun startDiscovery() { foundDevices.clear() - // Include already paired devices first if desired - btAdapter?.bondedDevices?.let { foundDevices.addAll(it) } // + btAdapter?.bondedDevices?.let { foundDevices.addAll(it) } - val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) // + val filter = IntentFilter(BluetoothDevice.ACTION_FOUND) context.registerReceiver(receiver, filter) - btAdapter?.startDiscovery() // + btAdapter?.startDiscovery() } @SuppressLint("MissingPermission") @@ -50,4 +49,4 @@ class BluetoothScanner(private val context: Context) { btAdapter?.cancelDiscovery() } -} +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsContract.kt new file mode 100644 index 00000000..b201c136 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsContract.kt @@ -0,0 +1,278 @@ +package org.sensorhub.android.ui.screens.sensors + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Audiotrack +import androidx.compose.material.icons.filled.DevicesOther +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.ui.graphics.vector.ImageVector +import org.sensorhub.android.R + +enum class SensorCategory(@StringRes val labelRes: Int) { + ON_DEVICE(R.string.category_on_device), + BLUETOOTH(R.string.category_bluetooth), + USB(R.string.category_usb), + OTHERS(R.string.category_others), +} + +sealed class SensorConfig { + data class BluetoothDevice( + @StringRes val selectLabelRes: Int, + val addressPrefKey: String, + ) : SensorConfig() + + data object Video : SensorConfig() + data object Audio : SensorConfig() + data object TruPulse : SensorConfig() +} + +data class SensorItem( + val id: String, + val prefKey: String, + @StringRes val nameRes: Int, + @StringRes val summaryRes: Int, + val category: SensorCategory, + val enabled: Boolean = false, + val config: SensorConfig? = null, +) + +data class ChoiceDialogSpec( + val prefKey: String, + @StringRes val titleRes: Int, + val icon: ImageVector, + val values: List, + val labels: List = values, + val defaultValue: String = values.first(), +) + +val CHOICE_DIALOGS = listOf( + ChoiceDialogSpec( + "audio_codec", R.string.title_codec, Icons.Default.Audiotrack, + listOf("AAC", "AMR-NB", "AMR-WB", "FLAC", "VORBIS") + ), + ChoiceDialogSpec( + "audio_samplerate", R.string.title_sample_rate, Icons.Default.Audiotrack, + listOf("8000", "11025", "22050", "44100", "48000"), + listOf("8000 Hz", "11025 Hz", "22050 Hz", "44100 Hz", "48000 Hz") + ), + ChoiceDialogSpec( + "audio_bitrate", R.string.title_bitrate, Icons.Default.Audiotrack, + listOf("32", "64", "96", "128", "160", "192"), + listOf("32 kbps", "64 kbps", "96 kbps", "128 kbps", "160 kbps", "192 kbps"), + defaultValue = "64" + ), + ChoiceDialogSpec( + "video_codec", R.string.title_codec, Icons.Default.Videocam, + listOf("JPEG", "H264", "H265", "VP9", "VP8") + ), + ChoiceDialogSpec( + "video_framerate", R.string.title_frame_rate, Icons.Default.Videocam, + listOf("10", "15", "24", "30", "60", "120"), + listOf("10 fps", "15 fps", "24 fps", "30 fps", "60 fps", "120 fps"), + defaultValue = "30" + ), + ChoiceDialogSpec( + "video_resolution", R.string.title_resolution, Icons.Default.Videocam, + listOf("320x240", "640x480", "1280x720", "1920x1080"), + defaultValue = "640x480" + ), + ChoiceDialogSpec( + "camera_select", R.string.title_camera, Icons.Default.Videocam, + listOf("0", "1"), + listOf("Camera 0", "Camera 1") + ), + ChoiceDialogSpec( + "trupulse_datasource", R.string.title_trupulse_datasource, Icons.Default.DevicesOther, + listOf("STREAM", "SIMULATED"), + listOf("Streaming Physical Device", "Simulate Virtual Device") + ), +) + +val ALL_SENSORS: List = listOf( + // On-device sensors (no config) + SensorItem( + "accelerometer", + "accel_enabled", + R.string.sensor_accelerometer, + R.string.summary_accel, + SensorCategory.ON_DEVICE + ), + SensorItem( + "gyroscope", + "gyro_enabled", + R.string.sensor_gyroscope, + R.string.summary_gyro, + SensorCategory.ON_DEVICE + ), + SensorItem( + "magnetometer", + "mag_enabled", + R.string.sensor_magnetometer, + R.string.summary_mag, + SensorCategory.ON_DEVICE + ), + SensorItem( + "orient_q", + "orient_quat_enabled", + R.string.sensor_orientation_quat, + R.string.summary_orientation, + SensorCategory.ON_DEVICE + ), + SensorItem( + "orient_e", + "orient_euler_enabled", + R.string.sensor_orientation_euler, + R.string.summary_orientation, + SensorCategory.ON_DEVICE + ), + SensorItem( + "gps", + "gps_enabled", + R.string.sensor_gps, + R.string.summary_gps, + SensorCategory.ON_DEVICE + ), + SensorItem( + "network", + "netloc_enabled", + R.string.sensor_network_location, + R.string.summary_netloc, + SensorCategory.ON_DEVICE + ), + SensorItem( + "video_roll", + "video_roll_enabled", + R.string.sensor_video_roll, + R.string.summary_video_roll, + SensorCategory.ON_DEVICE + ), + + // On-device sensors (with config) + SensorItem( + "camera", + "cam_enabled", + R.string.sensor_video, + R.string.summary_video, + SensorCategory.ON_DEVICE, + config = SensorConfig.Video + ), + SensorItem( + "audio", + "audio_enabled", + R.string.sensor_audio, + R.string.summary_audio, + SensorCategory.ON_DEVICE, + config = SensorConfig.Audio + ), + + // Bluetooth sensors + SensorItem( + "meshtastic", + "meshtastic_enabled", + R.string.sensor_meshtastic, + R.string.summary_meshtastic, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice( + R.string.title_select_meshtastic, + "meshtastic_device_address" + ) + ), + SensorItem( + "polar", + "polar_enabled", + R.string.sensor_polar, + R.string.summary_polar, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice(R.string.title_select_polar, "polar_device_address") + ), + SensorItem( + "kestrel", + "kestrel_enabled", + R.string.sensor_kestrel, + R.string.summary_kestrel, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice( + R.string.title_select_kestrel, + "kestrel_device_address" + ) + ), + SensorItem( + "garmin", + "garmin_enabled", + R.string.sensor_garmin, + R.string.summary_garmin, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice(R.string.title_select_garmin, "garmin_device_address") + ), + SensorItem( + "ste", + "ste_radpager_enabled", + R.string.sensor_ste, + R.string.summary_ste, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice(R.string.title_select_device, "ste_device_address") + ), + SensorItem( + "trupulse", + "trupulse_enabled", + R.string.sensor_trupulse, + R.string.summary_trupulse, + SensorCategory.BLUETOOTH, + config = SensorConfig.TruPulse + ), + SensorItem( + "angel", + "angel_enabled", + R.string.sensor_angel, + R.string.summary_angel, + SensorCategory.BLUETOOTH, + config = SensorConfig.BluetoothDevice(R.string.title_select_device, "angel_address") + ), + + // USB sensors + SensorItem( + "controller", + "controller_enabled", + R.string.sensor_controller, + R.string.summary_controller, + SensorCategory.USB + ), + SensorItem( + "flirone", + "flirone_enabled", + R.string.sensor_flirone, + R.string.summary_flirone, + SensorCategory.USB + ), + + // Others + SensorItem( + "wardriving", + "wardriving_enabled", + R.string.sensor_wardriving, + R.string.summary_wardriving, + SensorCategory.OTHERS + ), + SensorItem( + "template", + "template_enabled", + R.string.sensor_template, + R.string.summary_template, + SensorCategory.OTHERS, + config = SensorConfig.BluetoothDevice( + R.string.title_select_device, + "template_device_address" + ) + ), +) + +val BT_ADDRESS_PREF_KEYS = listOf( + "meshtastic_device_address", + "polar_device_address", + "kestrel_device_address", + "garmin_device_address", + "ste_device_address", + "trupulse_device_address", + "angel_address", + "template_device_address", +) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt new file mode 100644 index 00000000..c6c09f5e --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt @@ -0,0 +1,402 @@ +package org.sensorhub.android.ui.screens.sensors + +import android.Manifest +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Audiotrack +import androidx.compose.material.icons.filled.Bluetooth +import androidx.compose.material.icons.filled.DevicesOther +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.NavController +import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.R +import org.sensorhub.android.ui.navigation.Screen +import org.sensorhub.android.ui.components.OSHBluetoothPickerDialog +import org.sensorhub.android.ui.components.OSHClickableRowWithIcon +import org.sensorhub.android.ui.components.OSHExpandableSwitchCard +import org.sensorhub.android.ui.components.OSHFilterChip +import org.sensorhub.android.ui.components.OSHSensorCard +import org.sensorhub.android.ui.components.OSHSingleChoiceDialog +import org.sensorhub.android.ui.components.OSHSwitchRow +import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme + + +@Composable +fun SensorsScreen( + navController: NavController = rememberNavController(), + viewModel: SensorsViewModel = viewModel(), +) { + var selectedCategories by remember { mutableStateOf(SensorCategory.entries.toSet()) } + val scrollState = rememberScrollState() + var activeDialog by remember { mutableStateOf(null) } + + val context = LocalContext.current + val scanner = remember { BluetoothScanner(context) } + val bluetoothDevices = scanner.foundDevices + + val requiredPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + arrayOf(Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT) + } else { + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION) + } + + val permissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + ) { results -> + if (results.values.all { it }) { + scanner.startDiscovery() + } + } + + DisposableEffect(Unit) { + onDispose { scanner.stopDiscovery() } + } + + if (activeDialog != null && activeDialog in BT_ADDRESS_PREF_KEYS) { + val addressKey = activeDialog!! + OSHBluetoothPickerDialog( + devices = bluetoothDevices, + currentAddress = viewModel.stringStates[addressKey] ?: "", + onDeviceSelected = { address, _ -> + viewModel.setStringPref(addressKey, address) + activeDialog = null + }, + onDismiss = { + activeDialog = null + scanner.stopDiscovery() + }, + onStartScan = { permissionLauncher.launch(requiredPermissions) }, + ) + } + + CHOICE_DIALOGS.find { it.prefKey == activeDialog }?.let { spec -> + val current = viewModel.stringStates[spec.prefKey] ?: spec.defaultValue + OSHSingleChoiceDialog( + onDismissRequest = { activeDialog = null }, + dialogTitle = stringResource(spec.titleRes), + icon = spec.icon, + options = spec.labels, + selectedIndex = spec.values.indexOf(current).coerceAtLeast(0), + onOptionSelected = { index -> + viewModel.setStringPref(spec.prefKey, spec.values[index]) + }, + ) + } + + Scaffold( + topBar = { + OSHTopAppBarWithLogo( + title = stringResource(R.string.tab_sensors), + actions = { + IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = null, + ) + } + }, + ) + }, + ) { padding -> + val resolvedItems = ALL_SENSORS.map { + it.copy(enabled = viewModel.toggleStates[it.prefKey] ?: false) + } + val filteredItems = if (selectedCategories.isEmpty()) resolvedItems + else resolvedItems.filter { it.category in selectedCategories } + val grouped = filteredItems.groupBy { it.category } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding), + ) { + Row( + modifier = Modifier + .horizontalScroll(scrollState) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + SensorCategory.entries.forEach { category -> + OSHFilterChip( + onClick = { + selectedCategories = if (category in selectedCategories) + selectedCategories - category + else + selectedCategories + category + }, + text = stringResource(category.labelRes), + selected = category in selectedCategories, + ) + } + } + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + ) { + grouped.forEach { (category, sensors) -> + item { + Text( + text = stringResource(category.labelRes), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp), + ) + } + items(sensors, key = { it.id }) { sensor -> + SensorListItem( + sensor = sensor, + viewModel = viewModel, + onActiveDialogChange = { activeDialog = it }, + ) + } + } + } + } + } +} + +@Composable +private fun SensorListItem( + sensor: SensorItem, + viewModel: SensorsViewModel, + onActiveDialogChange: (String) -> Unit, +) { + val onToggle = { checked: Boolean -> + viewModel.toggleSensor(sensor.prefKey, checked) + } + + when (val config = sensor.config) { + is SensorConfig.Audio -> ConfigurableSensorCard(sensor, onToggle) { + AudioConfig( + codec = viewModel.stringStates["audio_codec"] ?: "AAC", + sampleRate = viewModel.stringStates["audio_samplerate"] ?: "8000", + bitRate = viewModel.stringStates["audio_bitrate"] ?: "64", + onCodecClick = { onActiveDialogChange("audio_codec") }, + onSampleRateClick = { onActiveDialogChange("audio_samplerate") }, + onBitRateClick = { onActiveDialogChange("audio_bitrate") }, + ) + } + + is SensorConfig.Video -> ConfigurableSensorCard(sensor, onToggle) { + VideoConfig( + codec = viewModel.stringStates["video_codec"] ?: "JPEG", + frameRate = viewModel.stringStates["video_framerate"] ?: "30", + resolution = viewModel.stringStates["video_resolution"] ?: "640x480", + camera = viewModel.stringStates["camera_select"] ?: "0", + onCodecClick = { onActiveDialogChange("video_codec") }, + onFrameRateClick = { onActiveDialogChange("video_framerate") }, + onResolutionClick = { onActiveDialogChange("video_resolution") }, + onCameraClick = { onActiveDialogChange("camera_select") }, + ) + } + + is SensorConfig.BluetoothDevice -> ConfigurableSensorCard(sensor, onToggle) { + BluetoothDeviceConfig( + selectLabelRes = config.selectLabelRes, + currentAddress = viewModel.stringStates[config.addressPrefKey] ?: "", + onSelect = { onActiveDialogChange(config.addressPrefKey) }, + ) + } + + is SensorConfig.TruPulse -> { + val dsSpec = CHOICE_DIALOGS.first { it.prefKey == "trupulse_datasource" } + val dsIndex = dsSpec.values.indexOf( + viewModel.stringStates["trupulse_datasource"] ?: "STREAM", + ).coerceAtLeast(0) + + ConfigurableSensorCard(sensor, onToggle) { + TruPulseConfig( + datasource = dsSpec.labels[dsIndex], + deviceAddress = viewModel.stringStates["trupulse_device_address"] ?: "", + onDatasourceClick = { onActiveDialogChange("trupulse_datasource") }, + onDeviceClick = { onActiveDialogChange("trupulse_device_address") }, + ) + } + } + + null -> { + OSHSensorCard { + OSHSwitchRow( + title = stringResource(sensor.nameRes), + subtitle = stringResource(sensor.summaryRes), + checked = sensor.enabled, + onCheckedChange = onToggle, + ) + } + } + } +} + +@Composable +private fun ConfigurableSensorCard( + sensor: SensorItem, + onToggle: (Boolean) -> Unit, + expandedContent: @Composable () -> Unit, +) { + OSHExpandableSwitchCard( + title = stringResource(sensor.nameRes), + subtitle = stringResource(sensor.summaryRes), + checked = sensor.enabled, + onCheckedChange = onToggle, + configHint = stringResource(R.string.hint_tap_to_configure), + expandedContent = { + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + expandedContent() + }, + ) +} + +@Composable +private fun AudioConfig( + codec: String, + sampleRate: String, + bitRate: String, + onCodecClick: () -> Unit, + onSampleRateClick: () -> Unit, + onBitRateClick: () -> Unit, +) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_codec), + subtitle = codec, + imageVector = Icons.Default.Audiotrack, + contentDescription = stringResource(R.string.title_codec), + onClick = onCodecClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_sample_rate), + subtitle = "$sampleRate Hz", + imageVector = Icons.Default.Audiotrack, + contentDescription = stringResource(R.string.title_sample_rate), + onClick = onSampleRateClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_bitrate), + subtitle = "$bitRate kbps", + imageVector = Icons.Default.Audiotrack, + contentDescription = stringResource(R.string.title_bitrate), + onClick = onBitRateClick, + ) +} + +@Composable +private fun VideoConfig( + codec: String, + frameRate: String, + resolution: String, + camera: String, + onCodecClick: () -> Unit, + onFrameRateClick: () -> Unit, + onResolutionClick: () -> Unit, + onCameraClick: () -> Unit, +) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_camera), + subtitle = "Camera $camera", + imageVector = Icons.Default.Videocam, + contentDescription = stringResource(R.string.title_camera), + onClick = onCameraClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_codec), + subtitle = codec, + imageVector = Icons.Default.Videocam, + contentDescription = stringResource(R.string.title_codec), + onClick = onCodecClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_resolution), + subtitle = resolution, + imageVector = Icons.Default.Videocam, + contentDescription = stringResource(R.string.title_resolution), + onClick = onResolutionClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_frame_rate), + subtitle = "$frameRate fps", + imageVector = Icons.Default.Videocam, + contentDescription = stringResource(R.string.title_frame_rate), + onClick = onFrameRateClick, + ) +} + +@Composable +private fun BluetoothDeviceConfig( + @StringRes selectLabelRes: Int, + currentAddress: String, + onSelect: () -> Unit, +) { + OSHClickableRowWithIcon( + title = stringResource(selectLabelRes), + subtitle = currentAddress.ifEmpty { stringResource(R.string.summary_select_device) }, + imageVector = Icons.Default.Bluetooth, + contentDescription = stringResource(selectLabelRes), + onClick = onSelect, + ) +} + +@Composable +private fun TruPulseConfig( + datasource: String, + deviceAddress: String, + onDatasourceClick: () -> Unit, + onDeviceClick: () -> Unit, +) { + OSHClickableRowWithIcon( + title = stringResource(R.string.title_trupulse_datasource), + subtitle = datasource, + imageVector = Icons.Default.DevicesOther, + contentDescription = stringResource(R.string.title_trupulse_datasource), + onClick = onDatasourceClick, + ) + OSHClickableRowWithIcon( + title = stringResource(R.string.title_select_trupulse), + subtitle = deviceAddress.ifEmpty { stringResource(R.string.summary_select_device) }, + imageVector = Icons.Default.Bluetooth, + contentDescription = stringResource(R.string.title_select_trupulse), + onClick = onDeviceClick, + ) +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun SensorsScreenPreview() { + OSHTheme { + SensorsScreen() + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsViewModel.kt new file mode 100644 index 00000000..3ef9270a --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsViewModel.kt @@ -0,0 +1,40 @@ +package org.sensorhub.android.ui.screens.sensors + +import android.app.Application +import androidx.compose.runtime.mutableStateMapOf +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager + +class SensorsViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + + private val _toggleStates = mutableStateMapOf() + val toggleStates: Map = _toggleStates + + private val _stringStates = mutableStateMapOf() + val stringStates: Map = _stringStates + + init { + ALL_SENSORS.forEach { sensor -> + _toggleStates[sensor.prefKey] = prefs.getBoolean(sensor.prefKey, false) + } + + CHOICE_DIALOGS.forEach { spec -> + _stringStates[spec.prefKey] = prefs.getString(spec.prefKey, spec.defaultValue) ?: spec.defaultValue + } + + BT_ADDRESS_PREF_KEYS.forEach { key -> + _stringStates[key] = prefs.getString(key, "") ?: "" + } + } + + fun toggleSensor(prefKey: String, enabled: Boolean) { + prefs.edit().putBoolean(prefKey, enabled).apply() + _toggleStates[prefKey] = enabled + } + + fun setStringPref(prefKey: String, value: String) { + prefs.edit().putString(prefKey, value).apply() + _stringStates[prefKey] = value + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersContract.kt new file mode 100644 index 00000000..59e1e430 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersContract.kt @@ -0,0 +1,7 @@ +package org.sensorhub.android.ui.screens.servers + +data class ServersState( + val sosEnabled: Boolean = true, + val csApiEnabled: Boolean = true, + val discoveryEnabled: Boolean = false +) \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt similarity index 69% rename from sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt index ecc3e70f..aba3cfad 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt @@ -1,7 +1,5 @@ -package org.sensorhub.android.ui.screens +package org.sensorhub.android.ui.screens.servers -import android.app.Application -import androidx.preference.PreferenceManager import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -18,57 +16,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import org.sensorhub.android.R -import org.sensorhub.android.ui.Screen +import org.sensorhub.android.ui.navigation.Screen import org.sensorhub.android.ui.components.OSHCard import org.sensorhub.android.ui.components.OSHClickableCardWithIcon import org.sensorhub.android.ui.components.OSHSwitchRow import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo import org.sensorhub.android.ui.theme.OSHTheme -data class ServersState( - val sosEnabled: Boolean = true, - val csApiEnabled: Boolean = true, - val discoveryEnabled: Boolean = false -) - -class ServersViewModel(application: Application) : AndroidViewModel(application) { - private val prefs = PreferenceManager.getDefaultSharedPreferences(application) - private val _state = MutableStateFlow(ServersState()) - val state: StateFlow = _state.asStateFlow() - - init { - _state.value = ServersState( - sosEnabled = prefs.getBoolean("sos_service", true), - csApiEnabled = prefs.getBoolean("csapi_service", true), - discoveryEnabled = prefs.getBoolean("discovery_service", false) - ) - } - - fun setSosEnabled(enabled: Boolean) { - prefs.edit().putBoolean("sos_service", enabled).apply() - _state.value = _state.value.copy(sosEnabled = enabled) - } - - fun setCsApiEnabled(enabled: Boolean) { - prefs.edit().putBoolean("csapi_service", enabled).apply() - _state.value = _state.value.copy(csApiEnabled = enabled) - } - - fun setDiscoveryEnabled(enabled: Boolean) { - prefs.edit().putBoolean("discovery_service", enabled).apply() - _state.value = _state.value.copy(discoveryEnabled = enabled) - } -} - @Composable fun ServersScreen( navController: NavController = rememberNavController(), diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersViewModel.kt new file mode 100644 index 00000000..e4b5c4a6 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersViewModel.kt @@ -0,0 +1,37 @@ +package org.sensorhub.android.ui.screens.servers + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class ServersViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + private val _state = MutableStateFlow(ServersState()) + val state: StateFlow = _state.asStateFlow() + + init { + _state.value = ServersState( + sosEnabled = prefs.getBoolean("sos_service", true), + csApiEnabled = prefs.getBoolean("csapi_service", true), + discoveryEnabled = prefs.getBoolean("discovery_service", false) + ) + } + + fun setSosEnabled(enabled: Boolean) { + prefs.edit().putBoolean("sos_service", enabled).apply() + _state.value = _state.value.copy(sosEnabled = enabled) + } + + fun setCsApiEnabled(enabled: Boolean) { + prefs.edit().putBoolean("csapi_service", enabled).apply() + _state.value = _state.value.copy(csApiEnabled = enabled) + } + + fun setDiscoveryEnabled(enabled: Boolean) { + prefs.edit().putBoolean("discovery_service", enabled).apply() + _state.value = _state.value.copy(discoveryEnabled = enabled) + } +} From 35b335e3007eb11f5b0970b0d0baef2c6c6528a8 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 16:36:22 -0500 Subject: [PATCH 56/63] Update Dialog.kt --- .../sensorhub/android/ui/components/Dialog.kt | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt index d15ed1ae..22c18e46 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/components/Dialog.kt @@ -1,5 +1,8 @@ package org.sensorhub.android.ui.components +import android.annotation.SuppressLint +import android.bluetooth.BluetoothDevice +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,11 +12,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.selection.selectable import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Bluetooth import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.RadioButton @@ -21,6 +27,7 @@ import androidx.compose.material3.RadioButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -28,9 +35,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import org.sensorhub.android.R import org.sensorhub.android.ui.theme.OSHTheme import org.sensorhub.android.ui.theme.Primary @@ -223,6 +232,101 @@ fun OSHSingleChoiceDialog( ) } +@SuppressLint("MissingPermission") +@Composable +fun OSHBluetoothPickerDialog( + devices: List, + currentAddress: String, + onDeviceSelected: (address: String, displayName: String) -> Unit, + onDismiss: () -> Unit, + onStartScan: () -> Unit +) { + var manualEntry by remember { mutableStateOf("") } + var showManualInput by remember { mutableStateOf(false) } + + DisposableEffect(Unit) { + onStartScan() + onDispose { } + } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { Icon(Icons.Default.Bluetooth, contentDescription = null) }, + title = { Text(stringResource(R.string.title_select_device)) }, + text = { + Column { + if (showManualInput) { + OutlinedTextField( + value = manualEntry, + onValueChange = { manualEntry = it }, + placeholder = { Text(stringResource(R.string.hint_manual_device)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + TextButton(onClick = { + if (manualEntry.isNotBlank()) { + onDeviceSelected(manualEntry.trim(), manualEntry.trim()) + } + }) { + Text(stringResource(R.string.btn_ok)) + } + HorizontalDivider() + } + + TextButton(onClick = { showManualInput = !showManualInput }) { + Text(stringResource(R.string.manual_entry_option)) + } + + HorizontalDivider() + Spacer(modifier = Modifier.height(8.dp)) + + if (devices.isEmpty()) { + Text( + text = "Scanning...", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + devices.forEach { device -> + val name = device.name ?: "Unknown Device" + val mac = device.address + val displayText = "$name ($mac)" + val isSelected = mac == currentAddress + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onDeviceSelected(mac, displayText) } + .padding(vertical = 10.dp, horizontal = 4.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = name, + style = MaterialTheme.typography.bodyLarge, + color = if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurface + ) + Text( + text = mac, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.btn_cancel)) + } + } + ) +} + @Preview(showBackground = true, backgroundColor = 0xFFFFFFFF) @Composable private fun DialogPreview() { From e5fe5c31f9d59c2149e37714ce360d04525c5eb2 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 16:58:21 -0500 Subject: [PATCH 57/63] removed dead files + added view model for home/dashboard screen --- sensorhub-android-app/res/xml/pref_app.xml | 63 ---- .../res/xml/pref_sensors.xml | 264 --------------- .../org/sensorhub/android/MainActivity.java | 4 +- .../sensorhub/android/SensorsFragment.java | 311 ------------------ .../sensorhub/android/SettingsFragment.java | 92 ------ .../server/EditServerProfileActivity.java | 206 ------------ .../android/server/ServerAdapter.java | 82 ----- .../server/ServerProfilesActivity.java | 95 ------ .../ui/screens/dashboard/HomeViewModel.kt | 13 + .../screens/profiles}/ServerProfile.java | 2 +- .../profiles}/ServerProfileRepository.java | 2 +- 11 files changed, 17 insertions(+), 1117 deletions(-) delete mode 100644 sensorhub-android-app/res/xml/pref_app.xml delete mode 100644 sensorhub-android-app/res/xml/pref_sensors.xml delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt rename sensorhub-android-app/src/org/sensorhub/android/{server => ui/screens/profiles}/ServerProfile.java (98%) rename sensorhub-android-app/src/org/sensorhub/android/{server => ui/screens/profiles}/ServerProfileRepository.java (99%) diff --git a/sensorhub-android-app/res/xml/pref_app.xml b/sensorhub-android-app/res/xml/pref_app.xml deleted file mode 100644 index a141187a..00000000 --- a/sensorhub-android-app/res/xml/pref_app.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml deleted file mode 100644 index 0aab7631..00000000 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ /dev/null @@ -1,264 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 671be06a..831260a2 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -51,8 +51,8 @@ import org.sensorhub.android.comm.BluetoothCommProviderConfig; import org.sensorhub.android.comm.ble.BleConfig; import org.sensorhub.android.comm.ble.BleNetwork; -import org.sensorhub.android.server.ServerProfile; -import org.sensorhub.android.server.ServerProfileRepository; +import org.sensorhub.android.ui.screens.profiles.ServerProfile; +import org.sensorhub.android.ui.screens.profiles.ServerProfileRepository; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.api.sensor.SensorConfig; import org.sensorhub.impl.client.sost.SOSTClient; diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java deleted file mode 100644 index acbd81a7..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ /dev/null @@ -1,311 +0,0 @@ -package org.sensorhub.android; - -import android.Manifest; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothManager; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.Camera; -import android.os.Build; -import android.os.Bundle; -import android.text.InputType; -import android.util.Log; -import android.widget.EditText; -import android.widget.FrameLayout; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityCompat; -import androidx.preference.ListPreference; -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; -import androidx.preference.SwitchPreferenceCompat; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Set; - - -/* - * Fragment for sensor preferences - */ -public class SensorsFragment extends PreferenceFragmentCompat { - - private static final String[][] SWITCH_DEPENDENTS = { - {"accel_enabled"}, - {"gyro_enabled"}, - {"mag_enabled"}, - {"orient_quat_enabled"}, - {"orient_euler_enabled"}, - {"gps_enabled"}, - {"netloc_enabled"}, - {"cam_enabled", "video_codec", "video_framerate", "video_resolution", "camera_select"}, - {"video_roll_enabled"}, - {"audio_enabled", "audio_codec", "audio_samplerate", "audio_bitrate"}, - {"angel_enabled", "angel_address"}, - {"flirone_enabled"}, - {"trupulse_enabled", "trupulse_datasource", "trupulse_device_address", "trupulse_simu"}, - {"kestrel_enabled", "kestrel_device_address"}, - {"garmin_enabled", "garmin_device_address"}, - {"polar_enabled", "polar_device_address"}, - {"meshtastic_enabled", "meshtastic_device_address"}, - {"ste_radpager_enabled"}, - {"wardriving_enabled"}, - {"controller_enabled"}, - {"template_enabled", "template_device_address"}, - }; - - /** Keys of Preferences that use the Bluetooth device picker dialog */ - private static final String[] BT_DEVICE_PREF_KEYS = { - "meshtastic_device_address", - "angel_address", - "polar_device_address", - "kestrel_device_address", - "trupulse_device_address", - "template_device_address", - }; - - private ArrayList frameRateList = new ArrayList<>(); - private ArrayList resList = new ArrayList<>(); - - @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { - setPreferencesFromResource(R.xml.pref_sensors, rootKey); - - for (String[] group : SWITCH_DEPENDENTS) { - String switchKey = group[0]; - SwitchPreferenceCompat switchPref = findPreference(switchKey); - if (switchPref == null) continue; - - boolean isChecked = switchPref.isChecked(); - for (int i = 1; i < group.length; i++) { - Preference dep = findPreference(group[i]); - if (dep != null) dep.setVisible(isChecked); - } - - switchPref.setOnPreferenceChangeListener((preference, newValue) -> { - boolean enabled = (boolean) newValue; - for (int i = 1; i < group.length; i++) { - Preference dep = findPreference(group[i]); - if (dep != null) dep.setVisible(enabled); - } - return true; - }); - } - - setupVideoPreferences(); - setupAudioPreferences(); - - setupBluetoothDevicePickers(); - } - - private void setupBluetoothDevicePickers() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - - for (String key : BT_DEVICE_PREF_KEYS) { - Preference pref = findPreference(key); - if (pref == null) continue; - - String saved = prefs.getString(key, ""); - if (!saved.isEmpty()) { - pref.setSummary(saved); - } - - pref.setOnPreferenceClickListener(p -> { - showDevicePickerDialog(key); - return true; - }); - } - } - - private void showDevicePickerDialog(String prefKey) { - List names = new ArrayList<>(); - List addresses = new ArrayList<>(); - - // Gather all bonded Bluetooth devices (classic + BLE) - BluetoothAdapter btAdapter = getBluetoothAdapter(); - if (btAdapter != null && btAdapter.isEnabled() && hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) { - Set bondedDevices = btAdapter.getBondedDevices(); - for (BluetoothDevice device : bondedDevices) { - String name = device.getName(); - String mac = device.getAddress(); - names.add(name != null ? name + " (" + mac + ")" : mac); - addresses.add(mac); - } - } - - names.add(getString(R.string.manual_entry_option)); - addresses.add(null); - - String[] displayNames = names.toArray(new String[0]); - - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.title_select_device) - .setItems(displayNames, (dialog, which) -> { - if (addresses.get(which) == null) { - showManualAddressDialog(prefKey); - } else { - saveDeviceAddress(prefKey, addresses.get(which), displayNames[which]); - } - }) - .setNegativeButton(R.string.btn_cancel, null) - .show(); - } - - private void showManualAddressDialog(String prefKey) { - EditText input = new EditText(requireContext()); - input.setInputType(InputType.TYPE_CLASS_TEXT); - input.setHint(R.string.hint_manual_device); - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - String current = prefs.getString(prefKey, ""); - if (!current.isEmpty()) { - input.setText(current); - input.selectAll(); - } - - int padding = (int) (24 * getResources().getDisplayMetrics().density); - FrameLayout container = new FrameLayout(requireContext()); - container.setPadding(padding, padding, padding, 0); - container.addView(input); - - new AlertDialog.Builder(requireContext()) - .setTitle(R.string.title_enter_device) - .setMessage(R.string.msg_enter_device) - .setView(container) - .setPositiveButton(R.string.btn_ok, (dialog, which) -> { - String address = input.getText().toString().trim(); - if (!address.isEmpty()) { - saveDeviceAddress(prefKey, address, address); - } - }) - .setNegativeButton(R.string.btn_cancel, null) - .show(); - } - - private void saveDeviceAddress(String prefKey, String address, String displayText) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - prefs.edit().putString(prefKey, address).apply(); - - Preference pref = findPreference(prefKey); - if (pref != null) { - pref.setSummary(displayText); - } - } - - - private void setupVideoPreferences() { - // Camera selection - ArrayList cameras = new ArrayList<>(); - try { - for (int i = 0; i < Camera.getNumberOfCameras(); i++) { - cameras.add(Integer.toString(i)); - } - } catch (Exception e) { - cameras.add("0"); - } - - ListPreference cameraSelectList = findPreference("camera_select"); - if (cameraSelectList != null) { - cameraSelectList.setEntries(cameras.toArray(new String[0])); - cameraSelectList.setEntryValues(cameras.toArray(new String[0])); - cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> { - Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue); - updateCameraSettings(Integer.parseInt((String) newValue)); - return true; - }); - } - - // Frame rates and resolutions from camera - Camera camera = null; - try { - camera = Camera.open(0); - Camera.Parameters camParams = camera.getParameters(); - for (int frameRate : camParams.getSupportedPreviewFrameRates()) - frameRateList.add(Integer.toString(frameRate)); - for (Camera.Size imgSize : camParams.getSupportedPreviewSizes()) - resList.add(imgSize.width + "x" + imgSize.height); - } catch (Exception e) { - frameRateList.add("30"); - resList.add("640x480"); - } finally { - if (camera != null) camera.release(); - } - - ListPreference frameRatePrefList = findPreference("video_framerate"); - if (frameRatePrefList != null) { - frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); - frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); - } - - // Resolution list - ListPreference resolutionPrefList = findPreference("video_resolution"); - if (resolutionPrefList != null) { - resolutionPrefList.setEntries(resList.toArray(new String[0])); - resolutionPrefList.setEntryValues(resList.toArray(new String[0])); - if (!resList.isEmpty() && resolutionPrefList.getValue() == null) - resolutionPrefList.setValue(resList.get(0)); - } - } - - private void updateCameraSettings(int cameraId) { - Camera camera = null; - try { - frameRateList.clear(); - resList.clear(); - camera = Camera.open(cameraId); - Camera.Parameters camParams = camera.getParameters(); - for (int frameRate : camParams.getSupportedPreviewFrameRates()) - frameRateList.add(Integer.toString(frameRate)); - for (Camera.Size imgSize : camParams.getSupportedPreviewSizes()) - resList.add(imgSize.width + "x" + imgSize.height); - - ListPreference frameRatePrefList = findPreference("video_framerate"); - if (frameRatePrefList != null) { - frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); - frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); - } - ListPreference resolutionPrefList = findPreference("video_resolution"); - if (resolutionPrefList != null) { - resolutionPrefList.setEntries(resList.toArray(new String[0])); - resolutionPrefList.setEntryValues(resList.toArray(new String[0])); - } - } catch (Exception e) { - Log.e("SensorsFragment", "Error updating camera settings", e); - } finally { - if (camera != null) camera.release(); - } - } - - - private void setupAudioPreferences() { - List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); - List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192"); - - ListPreference sampleRatePrefList = findPreference("audio_samplerate"); - if (sampleRatePrefList != null) { - sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0])); - sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0])); - } - - ListPreference bitRatePrefList = findPreference("audio_bitrate"); - if (bitRatePrefList != null) { - bitRatePrefList.setEntries(bitRateList.toArray(new String[0])); - bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0])); - } - } - - private BluetoothAdapter getBluetoothAdapter() { - BluetoothManager btManager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE); - return btManager != null ? btManager.getAdapter() : null; - } - - private boolean hasPermission(String permission) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true; - return ActivityCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_GRANTED; - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java deleted file mode 100644 index 3dbc700d..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.sensorhub.android; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.preference.Preference; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.SwitchPreferenceCompat; - -import org.sensorhub.android.server.ServerProfileRepository; -import org.sensorhub.android.server.ServerProfilesActivity; -import org.sensorhub.android.ui.AppPreferencesActivity; - - -/* - * Fragment for settings preferences - */ -public class SettingsFragment extends PreferenceFragmentCompat { - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - setPreferencesFromResource(R.xml.pref_settings, rootKey); - - setupAppPreferences(); - manageServerProfiles(); - setupDiscoveryToggle(); - } - - private void setupAppPreferences() { - Preference appPrefsPref = findPreference("app_preferences"); - if (appPrefsPref != null) { - appPrefsPref.setOnPreferenceClickListener(p -> { - startActivity(new Intent(requireContext(), AppPreferencesActivity.class)); - return true; - }); - } - } - - @Override - public void onResume() { - super.onResume(); - Preference serverPref = findPreference("manage_servers"); - if (serverPref != null) updateServerProfilesSummary(serverPref); - } - - private void manageServerProfiles() { - Preference serverPref = findPreference("manage_servers"); - - if (serverPref != null) { - updateServerProfilesSummary(serverPref); - serverPref.setOnPreferenceClickListener(p -> { - startActivity(new Intent(requireContext(), ServerProfilesActivity.class)); - return true; - }); - } - } - - - private void updateServerProfilesSummary(Preference pref) { - ServerProfileRepository repo = ServerProfileRepository.getInstance(requireContext()); - int total = repo.getAll().size(); - int enabled = repo.getEnabled().size(); - if (total == 0) { - pref.setSummary("No server profiles configured"); - } else { - pref.setSummary(enabled + " of " + total + " server(s) enabled"); - } - } - - private void setupDiscoveryToggle() { - SwitchPreferenceCompat enableDiscovery = findPreference("discovery_service"); - - Preference rules = findPreference("rules_link"); - - if (enableDiscovery != null) { - boolean isDiscovery = enableDiscovery.isChecked(); - setVisibility(isDiscovery, rules); - - enableDiscovery.setOnPreferenceChangeListener((pref, value) -> { - boolean isEnabled = (Boolean) value; - setVisibility(isEnabled, rules); - return true; - }); - } - } - - private void setVisibility(boolean visible, Preference... prefs) { - for (Preference p : prefs) { - if (p != null) p.setVisible(visible); - } - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java b/sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java deleted file mode 100644 index c7110039..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java +++ /dev/null @@ -1,206 +0,0 @@ -package org.sensorhub.android.server; - -import android.os.Bundle; -import android.view.View; -import android.widget.Toast; - -import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.button.MaterialButton; -import com.google.android.material.button.MaterialButtonToggleGroup; -import com.google.android.material.materialswitch.MaterialSwitch; -import com.google.android.material.textfield.TextInputEditText; - -import org.sensorhub.android.R; - -public class EditServerProfileActivity extends AppCompatActivity { - - public static final String EXTRA_PROFILE_ID = "profile_id"; - - private ServerProfileRepository repo; - private ServerProfile profile; - private boolean isEdit; - - private TextInputEditText nameInput, hostInput, portInput, endpointInput; - private TextInputEditText usernameInput, passwordInput; - private TextInputEditText tokenInput, clientIdInput, clientSecretInput; - private MaterialSwitch tlsSwitch, sslSwitch; - private MaterialButtonToggleGroup profileTypeToggle, authModeToggle; - private View basicAuthFields, oauthFields; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_edit_server_profile); - - repo = ServerProfileRepository.getInstance(this); - - String profileId = getIntent().getStringExtra(EXTRA_PROFILE_ID); - isEdit = profileId != null; - profile = isEdit ? repo.getById(profileId) : new ServerProfile(); - - if (isEdit && profile == null) { - Toast.makeText(this, R.string.msg_profile_not_found, Toast.LENGTH_SHORT).show(); - finish(); - return; - } - - setupToolbar(); - bindViews(); - setupListeners(); - if (isEdit) populateFields(); - updateSslVisibility(); - updateEndpointAutofill(); - - MaterialButton saveButton = findViewById(R.id.btn_save); - saveButton.setOnClickListener(v -> saveProfile()); - } - - private void setupToolbar() { - MaterialToolbar toolbar = findViewById(R.id.edit_profile_toolbar); - toolbar.setTitle(isEdit ? getString(R.string.title_edit_server) : getString(R.string.btn_add_server)); - toolbar.setNavigationOnClickListener(v -> finish()); - } - - private void bindViews() { - nameInput = findViewById(R.id.edit_name); - hostInput = findViewById(R.id.edit_host); - portInput = findViewById(R.id.edit_port); - endpointInput = findViewById(R.id.edit_endpoint); - usernameInput = findViewById(R.id.edit_username); - passwordInput = findViewById(R.id.edit_password); - tokenInput = findViewById(R.id.edit_token_endpoint); - clientIdInput = findViewById(R.id.edit_client_id); - clientSecretInput = findViewById(R.id.edit_client_secret); - tlsSwitch = findViewById(R.id.switch_tls); - sslSwitch = findViewById(R.id.switch_disable_ssl); - profileTypeToggle = findViewById(R.id.toggle_profile_type); - authModeToggle = findViewById(R.id.toggle_auth_mode); - basicAuthFields = findViewById(R.id.basic_auth_fields); - oauthFields = findViewById(R.id.oauth_fields); - } - - private void setupListeners() { - authModeToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { - if (isChecked) updateAuthVisibility(); - }); - - tlsSwitch.setOnCheckedChangeListener((btn, checked) -> updateSslVisibility()); - } - - private void updateAuthVisibility() { - int checkedId = authModeToggle.getCheckedButtonId(); - basicAuthFields.setVisibility(checkedId == R.id.btn_auth_basic ? View.VISIBLE : View.GONE); - oauthFields.setVisibility(checkedId == R.id.btn_auth_oauth ? View.VISIBLE : View.GONE); - } - - private void updateSslVisibility() { - sslSwitch.setVisibility(tlsSwitch.isChecked() ? View.VISIBLE : View.GONE); - if (!tlsSwitch.isChecked()) { - sslSwitch.setChecked(false); - } - } - - - private void updateEndpointAutofill() { - profileTypeToggle.addOnButtonCheckedListener((group, checkedId, isChecked) -> { - if (!isChecked) return; - if (checkedId == R.id.btn_type_consys) { - endpointInput.setText("/sensorhub/api"); - } else if (checkedId == R.id.btn_type_sos) { - endpointInput.setText("/sensorhub/sos"); - } - }); - } - private void populateFields() { - nameInput.setText(profile.name); - hostInput.setText(profile.host); - portInput.setText(String.valueOf(profile.port)); - endpointInput.setText(profile.endpointPath); - usernameInput.setText(profile.username); - tlsSwitch.setChecked(profile.enableTls); - sslSwitch.setChecked(profile.disableSslCheck); - updateSslVisibility(); - - // Profile type toggle - profileTypeToggle.check(profile.useConSysClient ? R.id.btn_type_consys : R.id.btn_type_sos); - - // Auth mode toggle - if (profile.oAuthEnabled) { - authModeToggle.check(R.id.btn_auth_oauth); - } else if (profile.username != null && !profile.username.isEmpty()) { - authModeToggle.check(R.id.btn_auth_basic); - } else { - authModeToggle.check(R.id.btn_auth_none); - } - updateAuthVisibility(); - - // Secure fields - passwordInput.setText(repo.getPassword(profile.id)); - tokenInput.setText(repo.getOAuthTokenEndpoint(profile.id)); - clientIdInput.setText(repo.getOAuthClientId(profile.id)); - clientSecretInput.setText(repo.getOAuthClientSecret(profile.id)); - } - - private void saveProfile() { - String name = nameInput.getText().toString().trim(); - String host = hostInput.getText().toString().trim(); - String portStr = portInput.getText().toString().trim(); - String endpoint = endpointInput.getText().toString().trim(); - - if (name.isEmpty() || host.isEmpty() || portStr.isEmpty()) { - Toast.makeText(this, R.string.msg_name_host_port_required, Toast.LENGTH_SHORT).show(); - return; - } - - if (host.contains(" ") || host.contains("://")) { - Toast.makeText(this, R.string.msg_no_protocol, Toast.LENGTH_SHORT).show(); - return; - } - - int port; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - Toast.makeText(this, R.string.msg_port_number, Toast.LENGTH_SHORT).show(); - return; - } - if (port < 1 || port > 65535) { - Toast.makeText(this, R.string.msg_port_range, Toast.LENGTH_SHORT).show(); - return; - } - - if (!endpoint.isEmpty() && !endpoint.startsWith("/")) { - endpoint = "/" + endpoint; - } - - int authCheckedId = authModeToggle.getCheckedButtonId(); - - profile.name = name; - profile.host = host; - profile.port = port; - profile.endpointPath = endpoint; - profile.username = authCheckedId == R.id.btn_auth_basic - ? usernameInput.getText().toString().trim() : ""; - profile.enableTls = tlsSwitch.isChecked(); - profile.disableSslCheck = sslSwitch.isChecked(); - profile.useConSysClient = profileTypeToggle.getCheckedButtonId() == R.id.btn_type_consys; - profile.oAuthEnabled = authCheckedId == R.id.btn_auth_oauth; - - repo.save(profile); - - String password = authCheckedId == R.id.btn_auth_basic - ? passwordInput.getText().toString().trim() : ""; - repo.setPassword(profile.id, password); - - if (profile.oAuthEnabled) { - repo.setOAuthClientId(profile.id, clientIdInput.getText().toString().trim()); - repo.setOAuthClientSecret(profile.id, clientSecretInput.getText().toString().trim()); - repo.setOAuthTokenEndpoint(profile.id, tokenInput.getText().toString().trim()); - } - - setResult(RESULT_OK); - finish(); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java deleted file mode 100644 index d58772b0..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.sensorhub.android.server; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.materialswitch.MaterialSwitch; - -import org.sensorhub.android.R; - -import java.util.List; - -public class ServerAdapter extends RecyclerView.Adapter { - - public interface Listener { - void onEditClicked(ServerProfile profile); - void onEnabledToggled(ServerProfile profile, boolean enabled); - void onDeleteRequested(ServerProfile profile); - } - - private final List servers; - private final Listener listener; - - public ServerAdapter(List servers, Listener listener) { - this.servers = servers; - this.listener = listener; - } - - public static class ViewHolder extends RecyclerView.ViewHolder { - TextView name, summary, mode; - MaterialSwitch enabledSwitch; - ImageButton editButton; - - public ViewHolder(View view) { - super(view); - name = view.findViewById(R.id.profile_name); - summary = view.findViewById(R.id.profile_summary); - mode = view.findViewById(R.id.profile_mode); - enabledSwitch = view.findViewById(R.id.profile_enabled_switch); - editButton = view.findViewById(R.id.btn_edit_profile); - } - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_server_profile, parent, false); - return new ViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, int position) { - ServerProfile p = servers.get(position); - - holder.name.setText(p.name); - holder.summary.setText(p.getDisplaySummary()); - holder.mode.setText(p.getClientModeLabel()); - - holder.enabledSwitch.setOnCheckedChangeListener(null); - holder.enabledSwitch.setChecked(p.enabled); - holder.enabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> - listener.onEnabledToggled(p, isChecked)); - - holder.editButton.setOnClickListener(v -> listener.onEditClicked(p)); - - holder.itemView.setOnLongClickListener(v -> { - listener.onDeleteRequested(p); - return true; - }); - } - - @Override - public int getItemCount() { - return servers.size(); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java deleted file mode 100644 index e1aac451..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java +++ /dev/null @@ -1,95 +0,0 @@ -package org.sensorhub.android.server; - -import android.content.Intent; -import android.os.Bundle; -import android.view.View; -import android.widget.TextView; - -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.appcompat.app.AppCompatActivity; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import org.sensorhub.android.R; - -import java.util.ArrayList; -import java.util.List; - -public class ServerProfilesActivity extends AppCompatActivity implements ServerAdapter.Listener { - - private RecyclerView recyclerView; - private TextView emptyText; - private ServerAdapter adapter; - private final List servers = new ArrayList<>(); - private ServerProfileRepository repo; - - private final ActivityResultLauncher editLauncher = - registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { - refreshList(); - }); - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_server_profiles); - - MaterialToolbar toolbar = findViewById(R.id.server_profiles_toolbar); - setSupportActionBar(toolbar); - toolbar.setNavigationOnClickListener(v -> onBackPressed()); - - repo = ServerProfileRepository.getInstance(this); - - recyclerView = findViewById(R.id.server_list); - emptyText = findViewById(R.id.empty_text); - - FloatingActionButton fab = findViewById(R.id.fab_add_server); - fab.setOnClickListener(v -> { - Intent intent = new Intent(this, EditServerProfileActivity.class); - editLauncher.launch(intent); - }); - - adapter = new ServerAdapter(servers, this); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setAdapter(adapter); - - refreshList(); - } - - @Override - public void onEditClicked(ServerProfile profile) { - Intent intent = new Intent(this, EditServerProfileActivity.class); - intent.putExtra(EditServerProfileActivity.EXTRA_PROFILE_ID, profile.id); - editLauncher.launch(intent); - } - - @Override - public void onEnabledToggled(ServerProfile profile, boolean enabled) { - repo.setEnabled(profile.id, enabled); - } - - @Override - public void onDeleteRequested(ServerProfile profile) { - new MaterialAlertDialogBuilder(this) - .setTitle(R.string.title_delete_server) - .setMessage(getString(R.string.msg_delete_server, profile.name)) - .setPositiveButton(R.string.btn_delete, (d, w) -> { - repo.delete(profile.id); - refreshList(); - }) - .setNegativeButton(R.string.btn_cancel, null) - .show(); - } - - private void refreshList() { - servers.clear(); - servers.addAll(repo.getAll()); - adapter.notifyDataSetChanged(); - emptyText.setVisibility(servers.isEmpty() ? View.VISIBLE : View.GONE); - emptyText.setText(servers.isEmpty() ? getString(R.string.empty_server_profiles) : ""); - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt new file mode 100644 index 00000000..1c007644 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt @@ -0,0 +1,13 @@ +package org.sensorhub.android.ui.screens.dashboard + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceManager + +class HomeViewModel(application: Application) : AndroidViewModel(application) { + private val prefs = PreferenceManager.getDefaultSharedPreferences(application) + + init { + + } +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java similarity index 98% rename from sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java index 79cc8a10..ead10c60 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java @@ -1,4 +1,4 @@ -package org.sensorhub.android.server; +package org.sensorhub.android.ui.screens.profiles; import org.json.JSONException; import org.json.JSONObject; diff --git a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java similarity index 99% rename from sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java rename to sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java index 1d41d9a2..764b7155 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java @@ -1,4 +1,4 @@ -package org.sensorhub.android.server; +package org.sensorhub.android.ui.screens.profiles; import android.content.Context; import android.content.SharedPreferences; From 2159e7a8b4724beaf8c8f5032e3fcb19d0c12eaa Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Jul 2026 16:59:01 -0500 Subject: [PATCH 58/63] add server form contract for state --- .../ui/screens/profiles/ServerFormContract.kt | 20 +++++++++++++++++++ .../profiles/ServerProfilesViewModel.kt | 1 - 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt new file mode 100644 index 00000000..55e62ab4 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt @@ -0,0 +1,20 @@ +package org.sensorhub.android.ui.screens.profiles + +data class ServerFormState( + val serverName: String = "", + val host: String = "", + val port: String = "", + val endpointPath: String = "/sensorhub/api", + val username: String = "", + val password: String = "", + val enableTls: Boolean = false, + val disableSslCheck: Boolean = false, + val enableOAuth: Boolean = false, + val clientId: String = "", + val clientSecret: String = "", + val tokenEndpoint: String = "", + val clientTypeIndex: Int = 0, + val nameError: String? = null, + val hostError: String? = null, + val portError: String? = null, +) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt index 0d2ddd6c..797d39be 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.AndroidViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import org.sensorhub.android.server.ServerProfileRepository class ServerProfilesViewModel(application: Application) : AndroidViewModel(application) { private val repo = ServerProfileRepository.getInstance(application) From 00d3b34a0c6cc265296c53f22be0427eb139aa44 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 30 Jul 2026 09:34:03 -0500 Subject: [PATCH 59/63] remove navcontroller from screens, remove duplicate serverprofile contracts, remove layouts --- .../layout/activity_edit_server_profile.xml | 380 ------------------ .../res/layout/activity_server_profiles.xml | 60 --- .../res/layout/item_server_profile.xml | 73 ---- .../sensorhub/android/ComposeMainActivity.kt | 16 +- .../org/sensorhub/android/MainActivity.java | 8 +- .../android/MainActivityViewModel.kt | 9 + .../android/ui/navigation/OSHNavHost.kt | 17 +- .../screens/appstatus/AppStatusViewModel.kt | 2 +- .../ui/screens/dashboard/HomeScreen.kt | 19 +- .../ui/screens/dashboard/HomeViewModel.kt | 4 + .../preferences/AppPreferencesScreen.kt | 11 +- .../ui/screens/profiles/ServerFormContract.kt | 20 - .../ui/screens/profiles/ServerProfile.java | 8 +- .../screens/profiles/ServerProfileContract.kt | 10 +- .../screens/profiles/ServerProfilesScreen.kt | 6 +- .../ui/screens/sensors/SensorsScreen.kt | 8 +- .../ui/screens/servers/ServersScreen.kt | 12 +- 17 files changed, 90 insertions(+), 573 deletions(-) delete mode 100644 sensorhub-android-app/res/layout/activity_edit_server_profile.xml delete mode 100644 sensorhub-android-app/res/layout/activity_server_profiles.xml delete mode 100644 sensorhub-android-app/res/layout/item_server_profile.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/MainActivityViewModel.kt delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt diff --git a/sensorhub-android-app/res/layout/activity_edit_server_profile.xml b/sensorhub-android-app/res/layout/activity_edit_server_profile.xml deleted file mode 100644 index ed15049d..00000000 --- a/sensorhub-android-app/res/layout/activity_edit_server_profile.xml +++ /dev/null @@ -1,380 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/layout/activity_server_profiles.xml b/sensorhub-android-app/res/layout/activity_server_profiles.xml deleted file mode 100644 index 411d6208..00000000 --- a/sensorhub-android-app/res/layout/activity_server_profiles.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/layout/item_server_profile.xml b/sensorhub-android-app/res/layout/item_server_profile.xml deleted file mode 100644 index 19bdf7df..00000000 --- a/sensorhub-android-app/res/layout/item_server_profile.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt index 009a66e3..196b1066 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController import org.sensorhub.android.ui.navigation.Navbar import org.sensorhub.android.ui.navigation.OSHNavHost @@ -30,6 +31,17 @@ fun MainScreen() { Scaffold( bottomBar = { Navbar(navController = navController) } ) { padding -> - OSHNavHost(navController = navController, modifier = Modifier.padding(padding)) + OSHNavHost( + navController = navController, + modifier = Modifier.padding(padding) + ) } -} \ No newline at end of file +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun MainScreenPreview() { + OSHTheme { + MainScreen() + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 831260a2..a11fbed2 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -445,7 +445,7 @@ public boolean verify(String arg0, SSLSession arg1) { for (ServerProfile sp : enabledServers) { URL profileUrl = sp.buildClientUrl(); if (profileUrl == null) { - log.error("Skipping server profile '{}': invalid URL", sp.name); + log.error("Skipping server profile '{}': invalid URL", sp.serverName); continue; } @@ -540,7 +540,7 @@ protected void addSosTConfig(SensorConfig sensorConf, ServerProfile profile, URL { SOSTClientConfig sosConfig = new SOSTClientConfig(); sosConfig.id = sensorConf.id + "_SOST_" + profile.id; - sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; + sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.serverName; sosConfig.autoStart = true; sosConfig.sos.remoteHost = serverUrl.getHost(); sosConfig.sos.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); @@ -560,7 +560,7 @@ protected void addCSApiConfig(SensorConfig sensorConf, ServerProfile profile, UR { ConSysApiClientConfig consysConfig = new ConSysApiClientConfig(); consysConfig.id = sensorConf.id + "_CONSYS_" + profile.id; - consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; + consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.serverName; consysConfig.autoStart = true; consysConfig.conSys.remoteHost = serverUrl.getHost(); consysConfig.conSys.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); @@ -709,8 +709,6 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) { return super.dispatchGenericMotionEvent(event); } - - boolean isAnySensorEnabled(SharedPreferences prefs) { return prefs.getBoolean("accel_enabled", false) || prefs.getBoolean("gyro_enabled", false) diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivityViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/MainActivityViewModel.kt new file mode 100644 index 00000000..0e6d296c --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivityViewModel.kt @@ -0,0 +1,9 @@ +package org.sensorhub.android + +import android.app.Application +import androidx.lifecycle.AndroidViewModel + +class MainActivityViewModel(application: Application) : AndroidViewModel(application) { + + +} \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt index 75aedb02..99883b7a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt @@ -30,13 +30,20 @@ fun OSHNavHost( modifier = modifier ) { composable(Screen.Dashboard.route) { - HomeScreen(navController = navController) + HomeScreen( + onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) } + ) } composable(Screen.Sensors.route) { - SensorsScreen(navController = navController) + SensorsScreen( + onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) } + ) } composable(Screen.Servers.route) { - ServersScreen(navController = navController) + ServersScreen( + onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) }, + onNavigateToServerProfiles = { navController.navigate(Screen.ServerProfiles.route) } + ) } composable(Screen.AppStatus.route) { AppStatusScreen( @@ -59,7 +66,9 @@ fun OSHNavHost( composable(Screen.AppPreferences.route) { AppPreferencesScreen( - onBackClick = { navController.popBackStack() } + onBackClick = { navController.popBackStack() }, + onNavigateToAppStatus = { navController.navigate(Screen.AppStatus.route) }, + onNavigateToHelpFaq = { navController.navigate(Screen.HelpFaq.route) } ) } composable(Screen.ServerProfiles.route) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt index e68abdcf..2ca838dd 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/appstatus/AppStatusViewModel.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.launch import org.sensorhub.android.SensorHubService import org.sensorhub.api.module.ModuleConfig -class AppStatusViewModel(application: Application): AndroidViewModel(application) { +class AppStatusViewModel(application: Application) : AndroidViewModel(application) { private val _state = MutableStateFlow(AppStatusState()) val state: StateFlow = _state.asStateFlow() private var boundService: SensorHubService? = null diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt index 623bfa6f..75f94961 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt @@ -12,22 +12,27 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.MainScreen import org.sensorhub.android.R import org.sensorhub.android.ui.navigation.Screen import org.sensorhub.android.ui.components.OSHTopAppBarWithLogo +import org.sensorhub.android.ui.theme.OSHTheme @Composable -fun HomeScreen(navController: NavController = rememberNavController()) { +fun HomeScreen( + onNavigateToAppPreferences : () -> Unit, +) { Scaffold( topBar = { OSHTopAppBarWithLogo( title = stringResource(R.string.app_name), actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route)}) { + IconButton(onClick = { onNavigateToAppPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" @@ -45,4 +50,14 @@ fun HomeScreen(navController: NavController = rememberNavController()) { } } +} + +@Preview(showBackground = true, backgroundColor = 0xFF121212) +@Composable +private fun HomeScreenPreview() { + OSHTheme { + HomeScreen( + {} + ) + } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt index 1c007644..3a7dcf97 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeViewModel.kt @@ -10,4 +10,8 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) { init { } + + override fun onCleared() { + super.onCleared() + } } \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt index 76963b70..25d02b6b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/preferences/AppPreferencesScreen.kt @@ -1,6 +1,5 @@ package org.sensorhub.android.ui.screens.preferences -import android.content.Intent import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height @@ -19,14 +18,11 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import org.sensorhub.android.ui.AppStatusActivity -import org.sensorhub.android.ui.HelpFaqActivity import org.sensorhub.android.R import org.sensorhub.android.ui.components.OSHAlertDialogWithoutDismiss import org.sensorhub.android.ui.components.OSHCard @@ -41,9 +37,10 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun AppPreferencesScreen( onBackClick: () -> Unit, + onNavigateToAppStatus: () -> Unit, + onNavigateToHelpFaq: () -> Unit, viewModel: AppPreferencesViewModel = viewModel() ) { - val context = LocalContext.current val state by viewModel.state.collectAsStateWithLifecycle() var showLanguageDialog by remember { mutableStateOf(false) } @@ -149,7 +146,7 @@ fun AppPreferencesScreen( title = stringResource(R.string.app_status_main_fragment), imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.app_status_main_fragment), - onClick = { context.startActivity(Intent(context, AppStatusActivity::class.java)) } + onClick = { onNavigateToAppStatus} ) OSHClickableRowWithIcon( @@ -163,7 +160,7 @@ fun AppPreferencesScreen( title = stringResource(R.string.title_help_faq), imageVector = Icons.Default.Help, contentDescription = stringResource(R.string.title_help_faq), - onClick = { context.startActivity(Intent(context, HelpFaqActivity::class.java)) } + onClick = { onNavigateToHelpFaq } ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt deleted file mode 100644 index 55e62ab4..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormContract.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.sensorhub.android.ui.screens.profiles - -data class ServerFormState( - val serverName: String = "", - val host: String = "", - val port: String = "", - val endpointPath: String = "/sensorhub/api", - val username: String = "", - val password: String = "", - val enableTls: Boolean = false, - val disableSslCheck: Boolean = false, - val enableOAuth: Boolean = false, - val clientId: String = "", - val clientSecret: String = "", - val tokenEndpoint: String = "", - val clientTypeIndex: Int = 0, - val nameError: String? = null, - val hostError: String? = null, - val portError: String? = null, -) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java index ead10c60..7e896754 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java @@ -11,7 +11,7 @@ public class ServerProfile { public String id; - public String name; + public String serverName; public String host; public int port; public String endpointPath; @@ -28,7 +28,7 @@ public class ServerProfile { public ServerProfile() { this.id = UUID.randomUUID().toString(); - this.name = "Local Server"; + this.serverName = "Local Server"; this.host = "127.0.0.1"; this.port = 8080; this.endpointPath = "/sensorhub/api"; @@ -43,7 +43,7 @@ public ServerProfile() { public JSONObject toJson() throws JSONException { JSONObject obj = new JSONObject(); obj.put("id", id); - obj.put("name", name); + obj.put("serverName", serverName); obj.put("host", host); obj.put("port", port); obj.put("endpointPath", endpointPath); @@ -59,7 +59,7 @@ public JSONObject toJson() throws JSONException { public static ServerProfile fromJson(JSONObject obj) throws JSONException { ServerProfile p = new ServerProfile(); p.id = obj.getString("id"); - p.name = obj.optString("name", ""); + p.serverName = obj.optString("serverName", ""); p.host = obj.optString("host", "127.0.0.1"); p.port = obj.optInt("port", 8080); p.endpointPath = obj.optString("endpointPath", "/sensorhub/api"); diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt index 4cd2be5f..0e0212e6 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt @@ -2,18 +2,18 @@ package org.sensorhub.android.ui.screens.profiles data class ServerProfileItem( val id: String = "", - val name: String = "", + val serverName: String = "", val host: String = "", - val port: Int = 8181, + val port: String = "", val endpointPath: String = "/sensorhub/api", val username: String = "", val password: String = "", val enableTls: Boolean = false, val disableSslCheck: Boolean = false, val useConSysClient: Boolean = false, - val oAuthEnabled: Boolean = false, - val enabled: Boolean = false, + val enableOAuth: Boolean = false, val clientId: String = "", val clientSecret: String = "", - val tokenEndpoint: String = "" + val tokenEndpoint: String = "", + val enabled: Boolean = false ) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt index c54d5592..4147c9fe 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt @@ -67,7 +67,7 @@ fun ServerProfilesScreen( profileToDelete = null }, dialogTitle = stringResource(R.string.title_delete_server), - dialogText = stringResource(R.string.msg_delete_server, profile.name), + dialogText = stringResource(R.string.msg_delete_server, profile.serverName), icon = Icons.Filled.Info ) } @@ -108,13 +108,13 @@ fun ServerProfilesScreen( ) { items(profiles, key = { it.id }) { item -> OSHActionCard( - title = item.name, + title = item.serverName, subtitle = "${if (item.enableTls) "https" else "http"}://${item.host}:${item.port}${item.endpointPath}", checked = item.enabled, onCheckedChange = { enabled -> viewModel.setEnabled(item.id, enabled) }, onClick = { navController.navigate(Screen.ServerForm.createRoute(item.id)) }, imageVector = Icons.Filled.Edit, - contentDescription = "Edit ${item.name}" + contentDescription = "Edit ${item.serverName}" ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt index c6c09f5e..554ee8a4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt @@ -57,7 +57,7 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun SensorsScreen( - navController: NavController = rememberNavController(), + onNavigateToAppPreferences : () -> Unit, viewModel: SensorsViewModel = viewModel(), ) { var selectedCategories by remember { mutableStateOf(SensorCategory.entries.toSet()) } @@ -122,7 +122,7 @@ fun SensorsScreen( OSHTopAppBarWithLogo( title = stringResource(R.string.tab_sensors), actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { + IconButton(onClick = { onNavigateToAppPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = null, @@ -397,6 +397,8 @@ private fun TruPulseConfig( @Composable private fun SensorsScreenPreview() { OSHTheme { - SensorsScreen() + SensorsScreen( + {} + ) } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt index aba3cfad..118a3267 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt @@ -30,7 +30,8 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServersScreen( - navController: NavController = rememberNavController(), + onNavigateToAppPreferences : () -> Unit, + onNavigateToServerProfiles : () -> Unit, viewModel: ServersViewModel = viewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -40,7 +41,7 @@ fun ServersScreen( OSHTopAppBarWithLogo( title = stringResource(R.string.tab_servers), actions = { - IconButton(onClick = { navController.navigate(Screen.AppPreferences.route) }) { + IconButton(onClick = { onNavigateToAppPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" @@ -61,7 +62,7 @@ fun ServersScreen( title = stringResource(R.string.manage_servers), imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.manage_servers), - onClick = { navController.navigate(Screen.ServerProfiles.route) }, + onClick = { onNavigateToServerProfiles }, ) OSHCard { @@ -92,6 +93,9 @@ fun ServersScreen( @Composable private fun ServersScreenPreview() { OSHTheme { - ServersScreen() + ServersScreen( + {}, + {} + ) } } From d4d702e88c3a0b66137ea4bd50af29c311df7e41 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 30 Jul 2026 09:36:14 -0500 Subject: [PATCH 60/63] function name updates --- .../src/org/sensorhub/android/ui/navigation/OSHNavHost.kt | 8 ++++---- .../sensorhub/android/ui/screens/dashboard/HomeScreen.kt | 4 ++-- .../sensorhub/android/ui/screens/sensors/SensorsScreen.kt | 4 ++-- .../sensorhub/android/ui/screens/servers/ServersScreen.kt | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt index 99883b7a..8f333222 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/navigation/OSHNavHost.kt @@ -31,18 +31,18 @@ fun OSHNavHost( ) { composable(Screen.Dashboard.route) { HomeScreen( - onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) } + onNavigateToPreferences = { navController.navigate(Screen.AppPreferences.route) } ) } composable(Screen.Sensors.route) { SensorsScreen( - onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) } + onNavigateToPreferences = { navController.navigate(Screen.AppPreferences.route) } ) } composable(Screen.Servers.route) { ServersScreen( - onNavigateToAppPreferences = { navController.navigate(Screen.AppPreferences.route) }, - onNavigateToServerProfiles = { navController.navigate(Screen.ServerProfiles.route) } + onNavigateToPreferences = { navController.navigate(Screen.AppPreferences.route) }, + onNavigateToProfiles = { navController.navigate(Screen.ServerProfiles.route) } ) } composable(Screen.AppStatus.route) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt index 75f94961..cc61676b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/dashboard/HomeScreen.kt @@ -25,14 +25,14 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun HomeScreen( - onNavigateToAppPreferences : () -> Unit, + onNavigateToPreferences : () -> Unit, ) { Scaffold( topBar = { OSHTopAppBarWithLogo( title = stringResource(R.string.app_name), actions = { - IconButton(onClick = { onNavigateToAppPreferences }) { + IconButton(onClick = { onNavigateToPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt index 554ee8a4..bfd0c93e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/sensors/SensorsScreen.kt @@ -57,7 +57,7 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun SensorsScreen( - onNavigateToAppPreferences : () -> Unit, + onNavigateToPreferences : () -> Unit, viewModel: SensorsViewModel = viewModel(), ) { var selectedCategories by remember { mutableStateOf(SensorCategory.entries.toSet()) } @@ -122,7 +122,7 @@ fun SensorsScreen( OSHTopAppBarWithLogo( title = stringResource(R.string.tab_sensors), actions = { - IconButton(onClick = { onNavigateToAppPreferences }) { + IconButton(onClick = { onNavigateToPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = null, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt index 118a3267..d963f104 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/servers/ServersScreen.kt @@ -30,8 +30,8 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServersScreen( - onNavigateToAppPreferences : () -> Unit, - onNavigateToServerProfiles : () -> Unit, + onNavigateToPreferences : () -> Unit, + onNavigateToProfiles : () -> Unit, viewModel: ServersViewModel = viewModel() ) { val state by viewModel.state.collectAsStateWithLifecycle() @@ -41,7 +41,7 @@ fun ServersScreen( OSHTopAppBarWithLogo( title = stringResource(R.string.tab_servers), actions = { - IconButton(onClick = { onNavigateToAppPreferences }) { + IconButton(onClick = { onNavigateToPreferences }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = "" @@ -62,7 +62,7 @@ fun ServersScreen( title = stringResource(R.string.manage_servers), imageVector = Icons.Default.Cloud, contentDescription = stringResource(R.string.manage_servers), - onClick = { onNavigateToServerProfiles }, + onClick = { onNavigateToProfiles }, ) OSHCard { From 0536adb605132aeab877c020c21063030e2c819a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 30 Jul 2026 11:20:00 -0500 Subject: [PATCH 61/63] moved ServerProfile to serverprofilecontract and updated screens and view model calls --- .../ui/screens/profiles/ServerProfile.java | 101 ------------------ .../screens/profiles/ServerProfileContract.kt | 68 ++++++++++-- .../profiles/ServerProfileRepository.java | 49 ++++----- .../screens/profiles/ServerProfilesScreen.kt | 2 +- .../profiles/ServerProfilesViewModel.kt | 16 +-- submodules/osh-core | 2 +- 6 files changed, 87 insertions(+), 151 deletions(-) delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java deleted file mode 100644 index 7e896754..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfile.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.sensorhub.android.ui.screens.profiles; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.util.UUID; - -public class ServerProfile { - public String id; - public String serverName; - public String host; - public int port; - public String endpointPath; - public String username; - public boolean enableTls; - public boolean disableSslCheck; - public boolean useConSysClient; - public boolean oAuthEnabled; - public boolean enabled; - public String password; - public String clientId; - public String clientSecret; - public String tokenEndpoint; - - public ServerProfile() { - this.id = UUID.randomUUID().toString(); - this.serverName = "Local Server"; - this.host = "127.0.0.1"; - this.port = 8080; - this.endpointPath = "/sensorhub/api"; - this.username = ""; - this.enableTls = false; - this.disableSslCheck = false; - this.useConSysClient = true; - this.oAuthEnabled = false; - this.enabled = true; - } - - public JSONObject toJson() throws JSONException { - JSONObject obj = new JSONObject(); - obj.put("id", id); - obj.put("serverName", serverName); - obj.put("host", host); - obj.put("port", port); - obj.put("endpointPath", endpointPath); - obj.put("username", username); - obj.put("enableTls", enableTls); - obj.put("disableSslCheck", disableSslCheck); - obj.put("useConSysClient", useConSysClient); - obj.put("oAuthEnabled", oAuthEnabled); - obj.put("enabled", enabled); - return obj; - } - - public static ServerProfile fromJson(JSONObject obj) throws JSONException { - ServerProfile p = new ServerProfile(); - p.id = obj.getString("id"); - p.serverName = obj.optString("serverName", ""); - p.host = obj.optString("host", "127.0.0.1"); - p.port = obj.optInt("port", 8080); - p.endpointPath = obj.optString("endpointPath", "/sensorhub/api"); - p.username = obj.optString("username", ""); - p.enableTls = obj.optBoolean("enableTls", false); - p.disableSslCheck = obj.optBoolean("disableSslCheck", false); - p.useConSysClient = obj.optBoolean("useConSysClient", true); - p.oAuthEnabled = obj.optBoolean("oAuthEnabled", false); - p.enabled = obj.optBoolean("enabled", true); - return p; - } - - public URL buildClientUrl() { - String cleanHost = host.replace("http://", "").replace("https://", "").trim(); - if (cleanHost.isEmpty()) - cleanHost = "127.0.0.1"; - - String path = endpointPath != null ? endpointPath.trim() : ""; - if (!path.isEmpty() && !path.startsWith("/")) { - path = "/" + path; - } - - - String urlStr = (enableTls ? "https://" : "http://") + cleanHost + ":" + port + path; - try { - return new URI(urlStr).toURL(); - } catch (URISyntaxException | MalformedURLException e) { - return null; - } - } - - public String getDisplaySummary() { - return (enableTls ? "https://" : "http://") + host + ":" + port + (endpointPath != null ? endpointPath : ""); - } - - public String getClientModeLabel() { - return useConSysClient ? "Connected Systems" : "SOS-T"; - } -} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt index 0e0212e6..1ea0234a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileContract.kt @@ -1,19 +1,73 @@ package org.sensorhub.android.ui.screens.profiles +import org.json.JSONObject +import java.net.URI +import java.net.URL +import java.util.UUID + data class ServerProfileItem( - val id: String = "", - val serverName: String = "", - val host: String = "", - val port: String = "", + val id: String = UUID.randomUUID().toString(), + val serverName: String = "Local Server", + val host: String = "127.0.0.1", + val port: Int = 8080, val endpointPath: String = "/sensorhub/api", val username: String = "", val password: String = "", val enableTls: Boolean = false, val disableSslCheck: Boolean = false, - val useConSysClient: Boolean = false, + val useConSysClient: Boolean = true, val enableOAuth: Boolean = false, val clientId: String = "", val clientSecret: String = "", val tokenEndpoint: String = "", - val enabled: Boolean = false -) + val enabled: Boolean = true, +) { + fun toJson(): JSONObject = JSONObject().apply { + put("id", id) + put("serverName", serverName) + put("host", host) + put("port", port) + put("endpointPath", endpointPath) + put("username", username) + put("enableTls", enableTls) + put("disableSslCheck", disableSslCheck) + put("useConSysClient", useConSysClient) + put("enableOAuth", enableOAuth) + put("enabled", enabled) + } + + fun buildClientUrl(): URL? { + var cleanHost = host.replace("http://", "").replace("https://", "").trim() + if (cleanHost.isEmpty()) cleanHost = "127.0.0.1" + + var path = endpointPath.trim() + if (path.isNotEmpty() && !path.startsWith("/")) path = "/$path" + + val urlStr = "${if (enableTls) "https" else "http"}://$cleanHost:$port$path" + return try { URI(urlStr).toURL() } catch (_: Exception) { null } + } + + fun getDisplaySummary(): String = + "${if (enableTls) "https" else "http"}://$host:$port$endpointPath" + + fun getClientModeLabel(): String = + if (useConSysClient) "Connected Systems" else "SOS-T" + + fun withEnabled(value: Boolean): ServerProfileItem = copy(enabled = value) + + companion object { + fun fromJson(obj: JSONObject): ServerProfileItem = ServerProfileItem( + id = obj.getString("id"), + serverName = obj.optString("serverName", ""), + host = obj.optString("host", "127.0.0.1"), + port = obj.optInt("port", 8080), + endpointPath = obj.optString("endpointPath", "/sensorhub/api"), + username = obj.optString("username", ""), + enableTls = obj.optBoolean("enableTls", false), + disableSslCheck = obj.optBoolean("disableSslCheck", false), + useConSysClient = obj.optBoolean("useConSysClient", true), + enableOAuth = obj.optBoolean("enableOAuth", false), + enabled = obj.optBoolean("enabled", true), + ) + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java index 764b7155..0490e2b0 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfileRepository.java @@ -8,6 +8,7 @@ import org.json.JSONArray; import org.json.JSONException; +import org.json.JSONObject; import org.sensorhub.android.SecurePrefs; import java.util.ArrayList; @@ -21,7 +22,7 @@ public class ServerProfileRepository implements SharedPreferences.OnSharedPrefer private final Context context; private final SharedPreferences prefs; - private volatile List cachedProfiles; + private volatile List cachedProfiles; private ServerProfileRepository(@NonNull Context context) { this.context = context.getApplicationContext(); @@ -47,17 +48,17 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin } } - public List getAll() { - List cached = cachedProfiles; + public List getAll() { + List cached = cachedProfiles; if (cached != null) return Collections.unmodifiableList(cached); - List profiles = new ArrayList<>(); + List profiles = new ArrayList<>(); String json = prefs.getString(KEY_PROFILES_JSON, null); if (json != null) { try { JSONArray arr = new JSONArray(json); for (int i = 0; i < arr.length(); i++) { - profiles.add(ServerProfile.fromJson(arr.getJSONObject(i))); + profiles.add(ServerProfileItem.Companion.fromJson(arr.getJSONObject(i))); } } catch (JSONException e) { // corrupted data, return empty @@ -67,26 +68,26 @@ public List getAll() { return Collections.unmodifiableList(profiles); } - public List getEnabled() { - List enabled = new ArrayList<>(); - for (ServerProfile p : getAll()) { - if (p.enabled) enabled.add(p); + public List getEnabled() { + List enabled = new ArrayList<>(); + for (ServerProfileItem p : getAll()) { + if (p.getEnabled()) enabled.add(p); } return enabled; } - public ServerProfile getById(String id) { - for (ServerProfile p : getAll()) { - if (p.id.equals(id)) return p; + public ServerProfileItem getById(String id) { + for (ServerProfileItem p : getAll()) { + if (p.getId().equals(id)) return p; } return null; } - public void save(ServerProfile profile) { - List all = new ArrayList<>(getAll()); + public void save(ServerProfileItem profile) { + List all = new ArrayList<>(getAll()); boolean found = false; for (int i = 0; i < all.size(); i++) { - if (all.get(i).id.equals(profile.id)) { + if (all.get(i).getId().equals(profile.getId())) { all.set(i, profile); found = true; break; @@ -97,17 +98,16 @@ public void save(ServerProfile profile) { } public void delete(String id) { - List all = new ArrayList<>(getAll()); - all.removeIf(p -> p.id.equals(id)); + List all = new ArrayList<>(getAll()); + all.removeIf(p -> p.getId().equals(id)); persist(all); SecurePrefs.removeByPrefix(context, "profile_" + id + "_"); } public void setEnabled(String id, boolean enabled) { - ServerProfile p = getById(id); + ServerProfileItem p = getById(id); if (p != null) { - p.enabled = enabled; - save(p); + save(p.withEnabled(enabled)); } } @@ -143,13 +143,10 @@ public void setOAuthClientSecret(String profileId, String value) { SecurePrefs.put(context, "profile_" + profileId + "_oauth_client_secret", value); } - private void persist(List profiles) { + private void persist(List profiles) { JSONArray arr = new JSONArray(); - for (ServerProfile p : profiles) { - try { - arr.put(p.toJson()); - } catch (JSONException ignored) { - } + for (ServerProfileItem p : profiles) { + arr.put(p.toJson()); } prefs.edit().putString(KEY_PROFILES_JSON, arr.toString()).apply(); cachedProfiles = profiles; diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt index 4147c9fe..56861656 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesScreen.kt @@ -109,7 +109,7 @@ fun ServerProfilesScreen( items(profiles, key = { it.id }) { item -> OSHActionCard( title = item.serverName, - subtitle = "${if (item.enableTls) "https" else "http"}://${item.host}:${item.port}${item.endpointPath}", + subtitle = item.getDisplaySummary(), checked = item.enabled, onCheckedChange = { enabled -> viewModel.setEnabled(item.id, enabled) }, onClick = { navController.navigate(Screen.ServerForm.createRoute(item.id)) }, diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt index 797d39be..67c2c136 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerProfilesViewModel.kt @@ -16,21 +16,7 @@ class ServerProfilesViewModel(application: Application) : AndroidViewModel(appli } fun refresh() { - _profiles.value = repo.all.map { p -> - ServerProfileItem( - id = p.id, - name = p.name, - host = p.host, - port = p.port, - endpointPath = p.endpointPath, - username = p.username, - enableTls = p.enableTls, - disableSslCheck = p.disableSslCheck, - useConSysClient = p.useConSysClient, - oAuthEnabled = p.oAuthEnabled, - enabled = p.enabled - ) - } + _profiles.value = repo.all } fun setEnabled(id: String, enabled: Boolean) { diff --git a/submodules/osh-core b/submodules/osh-core index a413b4d1..9b747363 160000 --- a/submodules/osh-core +++ b/submodules/osh-core @@ -1 +1 @@ -Subproject commit a413b4d19c5ec00d6bdef84305d9dd9992bc1a15 +Subproject commit 9b747363b13b60b93c83f8ab499b8a022d401288 From 650b4148801609676393c79f7af5cf4f77df2fc8 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 30 Jul 2026 12:56:54 -0500 Subject: [PATCH 62/63] add stubs to compose main activity, and added form view model for servers --- .../res/xml/pref_settings.xml | 61 ------- .../sensorhub/android/ComposeMainActivity.kt | 103 +++++++++++- .../screens/profiles/ServerFormViewModel.kt | 156 ++++++++++++++++++ 3 files changed, 258 insertions(+), 62 deletions(-) delete mode 100644 sensorhub-android-app/res/xml/pref_settings.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormViewModel.kt diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml deleted file mode 100644 index 5b416205..00000000 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt index 196b1066..4e3ad6c9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ComposeMainActivity.kt @@ -1,6 +1,13 @@ package org.sensorhub.android +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.ServiceConnection +import android.content.SharedPreferences import android.os.Bundle +import android.os.IBinder +import android.provider.Settings +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.padding @@ -9,11 +16,49 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.compose.rememberNavController +import org.sensorhub.android.SensorHubService.LocalBinder import org.sensorhub.android.ui.navigation.Navbar import org.sensorhub.android.ui.navigation.OSHNavHost import org.sensorhub.android.ui.theme.OSHTheme +import org.sensorhub.api.module.IModuleConfigRepository +import org.sensorhub.impl.client.sost.SOSTClient +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver +import org.sensorhub.impl.service.consys.client.ConSysApiClientModule +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Instant +import java.util.Date +import java.util.concurrent.CopyOnWriteArrayList + +class ComposeMainActivity: ComponentActivity(), SensorHubServiceProvider { + val ACTION_BROADCAST_RECEIVER: String = "org.sensorhub.android.BROADCAST_RECEIVER" + val ANDROID_SENSORS_MODULE_ID: String = "ANDROID_SENSORS" + val ANDROID_SENSORS_LAST_UPDATED: Date = Date(Instant.now().toEpochMilli()) + val log: Logger = LoggerFactory.getLogger(MainActivity::class.java) + + var boundService: SensorHubService? = null; + var sensorhubConfig: IModuleConfigRepository? = null + var oshStarted: Boolean = false + var showVideo: Boolean = false + var deviceID: String? = null + var runName: String? = null + private var broadcastReceiver: BroadcastReceiver? = null + + var sostClients: CopyOnWriteArrayList = CopyOnWriteArrayList() + var conSysClients: CopyOnWriteArrayList = CopyOnWriteArrayList() + var androidSensors: AndroidSensorsDriver? = null + + + private val sConn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + boundService = (service as LocalBinder).getService() + } + + override fun onServiceDisconnected(name: ComponentName?) { + boundService = null + } + } -class ComposeMainActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { @@ -22,6 +67,62 @@ class ComposeMainActivity: ComponentActivity() { } } } + + override fun getBoundService(): SensorHubService? { + return boundService + } + + override fun isOshStarted(): Boolean { + return oshStarted + } + + override fun setOshStarted(started: Boolean) { + this.oshStarted = started + } + + override fun getSensorhubConfig(): IModuleConfigRepository? { + return sensorhubConfig + } + + override fun getSostClients(): List? { + return sostClients + } + + override fun getConSysClients(): List? { + return conSysClients + } + + override fun getAndroidSensors(): AndroidSensorsDriver? { + return androidSensors + } + + override fun setAndroidSensors(driver: AndroidSensorsDriver?) { + this.androidSensors = driver + } + + override fun getShowVideo(): Boolean { + return showVideo + } + + override fun updateConfig(prefs: SharedPreferences?, runName: String?) { + deviceID = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) + + } + + override fun startSensorHub() { + if (boundService != null && sensorhubConfig != null) + boundService?.startSensorHub(sensorhubConfig, showVideo) + } + + override fun stopSensorHub() { + sostClients.clear() + conSysClients.clear() + if (boundService != null) + boundService?.stopSensorHub() + oshStarted = false + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } @Composable diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormViewModel.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormViewModel.kt new file mode 100644 index 00000000..91a7f628 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormViewModel.kt @@ -0,0 +1,156 @@ +package org.sensorhub.android.ui.screens.profiles + +import android.app.Application +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel + +class ServerFormViewModel(application: Application) : AndroidViewModel(application) { + + private val repo = ServerProfileRepository.getInstance(application) + + var state by mutableStateOf(ServerProfileItem()) + private set + + var portText by mutableStateOf("") + private set + + var nameError by mutableStateOf(null) + private set + var hostError by mutableStateOf(null) + private set + var portError by mutableStateOf(null) + private set + + private var existingProfileId: String? = null + var isEdit = false + private set + + fun loadProfile(profileId: String?) { + isEdit = profileId != null && profileId != "new" + if (!isEdit) return + + val profile = repo.getById(profileId) ?: return + existingProfileId = profile.id + state = profile.copy( + password = repo.getPassword(profile.id), + clientId = repo.getOAuthClientId(profile.id), + clientSecret = repo.getOAuthClientSecret(profile.id), + tokenEndpoint = repo.getOAuthTokenEndpoint(profile.id), + ) + portText = profile.port.toString() + } + + fun updateServerName(value: String) { + state = state.copy(serverName = value) + nameError = null + } + + fun updateHost(value: String) { + state = state.copy(host = value) + hostError = null + } + + fun updatePort(value: String) { + portText = value + portError = null + } + + fun updateEndpointPath(value: String) { + state = state.copy(endpointPath = value) + } + + fun updateUsername(value: String) { + state = state.copy(username = value) + } + + fun updatePassword(value: String) { + state = state.copy(password = value) + } + + fun updateEnableTls(value: Boolean) { + state = state.copy(enableTls = value, disableSslCheck = if (!value) false else state.disableSslCheck) + } + + fun updateDisableSslCheck(value: Boolean) { + state = state.copy(disableSslCheck = value) + } + + fun updateEnableOAuth(value: Boolean) { + state = state.copy(enableOAuth = value) + } + + fun updateClientId(value: String) { + state = state.copy(clientId = value) + } + + fun updateClientSecret(value: String) { + state = state.copy(clientSecret = value) + } + + fun updateTokenEndpoint(value: String) { + state = state.copy(tokenEndpoint = value) + } + + fun updateUseConSysClient(useConSys: Boolean) { + val path = if (useConSys) "/sensorhub/api" else "/sensorhub/sos" + state = state.copy(useConSysClient = useConSys, endpointPath = path) + } + + fun validate(): Boolean { + var valid = true + + if (state.serverName.isBlank()) { valid = false; } else null + + if (state.host.isBlank()) { + valid = false; + } else if (state.host.contains(" ") || state.host.contains("://")) { + valid = false; + } else null + + if (portText.isBlank()) { + valid = false; + } else { + val portNum = portText.toIntOrNull() + if (portNum == null) { + valid = false; + } + else if (portNum < 1 || portNum > 65535) { + valid = false; + } + else null + } + + return valid + } + + fun saveProfile(): Boolean { + if (!validate()) return false + + var ep = state.endpointPath.trim() + if (ep.isNotEmpty() && !ep.startsWith("/")) ep = "/$ep" + + val profile = state.copy( + id = existingProfileId ?: state.id, + serverName = state.serverName.trim(), + host = state.host.trim(), + port = portText.toInt(), + endpointPath = ep, + username = if (!state.enableOAuth) state.username.trim() else "", + ) + + repo.save(profile) + + val pwd = if (!state.enableOAuth) state.password.trim() else "" + repo.setPassword(profile.id, pwd) + + if (state.enableOAuth) { + repo.setOAuthClientId(profile.id, state.clientId.trim()) + repo.setOAuthClientSecret(profile.id, state.clientSecret.trim()) + repo.setOAuthTokenEndpoint(profile.id, state.tokenEndpoint.trim()) + } + + return true + } +} From 4394371027e7811e0c0a080b0b985260470b4537 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 30 Jul 2026 12:57:29 -0500 Subject: [PATCH 63/63] cleaned up server form screen to use viewModel --- .../ui/screens/profiles/ServerFormScreen.kt | 171 +++++------------- 1 file changed, 45 insertions(+), 126 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt index 833eb82e..38829652 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt +++ b/sensorhub-android-app/src/org/sensorhub/android/ui/screens/profiles/ServerFormScreen.kt @@ -18,13 +18,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -32,9 +31,8 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import org.sensorhub.android.R -import org.sensorhub.android.server.ServerProfile -import org.sensorhub.android.server.ServerProfileRepository import org.sensorhub.android.ui.components.OSHButton import org.sensorhub.android.ui.components.OSHInputField import org.sensorhub.android.ui.components.OSHSegmentedButton @@ -46,42 +44,14 @@ import org.sensorhub.android.ui.theme.OSHTheme @Composable fun ServerFormScreen( onBackClick: () -> Unit, - profileId: String? = null + profileId: String? = null, + viewModel: ServerFormViewModel = viewModel() ) { - val context = LocalContext.current - val repo = remember { ServerProfileRepository.getInstance(context) } - val isEdit = profileId != null && profileId != "new" - val existingProfile = remember { if (isEdit) repo.getById(profileId) else null } - - var serverName by rememberSaveable { mutableStateOf(existingProfile?.name ?: "") } - var host by rememberSaveable { mutableStateOf(existingProfile?.host ?: "") } - var port by rememberSaveable { mutableStateOf(existingProfile?.port?.toString() ?: "") } - var endpointPath by rememberSaveable { mutableStateOf(existingProfile?.endpointPath ?: "/sensorhub/api") } - var username by rememberSaveable { mutableStateOf(existingProfile?.username ?: "") } - var password by rememberSaveable { - mutableStateOf(if (isEdit && existingProfile != null) repo.getPassword(existingProfile.id) else "") - } - var enableTls by rememberSaveable { mutableStateOf(existingProfile?.enableTls ?: false) } - var disableSslCheck by rememberSaveable { mutableStateOf(existingProfile?.disableSslCheck ?: false) } - var enableOAuth by rememberSaveable { mutableStateOf(existingProfile?.oAuthEnabled ?: false) } - var clientId by rememberSaveable { - mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthClientId(existingProfile.id) else "") - } - var clientSecret by rememberSaveable { - mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthClientSecret(existingProfile.id) else "") - } - var tokenEndpoint by rememberSaveable { - mutableStateOf(if (isEdit && existingProfile != null) repo.getOAuthTokenEndpoint(existingProfile.id) else "") - } - - var clientTypeIndex by rememberSaveable { - mutableStateOf(if (existingProfile?.useConSysClient == false) 1 else 0) + LaunchedEffect(profileId) { + viewModel.loadProfile(profileId) } - var nameError by rememberSaveable { mutableStateOf(null) } - var hostError by rememberSaveable { mutableStateOf(null) } - var portError by rememberSaveable { mutableStateOf(null) } - + val state = viewModel.state var isPasswordVisible by remember { mutableStateOf(false) } var isClientSecretVisible by remember { mutableStateOf(false) } @@ -90,59 +60,10 @@ fun ServerFormScreen( val msgPortNumber = stringResource(R.string.msg_port_number) val msgPortRange = stringResource(R.string.msg_port_range) - fun validate(): Boolean { - var valid = true - nameError = if (serverName.isBlank()) { valid = false; msgRequired } else null - hostError = if (host.isBlank()) { - valid = false; msgRequired - } else if (host.contains(" ") || host.contains("://")) { - valid = false; msgNoProtocol - } else null - portError = if (port.isBlank()) { - valid = false; msgRequired - } else { - val portNum = port.toIntOrNull() - if (portNum == null) { valid = false; msgPortNumber } - else if (portNum < 1 || portNum > 65535) { valid = false; msgPortRange } - else null - } - return valid - } - - fun saveProfile() { - if (!validate()) return - - val profile = existingProfile ?: ServerProfile() - profile.name = serverName.trim() - profile.host = host.trim() - profile.port = port.toInt() - var ep = endpointPath.trim() - if (ep.isNotEmpty() && !ep.startsWith("/")) ep = "/$ep" - profile.endpointPath = ep - profile.useConSysClient = clientTypeIndex == 0 - profile.enableTls = enableTls - profile.disableSslCheck = disableSslCheck - profile.oAuthEnabled = enableOAuth - profile.username = if (!enableOAuth) username.trim() else "" - - repo.save(profile) - - val pwd = if (!enableOAuth) password.trim() else "" - repo.setPassword(profile.id, pwd) - - if (enableOAuth) { - repo.setOAuthClientId(profile.id, clientId.trim()) - repo.setOAuthClientSecret(profile.id, clientSecret.trim()) - repo.setOAuthTokenEndpoint(profile.id, tokenEndpoint.trim()) - } - - onBackClick() - } - Scaffold( topBar = { OSHTopAppBarWithBack( - title = stringResource(if (isEdit) R.string.title_edit_server else R.string.add_server), + title = stringResource(if (viewModel.isEdit) R.string.title_edit_server else R.string.add_server), onBackClick = onBackClick ) }, @@ -160,81 +81,75 @@ fun ServerFormScreen( OSHSegmentedButton( options = listOf("CS API Client", "SOS-T Client"), - selectedIndex = clientTypeIndex, - onOptionSelected = { index -> - clientTypeIndex = index - endpointPath = if (index == 0) "/sensorhub/api" else "/sensorhub/sos" - } + selectedIndex = if (state.useConSysClient) 0 else 1, + onOptionSelected = { viewModel.updateUseConSysClient(it == 0) } ) OSHInputField( - value = serverName, - onValueChange = { serverName = it; nameError = null }, + value = state.serverName, + onValueChange = { viewModel.updateServerName(it) }, label = "Server name", - error = nameError + error = viewModel.nameError ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { OSHInputField( - value = host, - onValueChange = { host = it; hostError = null }, + value = state.host, + onValueChange = { viewModel.updateHost(it) }, label = "Host / IP", modifier = Modifier.weight(1f), - error = hostError + error = viewModel.hostError ) OSHInputField( - value = port, - onValueChange = { port = it; portError = null }, + value = viewModel.portText, + onValueChange = { viewModel.updatePort(it) }, label = "Port", modifier = Modifier.weight(0.4f), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - error = portError + error = viewModel.portError ) } OSHInputField( - value = endpointPath, - onValueChange = { endpointPath = it }, + value = state.endpointPath, + onValueChange = { viewModel.updateEndpointPath(it) }, label = "Endpoint path", ) OSHSwitchRow( title = "Enable TLS", - checked = enableTls, - onCheckedChange = { - enableTls = it - if (!it) disableSslCheck = false - }, + checked = state.enableTls, + onCheckedChange = { viewModel.updateEnableTls(it) }, ) - if (enableTls) { + if (state.enableTls) { OSHSwitchRow( title = "Disable SSL Check", - checked = disableSslCheck, - onCheckedChange = { disableSslCheck = it }, + checked = state.disableSslCheck, + onCheckedChange = { viewModel.updateDisableSslCheck(it) }, ) } OSHSwitchRow( title = "Enable OAuth", - checked = enableOAuth, - onCheckedChange = { enableOAuth = it }, + checked = state.enableOAuth, + onCheckedChange = { viewModel.updateEnableOAuth(it) }, ) - if (enableOAuth) { + if (state.enableOAuth) { OSHInputField( - value = tokenEndpoint, - onValueChange = { tokenEndpoint = it }, + value = state.tokenEndpoint, + onValueChange = { viewModel.updateTokenEndpoint(it) }, label = "Token Endpoint", ) OSHInputField( - value = clientId, - onValueChange = { clientId = it }, + value = state.clientId, + onValueChange = { viewModel.updateClientId(it) }, label = "Client ID", ) OSHInputField( - value = clientSecret, - onValueChange = { clientSecret = it }, + value = state.clientSecret, + onValueChange = { viewModel.updateClientSecret(it) }, label = "Client Secret", visualTransformation = if (isClientSecretVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { @@ -252,13 +167,13 @@ fun ServerFormScreen( ) } else { OSHInputField( - value = username, - onValueChange = { username = it }, + value = state.username, + onValueChange = { viewModel.updateUsername(it) }, label = "Username", ) OSHInputField( - value = password, - onValueChange = { password = it }, + value = state.password, + onValueChange = { viewModel.updatePassword(it) }, label = "Password", visualTransformation = if (isPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(), trailingIcon = { @@ -279,8 +194,12 @@ fun ServerFormScreen( Spacer(modifier = Modifier.height(16.dp)) OSHButton( - onClick = { saveProfile() }, - text = stringResource(if (isEdit) R.string.title_edit_server else R.string.btn_add_server), + onClick = { + if (viewModel.saveProfile()) { + onBackClick() + } + }, + text = stringResource(if (viewModel.isEdit) R.string.title_edit_server else R.string.btn_add_server), modifier = Modifier.fillMaxWidth() ) }