diff --git a/.github/workflows/gradle_build.yml b/.github/workflows/gradle_build.yml index 9523766c..e6a970d5 100644 --- a/.github/workflows/gradle_build.yml +++ b/.github/workflows/gradle_build.yml @@ -10,11 +10,6 @@ jobs: build: runs-on: ubuntu-latest steps: - - name: Configure SSH for private submodules - uses: webfactory/ssh-agent@v0.9.0 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} - - name: Checkout code uses: actions/checkout@v4 with: diff --git a/build.gradle b/build.gradle index 46c4df04..fd680682 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ ext.compileSdkVersion = 34 ext.minSdkVersion = 33 ext.targetSdkVersion = 30 ext.buildToolsVersion = "34.0.0" -version = '4.0.2' +version = '4.1.1' buildscript { repositories { @@ -13,6 +13,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.4.2' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20' } } diff --git a/gradle.properties b/gradle.properties index 94adc3a3..4de64f2e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx4096M android.useAndroidX=true android.enableJetifier=true diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index cc935964..c1b78ac4 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -82,6 +82,13 @@ android { java.srcDirs = ['src'] res.srcDirs = ['res'] assets.srcDirs = ['assets'] + resources.srcDirs = ['src/main/resources'] + } + } + + applicationVariants.all { variant -> + variant.outputs.all { + outputFileName = "osh-android-${variant.versionName}-${variant.name}.apk" } } diff --git a/sensorhub-android-app/res/color/bottom_nav_selector.xml b/sensorhub-android-app/res/color/bottom_nav_selector.xml index 68a6dc27..32651b24 100644 --- a/sensorhub-android-app/res/color/bottom_nav_selector.xml +++ b/sensorhub-android-app/res/color/bottom_nav_selector.xml @@ -1,5 +1,5 @@ - - + + diff --git a/sensorhub-android-app/res/drawable/status_dot_background.xml b/sensorhub-android-app/res/drawable/ic_circle.xml similarity index 100% rename from sensorhub-android-app/res/drawable/status_dot_background.xml rename to sensorhub-android-app/res/drawable/ic_circle.xml diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml index 69a569ce..c4a64d8a 100644 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -55,7 +55,7 @@ android:id="@+id/sos_status_dot" android:layout_width="@dimen/status_dot_size" android:layout_height="@dimen/status_dot_size" - android:background="@drawable/status_dot_background" /> + android:background="@drawable/ic_circle" /> + android:background="@drawable/ic_circle" /> + android:background="@drawable/ic_circle" /> + android:background="@drawable/ic_circle" /> + android:background="@drawable/ic_circle" /> + android:background="@drawable/ic_circle" /> + app:strokeWidth="0dp"> + app:strokeWidth="0dp"> + android:singleLine="true" + android:text="/sensorhub/api"/> + app:strokeWidth="0dp"> + app:strokeWidth="0dp"> + android:background="@drawable/ic_circle" /> + app:strokeWidth="0dp"> + android:background="@drawable/ic_circle" /> + app:tint="@android:color/white" + app:backgroundTint="@color/md_theme_surfaceVariant" /> diff --git a/sensorhub-android-app/res/layout/item_datastream.xml b/sensorhub-android-app/res/layout/item_datastream.xml new file mode 100644 index 00000000..361786ef --- /dev/null +++ b/sensorhub-android-app/res/layout/item_datastream.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_faq_entry.xml b/sensorhub-android-app/res/layout/item_faq_entry.xml index 6fe1b4a8..7d88c563 100644 --- a/sensorhub-android-app/res/layout/item_faq_entry.xml +++ b/sensorhub-android-app/res/layout/item_faq_entry.xml @@ -7,7 +7,7 @@ android:layout_marginBottom="8dp" app:cardCornerRadius="@dimen/card_corner_radius" app:cardElevation="@dimen/card_elevation" - app:cardBackgroundColor="@color/md_theme_surface"> + app:cardBackgroundColor="@color/dialog_bg"> @@ -36,7 +36,7 @@ android:layout_height="24dp" android:layout_marginStart="8dp" android:src="@drawable/ic_expand_more" - app:tint="@color/md_theme_onSurfaceVariant" + app:tint="@android:color/white" android:contentDescription="Expand" /> @@ -46,7 +46,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="12dp" - android:textColor="@color/md_theme_onSurfaceVariant" + android:textColor="@android:color/white" android:textSize="14sp" android:lineSpacingExtra="4dp" android:visibility="gone" /> diff --git a/sensorhub-android-app/res/layout/item_sensor_group.xml b/sensorhub-android-app/res/layout/item_sensor_group.xml new file mode 100644 index 00000000..b200401d --- /dev/null +++ b/sensorhub-android-app/res/layout/item_sensor_group.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_server_profile.xml b/sensorhub-android-app/res/layout/item_server_profile.xml index 45fa407b..19bdf7df 100644 --- a/sensorhub-android-app/res/layout/item_server_profile.xml +++ b/sensorhub-android-app/res/layout/item_server_profile.xml @@ -11,8 +11,7 @@ app:cardCornerRadius="@dimen/card_corner_radius" app:cardElevation="0dp" app:cardBackgroundColor="@color/surface_card" - app:strokeColor="@color/md_theme_outline" - app:strokeWidth="1dp"> + app:strokeWidth="0dp"> + android:layout_marginHorizontal="16dp" + android:layout_marginTop="8dp" + app:cardCornerRadius="16dp" + app:cardElevation="2dp" + app:strokeWidth="1dp" + app:strokeColor="?attr/colorOutlineVariant"> + android:orientation="horizontal" + android:padding="16dp"> - + + android:orientation="vertical"> + android:textAppearance="?attr/textAppearanceTitleMedium" + android:textColor="?attr/colorOnSurface"/> - + android:layout_marginTop="2dp" + android:textAppearance="?attr/textAppearanceBodySmall" + android:textColor="?attr/colorOnSurfaceVariant"/> - + - + + - + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/pref_switch_bottom.xml b/sensorhub-android-app/res/layout/pref_switch_bottom.xml index 514d43bf..d62c2468 100644 --- a/sensorhub-android-app/res/layout/pref_switch_bottom.xml +++ b/sensorhub-android-app/res/layout/pref_switch_bottom.xml @@ -35,12 +35,10 @@ android:layout_marginTop="2dp" /> - + android:clickable="false" /> diff --git a/sensorhub-android-app/res/layout/pref_switch_mid.xml b/sensorhub-android-app/res/layout/pref_switch_mid.xml index 4141a24f..82b79197 100644 --- a/sensorhub-android-app/res/layout/pref_switch_mid.xml +++ b/sensorhub-android-app/res/layout/pref_switch_mid.xml @@ -35,12 +35,10 @@ android:layout_marginTop="2dp" /> - + android:clickable="false" /> diff --git a/sensorhub-android-app/res/layout/pref_switch_top.xml b/sensorhub-android-app/res/layout/pref_switch_top.xml index 9bf22283..e23458fd 100644 --- a/sensorhub-android-app/res/layout/pref_switch_top.xml +++ b/sensorhub-android-app/res/layout/pref_switch_top.xml @@ -35,12 +35,10 @@ android:layout_marginTop="2dp" /> - + android:clickable="false" /> diff --git a/sensorhub-android-app/res/layout/preference_switch_item.xml b/sensorhub-android-app/res/layout/preference_switch_item.xml index 24ec3c5c..6a6101c9 100644 --- a/sensorhub-android-app/res/layout/preference_switch_item.xml +++ b/sensorhub-android-app/res/layout/preference_switch_item.xml @@ -30,16 +30,13 @@ android:textAlignment="viewStart" /> - + android:paddingEnd="0dp" /> \ No newline at end of file diff --git a/sensorhub-android-app/res/values/colors.xml b/sensorhub-android-app/res/values/colors.xml index d04f4a14..42b44bf1 100644 --- a/sensorhub-android-app/res/values/colors.xml +++ b/sensorhub-android-app/res/values/colors.xml @@ -1,73 +1,74 @@ - #D35400 + #E64A19 #FFFFFF - #24160A - #E6B89C + #4E1700 + #FFCCBC - #C24E00 + #BF360C - #9A9A9A + #9E9E9E #0A0A0A - #242424 - #F5F5F5 + #424242 + #E0E0E0 - #B84A0D + #FF7043 #FFFFFF - #1C0A00 - #E6B89C + #4E1700 + #FFCCBC #CF6679 #1C0006 - #4A0011 - #FFB3BA + #93000A + #FFDAD6 - #0A0A0A + #121212 - #F5F5F5 + #E0E0E0 - #141414 - #F5F5F5 + #1E1E1E + #E0E0E0 - #1C1C1C - #9A9A9A + #2C2C2C + #BDBDBD - #2E2E2E + #616161 - #0A0A0A - #141414 - #1C1C1C - #242424 + #E64A19 + #1E1E1E + #2C2C2C + #333333 - #F5F5F5 - #9A9A9A - #5A5A5A + #E0E0E0 + #BDBDBD + #757575 - #D35400 - #C24E00 - #1AD35400 + #E64A19 + #BF360C + #1AE64A19 - #141414 - #F5F5F5 + #E64A19 + #FFFFFF - #141414 - #D35400 - #6A6A6A + #1E1E1E + #E64A19 + #757575 #4CAF50 - #F44336 + #EF5350 #FF9800 - #5A5A5A + #757575 #1AFFFFFF #99000000 - #1C1C1C + #212121 + #212121 - \ No newline at end of file + diff --git a/sensorhub-android-app/res/values/styles.xml b/sensorhub-android-app/res/values/styles.xml index f7d52d6c..1e68ed1b 100644 --- a/sensorhub-android-app/res/values/styles.xml +++ b/sensorhub-android-app/res/values/styles.xml @@ -4,12 +4,11 @@ - + + + @@ -77,8 +91,7 @@ 16dp 0dp @color/surface_card - @color/md_theme_outline - 1dp + 0dp @@ -214,6 +228,8 @@ bold sans-serif-medium 16sp + 10dp + - \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 0aab7631..4ac76b84 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -11,6 +11,7 @@ android:singleLine="true" android:title="@string/pref_uid_extension" android:summary="@string/summary_uid_extension" + app:useSimpleSummaryProvider="true" android:layout="@layout/preference_item" /> diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index 4394087b..6a30d18a 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -1,28 +1,26 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> - - + + + android:selectAllOnFocus="true" + app:useSimpleSummaryProvider="true" + android:layout="@layout/pref_value_single" /> - - - + + android:layout="@layout/pref_switch_top" /> + android:layout="@layout/pref_switch_mid" /> + android:layout="@layout/pref_switch_bottom" /> + android:layout="@layout/pref_edit_bottom" /> - - \ No newline at end of file + diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index a4b15456..86350ec9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -14,16 +14,19 @@ import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageButton; +import android.widget.ImageView; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; +import com.google.android.material.chip.Chip; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.textfield.TextInputEditText; @@ -36,6 +39,10 @@ import android.widget.Toast; import androidx.core.content.ContextCompat; +import androidx.transition.AutoTransition; +import androidx.transition.TransitionManager; + +import net.opengis.swe.v20.DataBlock; import org.sensorhub.api.command.CommandData; import org.sensorhub.api.command.IStreamingControlInterface; @@ -55,6 +62,8 @@ import org.sensorhub.impl.sensor.meshtastic.control.TextMessageControl; import org.sensorhub.impl.service.consys.client.ConSysApiClientModule; +import java.io.IOException; + import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; @@ -64,11 +73,11 @@ import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.Flow; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import android.widget.LinearLayout; -import net.opengis.swe.v20.DataBlock; - public class DashboardFragment extends Fragment implements TextureView.SurfaceTextureListener, Flow.Subscriber { @@ -82,7 +91,6 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe private LinearLayout videoControlsOverlay; private int currentZoomLevel = 0; private MaterialCardView meshtasticCard; - private View videoStatusDot; private FloatingActionButton fab; private LinearLayout serverStatusContainer; @@ -95,6 +103,34 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe private final Map serverCardViews = new HashMap<>(); private final Set expandedServers = new HashSet<>(); + private final Set expandedSensors = new HashSet<>(); + + private static class DataStreamStatus { + final String outputName; + final String statusText; + final int statusColor; + final boolean isOk; + + DataStreamStatus(String outputName, String statusText, int statusColor, boolean isOk) { + this.outputName = outputName; + this.statusText = statusText; + this.statusColor = statusColor; + this.isOk = isOk; + } + } + + private static class SensorGroupInfo { + final String sensorId; + final String sensorName; + final java.util.List streams = new java.util.ArrayList<>(); + boolean allOk = true; + boolean hasError = false; + + SensorGroupInfo(String sensorId, String sensorName) { + this.sensorId = sensorId; + this.sensorName = sensorName; + } + } @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -197,18 +233,21 @@ private void hideFabProgress() { } private void stopHub() { + Toast.makeText(requireContext(), "Stopping SensorHub", Toast.LENGTH_SHORT).show(); showFabProgress(); newStatusMessage(getString(R.string.stopping_sensorhub)); + stopRefreshingStatus(); hideVideoPreview(); clearTextureView(); videoStatusCard.setVisibility(View.GONE); if (videoControlsOverlay != null) videoControlsOverlay.setVisibility(View.GONE); currentZoomLevel = 0; + if (btnFlipCamera != null) btnFlipCamera.setVisibility(View.GONE); + if (meshtasticCard != null) meshtasticCard.setVisibility(View.GONE); + requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); if (meshtasticCard != null) meshtasticCard.setVisibility(View.GONE); provider.stopSensorHub(); - - // Brief delay to let the service finish stopping on its background thread displayHandler.postDelayed(() -> { if (!isAdded()) return; hideFabProgress(); @@ -317,7 +356,6 @@ private void pollHubReady() { if (!provider.isOshStarted()) { provider.setOshStarted(true); hideFabProgress(); - updateFabIcon(); serverStatusContainer.removeAllViews(); serverCardViews.clear(); startRefreshingStatus(); @@ -329,8 +367,7 @@ private void pollHubReady() { } else if (hubPollAttempts < HUB_POLL_MAX_ATTEMPTS) { displayHandler.postDelayed(this::pollHubReady, HUB_POLL_INTERVAL_MS); } else { - hideFabProgress(); - newStatusMessage(getString(R.string.sensorhub_start_failed)); + newStatusMessage("SensorHub failed to start"); updateFabIcon(); } } @@ -373,7 +410,8 @@ protected synchronized void displayStatus() { String clientMode = "SOS-T"; Map dataStreams = client.getDataStreams(); - StringBuffer detailHtml = new StringBuffer(); + String errorText = null; + String statusMsg = null; boolean hasError = false; if (client.getCurrentError() != null) { @@ -383,35 +421,18 @@ protected synchronized void displayStatus() { if (!errorMsg.endsWith(".")) errorMsg += ". "; if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) errorMsg += errorObj.getCause().getMessage(); - detailHtml.append("" + errorMsg + "
"); + errorText = errorMsg; } if (dataStreams.isEmpty() && client.getStatusMessage() != null) { - detailHtml.append(client.getStatusMessage() + "
"); + statusMsg = client.getStatusMessage(); } long now = System.currentTimeMillis(); boolean allOk = !hasError && !dataStreams.isEmpty(); - for (Entry stream : dataStreams.entrySet()) { - detailHtml.append("" + stream.getKey() + " : "); - long lastEventTime = stream.getValue().lastEventTime; - long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) { - detailHtml.append("NO OBS"); - allOk = false; - } else if (dt > stream.getValue().measPeriodMs) { - detailHtml.append("NOK (" + dt + "ms ago)"); - allOk = false; - } else { - detailHtml.append("OK (" + dt + "ms ago)"); - } - if (stream.getValue().errorCount > 0) { - detailHtml.append(" (" + stream.getValue().errorCount + ")"); - allOk = false; - } - detailHtml.append("
"); - } + java.util.List sensorGroups = buildSensorGroups(dataStreams, now); + for (SensorGroupInfo g : sensorGroups) { if (!g.allOk) allOk = false; if (g.hasError) hasError = true; } - updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); + updateServerCard(clientId, serverName, clientMode, allOk, hasError, errorText, statusMsg, sensorGroups); } for (ConSysApiClientModule client : provider.getConSysClients()) { @@ -421,7 +442,8 @@ protected synchronized void displayStatus() { String clientMode = "Connected Systems"; Map dataStreams = client.getDataStreams(); - StringBuffer detailHtml = new StringBuffer(); + String errorText = null; + String statusMsg = null; boolean hasError = false; if (client.getCurrentError() != null) { @@ -431,35 +453,18 @@ protected synchronized void displayStatus() { if (!errorMsg.endsWith(".")) errorMsg += ". "; if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) errorMsg += errorObj.getCause().getMessage(); - detailHtml.append("" + errorMsg + "
"); + errorText = errorMsg; } if (dataStreams.isEmpty() && client.getStatusMessage() != null) { - detailHtml.append(client.getStatusMessage() + "
"); + statusMsg = client.getStatusMessage(); } long now = System.currentTimeMillis(); boolean allOk = !hasError && !dataStreams.isEmpty(); - for (Entry stream : dataStreams.entrySet()) { - detailHtml.append("" + stream.getKey() + " : "); - long lastEventTime = stream.getValue().lastEventTime; - long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) { - detailHtml.append("NO OBS"); - allOk = false; - } else if (dt > stream.getValue().measPeriodMs) { - detailHtml.append("NOK (" + dt + "ms ago)"); - allOk = false; - } else { - detailHtml.append("OK (" + dt + "ms ago)"); - } - if (stream.getValue().errorCount > 0) { - detailHtml.append(" (" + stream.getValue().errorCount + ")"); - allOk = false; - } - detailHtml.append("
"); - } + java.util.List sensorGroups = buildSensorGroups(dataStreams, now); + for (SensorGroupInfo g : sensorGroups) { if (!g.allOk) allOk = false; if (g.hasError) hasError = true; } - updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); + updateServerCard(clientId, serverName, clientMode, allOk, hasError, errorText, statusMsg, sensorGroups); } Set staleIds = new HashSet<>(serverCardViews.keySet()); @@ -526,6 +531,107 @@ protected synchronized void newStatusMessage(String msg) { }); } + private String extractSensorId(String streamKey) { + Matcher m = Pattern.compile("systems/([^/]+)/").matcher(streamKey); + if (m.find()) return m.group(1); + return streamKey; + } + + private String formatSensorName(String sensorId) { + String[] parts = sensorId.replace("urn:", "").split(":"); + String name; + if (parts.length >= 3) { + name = parts[parts.length - 2]; + } else if (parts.length == 2) { + name = parts[0]; + } else { + name = sensorId; + } + name = name.replace("_", " ").replace("-", " "); + return name.substring(0, 1).toUpperCase() + name.substring(1); //capitalizes first letter + } + + private String formatOutputName(String streamKey) { + Matcher m = Pattern.compile("outputs/([^/]+)").matcher(streamKey); + String raw; + if (m.find()) { + raw = m.group(1); + } else { + raw = streamKey; + } + raw = raw.replaceAll("(?i)_data$", ""); + raw = raw.replace("_", " ").replace("-", " "); + return raw.substring(0, 1).toUpperCase() + raw.substring(1); //capitalizes first letter + } + + + private interface StreamAccessor { + long getLastEventTime(T info); + long getMeasPeriodMs(T info); + int getErrorCount(T info); + } + + private java.util.List buildSensorGroups(Map dataStreams, long now) { + StreamAccessor accessor; + if (!dataStreams.isEmpty()) { + Object first = dataStreams.values().iterator().next(); + if (first instanceof StreamInfo) { + accessor = new StreamAccessor() { + public long getLastEventTime(Object o) { return ((StreamInfo)o).lastEventTime; } + public long getMeasPeriodMs(Object o) { return ((StreamInfo)o).measPeriodMs; } + public int getErrorCount(Object o) { return ((StreamInfo)o).errorCount; } + }; + } else { + accessor = new StreamAccessor() { + public long getLastEventTime(Object o) { return ((ConSysApiClientModule.StreamInfo)o).lastEventTime; } + public long getMeasPeriodMs(Object o) { return ((ConSysApiClientModule.StreamInfo)o).measPeriodMs; } + public int getErrorCount(Object o) { return ((ConSysApiClientModule.StreamInfo)o).errorCount; } + }; + } + } else { + return new java.util.ArrayList<>(); + } + + Map grouped = new java.util.LinkedHashMap<>(); + for (Entry stream : dataStreams.entrySet()) { + String sensorId = extractSensorId(stream.getKey()); + SensorGroupInfo group = grouped.computeIfAbsent(sensorId, + k -> new SensorGroupInfo(k, formatSensorName(k))); + + String outputName = formatOutputName(stream.getKey()); + long lastEventTime = accessor.getLastEventTime(stream.getValue()); + long dt = now - lastEventTime; + long measPeriod = accessor.getMeasPeriodMs(stream.getValue()); + int errorCount = accessor.getErrorCount(stream.getValue()); + + String statusText; + int statusColor; + boolean isOk; + if (lastEventTime == Long.MIN_VALUE) { + statusText = "NO OBS"; + statusColor = R.color.status_stopped; + isOk = false; + } else if (dt > measPeriod) { + statusText = "NOK (" + dt + "ms)"; + statusColor = R.color.status_stopped; + isOk = false; + } else { + statusText = "OK (" + dt + "ms)"; + statusColor = R.color.status_started; + isOk = true; + } + if (errorCount > 0) { + statusText += " (" + errorCount + ")"; + statusColor = R.color.status_stopped; + isOk = false; + } + if (!isOk) group.allOk = false; + + group.streams.add(new DataStreamStatus(outputName, statusText, statusColor, isOk)); + } + return new java.util.ArrayList<>(grouped.values()); + } + private String extractServerName(String clientName, String fallback) { if (clientName != null && clientName.contains(" -> ")) { return clientName.substring(clientName.lastIndexOf(" -> ") + 4); @@ -534,7 +640,9 @@ private String extractServerName(String clientName, String fallback) { } private void updateServerCard(String clientId, String serverName, String clientMode, - boolean allOk, boolean hasError, String detailHtml) { + boolean allOk, boolean hasError, + String errorText, String statusMsg, + java.util.List sensorGroups) { View card = serverCardViews.get(clientId); if (card == null) { @@ -545,54 +653,135 @@ private void updateServerCard(String clientId, String serverName, String clientM final View cardRef = card; final String idRef = clientId; - View header = card.findViewById(R.id.server_status_header); - header.setOnClickListener(v -> { - boolean expanded = expandedServers.contains(idRef); - TextView details = cardRef.findViewById(R.id.server_status_details); - ImageButton toggle = cardRef.findViewById(R.id.btn_toggle_server_details); + ImageButton toggleBtn = card.findViewById(R.id.btn_toggle_server_details); + toggleBtn.setOnClickListener(v -> { + LinearLayout details = cardRef.findViewById(R.id.server_status_details); + View divider = cardRef.findViewById(R.id.server_divider); + ImageButton arrow = cardRef.findViewById(R.id.btn_toggle_server_details); + boolean expanded = details.getVisibility() == View.VISIBLE; + + TransitionManager.beginDelayedTransition( + (ViewGroup) cardRef, + new AutoTransition().setDuration(200) + ); + + details.setVisibility(expanded ? View.GONE : View.VISIBLE); + divider.setVisibility(expanded ? View.GONE : View.VISIBLE); + + arrow.animate() + .rotation(expanded ? 0f : 90f) + .setDuration(200) + .start(); + if (expanded) { expandedServers.remove(idRef); - details.setVisibility(View.GONE); - toggle.setImageResource(R.drawable.ic_expand_more); } else { expandedServers.add(idRef); - details.setVisibility(View.VISIBLE); - toggle.setImageResource(R.drawable.ic_expand_less); } }); } TextView nameView = card.findViewById(R.id.server_status_name); - TextView modeView = card.findViewById(R.id.server_status_mode); nameView.setText(serverName); - modeView.setText(clientMode); - View dot = card.findViewById(R.id.server_status_dot); - if (dot.getBackground() instanceof GradientDrawable) { - GradientDrawable bg = (GradientDrawable) dot.getBackground(); - int colorRes; - if (hasError) colorRes = R.color.status_stopped; - else if (allOk) colorRes = R.color.status_started; - else colorRes = R.color.status_initializing; - bg.setColor(ContextCompat.getColor(requireContext(), colorRes)); + TextView subtitleView = card.findViewById(R.id.server_status_subtitle); + subtitleView.setText(clientMode); + + View serverDot = card.findViewById(R.id.server_overall_status_dot); + if (serverDot.getBackground() instanceof GradientDrawable) { + GradientDrawable dotBg = (GradientDrawable) serverDot.getBackground(); + boolean serverOk = errorText == null; + if (serverOk) { + for (SensorGroupInfo g : sensorGroups) { + if (!g.allOk) { serverOk = false; break; } + } + } + int dotColor = serverOk ? R.color.status_started : R.color.status_stopped; + dotBg.setColor(ContextCompat.getColor(requireContext(), dotColor)); } - if (card instanceof MaterialCardView) { - int strokeColorRes; - if (hasError) strokeColorRes = R.color.status_stopped; - else if (allOk) strokeColorRes = R.color.status_started; - else strokeColorRes = R.color.md_theme_outline; - ((MaterialCardView) card).setStrokeColor( - ContextCompat.getColor(requireContext(), strokeColorRes)); + LinearLayout detailsContainer = card.findViewById(R.id.server_status_details); + detailsContainer.removeAllViews(); + + if (errorText != null) { + TextView errorView = new TextView(requireContext()); + errorView.setText(errorText); + errorView.setTextColor(ContextCompat.getColor(requireContext(), R.color.status_stopped)); + errorView.setTextSize(12); + errorView.setPadding(dpToPx(16), dpToPx(4), dpToPx(16), dpToPx(4)); + detailsContainer.addView(errorView); + } + + if (statusMsg != null) { + TextView statusView = new TextView(requireContext()); + statusView.setText(statusMsg); + statusView.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurfaceVariant)); + statusView.setTextSize(12); + statusView.setPadding(dpToPx(16), dpToPx(4), dpToPx(16), dpToPx(4)); + detailsContainer.addView(statusView); + } + + LayoutInflater inflater = LayoutInflater.from(requireContext()); + for (SensorGroupInfo group : sensorGroups) { + View groupView = inflater.inflate(R.layout.item_sensor_group, detailsContainer, false); + String sensorKey = clientId + ":" + group.sensorId; + + TextView sensorName = groupView.findViewById(R.id.sensor_group_name); + ImageView sensorArrow = groupView.findViewById(R.id.sensor_group_arrow); + View serverStatusDot = groupView.findViewById(R.id.server_status_dot); + LinearLayout streamsContainer = groupView.findViewById(R.id.sensor_group_streams); + View sensorHeader = groupView.findViewById(R.id.sensor_group_header); + + sensorName.setText(group.sensorName); + + if (serverStatusDot.getBackground() instanceof GradientDrawable) { + GradientDrawable dotBg = (GradientDrawable) serverStatusDot.getBackground(); + int dotColor = group.allOk ? R.color.status_started : R.color.status_stopped; + dotBg.setColor(ContextCompat.getColor(requireContext(), dotColor)); + } + + for (DataStreamStatus stream : group.streams) { + View streamView = inflater.inflate(R.layout.item_datastream, streamsContainer, false); + TextView streamName = streamView.findViewById(R.id.datastream_name); + TextView streamStatus = streamView.findViewById(R.id.datastream_status); + streamName.setText(stream.outputName); + streamStatus.setText(stream.statusText); + streamStatus.setTextColor(ContextCompat.getColor(requireContext(), stream.statusColor)); + streamsContainer.addView(streamView); + } + + boolean sensorExpanded = expandedSensors.contains(sensorKey); + streamsContainer.setVisibility(sensorExpanded ? View.VISIBLE : View.GONE); + sensorArrow.setRotation(sensorExpanded ? 90f : 0f); + + sensorHeader.setOnClickListener(v -> { + boolean exp = expandedSensors.contains(sensorKey); + if (exp) { + expandedSensors.remove(sensorKey); + streamsContainer.setVisibility(View.GONE); + sensorArrow.animate().rotation(0f).setDuration(200).start(); + } else { + expandedSensors.add(sensorKey); + streamsContainer.setVisibility(View.VISIBLE); + sensorArrow.animate().rotation(90f).setDuration(200).start(); + } + }); + + detailsContainer.addView(groupView); } - TextView details = card.findViewById(R.id.server_status_details); - details.setText(Html.fromHtml(detailHtml)); boolean expanded = expandedServers.contains(clientId); - details.setVisibility(expanded ? View.VISIBLE : View.GONE); + detailsContainer.setVisibility(expanded ? View.VISIBLE : View.GONE); + + View divider = card.findViewById(R.id.server_divider); + divider.setVisibility(expanded ? View.VISIBLE : View.GONE); ImageButton toggle = card.findViewById(R.id.btn_toggle_server_details); - toggle.setImageResource(expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); + toggle.setRotation(expanded ? 90f : 0f); + } + + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); } private void updateVideoStatusCard() { @@ -639,6 +828,24 @@ private void updateVideoControlsVisibility() { boolean showFlip = hasVideo && android.hardware.Camera.getNumberOfCameras() > 1; btnFlipCamera.setVisibility(showFlip ? View.VISIBLE : View.GONE); } + + boolean isBackCamera = isBackCameraActive(); + if (btnZoomIn != null) btnZoomIn.setVisibility(isBackCamera ? View.VISIBLE : View.GONE); + if (btnZoomOut != null) btnZoomOut.setVisibility(isBackCamera ? View.VISIBLE : View.GONE); + } + + @SuppressWarnings("deprecation") + private boolean isBackCameraActive() { + AndroidSensorsDriver sensors = provider.getAndroidSensors(); + if (sensors == null) return true; + try { + int cameraId = sensors.getConfiguration().selectedCameraId; + android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo(); + android.hardware.Camera.getCameraInfo(cameraId, info); + return info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_BACK; + } catch (Exception e) { + return true; + } } @SuppressWarnings("deprecation") @@ -744,7 +951,7 @@ private void showMeshtasticDialog() { private void sendMeshtasticMessage(String message, String nodeId) { SensorHubService service = provider.getBoundService(); if (service == null || service.getSensorHub() == null) { - Toast.makeText(requireContext(), R.string.msg_sensorhub_not_running, Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), "SensorHub not running", Toast.LENGTH_SHORT).show(); return; } @@ -770,9 +977,9 @@ private void sendMeshtasticMessage(String message, String nodeId) { .build(); textMessageControl.submitCommand(cmd); - Toast.makeText(requireContext(), R.string.msg_message_sent, Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), "Message sent", Toast.LENGTH_SHORT).show(); } catch (Exception e) { - Toast.makeText(requireContext(), R.string.msg_message_failed, Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), "Failed to send message", Toast.LENGTH_SHORT).show(); } } @Override @@ -803,7 +1010,7 @@ public void onNext(Event e) { if (e instanceof ModuleEvent) { if (!provider.isOshStarted() && ((ModuleEvent) e).getType() == ModuleEvent.Type.LOADED) { provider.setOshStarted(true); - requireActivity().runOnUiThread(this::updateFabIcon); + requireActivity().runOnUiThread(this::hideFabProgress); startRefreshingStatus(); subscription.request(10); return; diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 2d1fed82..1d6c2caf 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -303,57 +303,6 @@ public boolean verify(String arg0, SSLSession arg1) { serverConfig.autoStart = true; sensorhubConfig.add(serverConfig); - // SOS Service - SOSServiceConfig sosConfig = new SOSServiceConfig(); - sosConfig.moduleClass = SOSService.class.getCanonicalName(); - sosConfig.id = "SOS_SERVICE"; - sosConfig.name = "SOS Service"; - sosConfig.autoStart = true; - sosConfig.enableTransactional = true; - sosConfig.exposedResources = new ObsSystemDatabaseViewConfig(); - - // Connected Systems Service - ConSysApiServiceConfig conSysApiService = new ConSysApiServiceConfig(); - conSysApiService.moduleClass = ConSysApiService.class.getCanonicalName(); - conSysApiService.id = "CON_SYS_SERVICE"; - conSysApiService.name = "Connected Systems API Service"; - conSysApiService.autoStart = true; - conSysApiService.enableTransactional = true; - conSysApiService.exposedResources = new ObsSystemDatabaseViewConfig(); - - // Discovery Service - DiscoveryServiceConfig discoveryServiceConfig = new DiscoveryServiceConfig(); - discoveryServiceConfig.moduleClass = DiscoveryService.class.getCanonicalName(); - discoveryServiceConfig.id = "DISCOVERY_SERVICE"; - discoveryServiceConfig.name= "Discovery Service"; - discoveryServiceConfig.autoStart = true; - - File outFile = new File(getApplicationContext().getFilesDir(), "rules.txt"); - String rulesLink = prefs.getString("rules_link", ""); - FutureTask downloadTask = new java.util.concurrent.FutureTask<>(() -> { - URL rulesUrl = new URL(rulesLink); - HttpURLConnection conn = (HttpURLConnection) rulesUrl.openConnection(); - conn.setInstanceFollowRedirects(true); - try (InputStream in = conn.getInputStream(); - OutputStream out = new FileOutputStream(outFile)) { - byte[] buffer = new byte[1024]; - int len; - while ((len = in.read(buffer)) > 0) { - out.write(buffer, 0, len); - } - } finally { - conn.disconnect(); - } - return null; - }); - new Thread(downloadTask).start(); - try { - downloadTask.get(); - } catch (Exception e) { - Log.e("OSH - Discovery", "Failed to download rules file", e); - } - discoveryServiceConfig.rulesFilePath = outFile.getAbsolutePath(); - sensorhubConfig.add(sensorsConfig); // TruPulse sensor @@ -526,12 +475,63 @@ public boolean verify(String arg0, SSLSession arg1) { //---------- SERVICES --------------------- if (isApiServiceEnabled) { + // Connected Systems Service + ConSysApiServiceConfig conSysApiService = new ConSysApiServiceConfig(); + conSysApiService.moduleClass = ConSysApiService.class.getCanonicalName(); + conSysApiService.id = "CON_SYS_SERVICE"; + conSysApiService.name = "Connected Systems API Service"; + conSysApiService.autoStart = true; + conSysApiService.enableTransactional = true; + conSysApiService.exposedResources = new ObsSystemDatabaseViewConfig(); + sensorhubConfig.add(conSysApiService); } if (isSosServiceEnabled) { + // SOS Service + SOSServiceConfig sosConfig = new SOSServiceConfig(); + sosConfig.moduleClass = SOSService.class.getCanonicalName(); + sosConfig.id = "SOS_SERVICE"; + sosConfig.name = "SOS Service"; + sosConfig.autoStart = true; + sosConfig.enableTransactional = true; + sosConfig.exposedResources = new ObsSystemDatabaseViewConfig(); + sensorhubConfig.add(sosConfig); } if (isDiscoveryServiceEnabled) { + // Discovery Service + DiscoveryServiceConfig discoveryServiceConfig = new DiscoveryServiceConfig(); + discoveryServiceConfig.moduleClass = DiscoveryService.class.getCanonicalName(); + discoveryServiceConfig.id = "DISCOVERY_SERVICE"; + discoveryServiceConfig.name= "Discovery Service"; + discoveryServiceConfig.autoStart = true; + + File outFile = new File(getApplicationContext().getFilesDir(), "rules.txt"); + String rulesLink = prefs.getString("rules_link", ""); + FutureTask downloadTask = new java.util.concurrent.FutureTask<>(() -> { + URL rulesUrl = new URL(rulesLink); + HttpURLConnection conn = (HttpURLConnection) rulesUrl.openConnection(); + conn.setInstanceFollowRedirects(true); + try (InputStream in = conn.getInputStream(); + OutputStream out = new FileOutputStream(outFile)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } finally { + conn.disconnect(); + } + return null; + }); + new Thread(downloadTask).start(); + try { + downloadTask.get(); + } catch (Exception e) { + Log.e("OSH - Discovery", "Failed to download rules file", e); + } + discoveryServiceConfig.rulesFilePath = outFile.getAbsolutePath(); + sensorhubConfig.add(discoveryServiceConfig); } } diff --git a/settings.gradle b/settings.gradle index 23bd0d4d..dc9b58ed 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,7 +32,9 @@ def repos = [ 'sensors/health/sensorhub-driver-angelsensor', 'processing/sensorhub-process-vecmath', 'processing/sensorhub-process-geoloc', - 'services/sensorhub-service-discovery' + 'services/sensorhub-service-discovery', + 'util/sensorhub-lib-rules-engine', + ] ] diff --git a/submodules/osh-addons b/submodules/osh-addons index a7bc58af..69147820 160000 --- a/submodules/osh-addons +++ b/submodules/osh-addons @@ -1 +1 @@ -Subproject commit a7bc58af4afce56385f117597e302ad37220f2ee +Subproject commit 691478201942922bc85aba20574b9d4c6e4363b5 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