From aad64bf304146881f50e1d4f34ab89b3d0ef59c4 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 1 Apr 2026 20:20:41 -0500 Subject: [PATCH 01/26] making UI changes --- sensorhub-android-app/AndroidManifest.xml | 5 +- sensorhub-android-app/build.gradle | 12 +- .../res/drawable/ic_arrow_back.xml | 10 + .../res/drawable/ic_info.xml | 10 + .../res/drawable/ic_message.xml | 10 + .../res/drawable/ic_play.xml | 10 + .../res/drawable/ic_settings.xml | 10 + .../res/drawable/ic_stop.xml | 10 + .../res/drawable/status_dot_background.xml | 8 + .../res/layout/activity_app_status.xml | 411 ++++++++++++------ .../res/layout/activity_main.xml | 89 ++-- .../res/layout/dialog_meshtastic.xml | 60 +-- sensorhub-android-app/res/menu/main.xml | 49 ++- .../res/values-v11/styles.xml | 11 - .../res/values-v14/styles.xml | 12 - sensorhub-android-app/res/values/colors.xml | 53 +++ sensorhub-android-app/res/values/dimens.xml | 22 +- sensorhub-android-app/res/values/strings.xml | 2 +- sensorhub-android-app/res/values/styles.xml | 67 ++- .../sensorhub/android/AppStatusActivity.java | 56 ++- .../org/sensorhub/android/MainActivity.java | 70 ++- .../res/values-v11/styles.xml | 11 - .../res/values-v14/styles.xml | 12 - .../android/OkHttpClientWrapper.java | 49 ++- submodules/osh-core | 2 +- 25 files changed, 737 insertions(+), 324 deletions(-) create mode 100644 sensorhub-android-app/res/drawable/ic_arrow_back.xml create mode 100644 sensorhub-android-app/res/drawable/ic_info.xml create mode 100644 sensorhub-android-app/res/drawable/ic_message.xml create mode 100644 sensorhub-android-app/res/drawable/ic_play.xml create mode 100644 sensorhub-android-app/res/drawable/ic_settings.xml create mode 100644 sensorhub-android-app/res/drawable/ic_stop.xml create mode 100644 sensorhub-android-app/res/drawable/status_dot_background.xml delete mode 100644 sensorhub-android-app/res/values-v11/styles.xml delete mode 100644 sensorhub-android-app/res/values-v14/styles.xml create mode 100644 sensorhub-android-app/res/values/colors.xml delete mode 100644 sensorhub-android-lib/res/values-v11/styles.xml delete mode 100644 sensorhub-android-lib/res/values-v14/styles.xml diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index c9f4be8b..2e4cc8a6 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -54,11 +54,12 @@ + android:label="@string/title_activity_user_settings" + android:theme="@style/SettingsTheme" /> + android:theme="@style/AppStatusTheme" /> diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 9591faf7..3887dfc8 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -14,12 +14,12 @@ dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:design:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:2.0.4' - implementation 'android.arch.navigation:navigation-fragment:1.0.0' - implementation 'android.arch.navigation:navigation-ui:1.0.0' - implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment:2.5.3' + implementation 'androidx.navigation:navigation-ui:2.5.3' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation project(path: ':sensorhub-datastore-h2') implementation project(path: ':sensorhub-service-consys') diff --git a/sensorhub-android-app/res/drawable/ic_arrow_back.xml b/sensorhub-android-app/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..14401611 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_info.xml b/sensorhub-android-app/res/drawable/ic_info.xml new file mode 100644 index 00000000..0355cbed --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_message.xml b/sensorhub-android-app/res/drawable/ic_message.xml new file mode 100644 index 00000000..baad9323 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_message.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_play.xml b/sensorhub-android-app/res/drawable/ic_play.xml new file mode 100644 index 00000000..ce91a8b0 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_play.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_settings.xml b/sensorhub-android-app/res/drawable/ic_settings.xml new file mode 100644 index 00000000..7926ba39 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_stop.xml b/sensorhub-android-app/res/drawable/ic_stop.xml new file mode 100644 index 00000000..41823516 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_stop.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/status_dot_background.xml b/sensorhub-android-app/res/drawable/status_dot_background.xml new file mode 100644 index 00000000..cd4498be --- /dev/null +++ b/sensorhub-android-app/res/drawable/status_dot_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml index d8d919c9..0b735b5b 100644 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -1,134 +1,293 @@ - - + - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/activity_main.xml b/sensorhub-android-app/res/layout/activity_main.xml index 16b74ed8..7778739d 100644 --- a/sensorhub-android-app/res/layout/activity_main.xml +++ b/sensorhub-android-app/res/layout/activity_main.xml @@ -1,34 +1,73 @@ - - - + android:fitsSystemWindows="true" + tools:context=".MainActivity"> - + + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + - + android:layout_width="0dp" + android:layout_height="0dp" + android:visibility="gone" /> + + diff --git a/sensorhub-android-app/res/layout/dialog_meshtastic.xml b/sensorhub-android-app/res/layout/dialog_meshtastic.xml index f8657c40..bfbedc20 100644 --- a/sensorhub-android-app/res/layout/dialog_meshtastic.xml +++ b/sensorhub-android-app/res/layout/dialog_meshtastic.xml @@ -1,42 +1,50 @@ + android:padding="@dimen/content_padding_large"> - - - - - - - - - + app:boxCornerRadiusTopStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusBottomEnd="8dp" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> + + - - - - - - - + - + app:boxCornerRadiusTopStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusBottomEnd="8dp" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> + + + + diff --git a/sensorhub-android-app/res/menu/main.xml b/sensorhub-android-app/res/menu/main.xml index 4404e5d9..f87d2ead 100644 --- a/sensorhub-android-app/res/menu/main.xml +++ b/sensorhub-android-app/res/menu/main.xml @@ -1,40 +1,41 @@ + tools:context="org.sensorhub.android.MainActivity"> + - - - - + android:icon="@drawable/ic_play" + android:title="@string/action_start" + app:showAsAction="ifRoom" /> + android:orderInCategory="102" + android:icon="@drawable/ic_info" + android:title="@string/action_status" + app:showAsAction="ifRoom" /> + android:icon="@drawable/ic_message" + android:title="@string/action_meshtastic" + app:showAsAction="never" /> + + android:icon="@drawable/ic_settings" + android:title="@string/action_settings" + app:showAsAction="never" /> + + diff --git a/sensorhub-android-app/res/values-v11/styles.xml b/sensorhub-android-app/res/values-v11/styles.xml deleted file mode 100644 index 3c02242a..00000000 --- a/sensorhub-android-app/res/values-v11/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/sensorhub-android-app/res/values-v14/styles.xml b/sensorhub-android-app/res/values-v14/styles.xml deleted file mode 100644 index a91fd037..00000000 --- a/sensorhub-android-app/res/values-v14/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/sensorhub-android-app/res/values/colors.xml b/sensorhub-android-app/res/values/colors.xml new file mode 100644 index 00000000..fc6cb078 --- /dev/null +++ b/sensorhub-android-app/res/values/colors.xml @@ -0,0 +1,53 @@ + + + + + + #FF6D00 + #FFFFFF + #FFE0B2 + #3E1C00 + #E65100 + + + #212121 + #FFFFFF + #424242 + #FFFFFF + + + #FF9100 + #FFFFFF + #FFF3E0 + #3E2700 + + + #D32F2F + #FFFFFF + #FFCDD2 + #410002 + + + #FFFFFF + #212121 + #FFFFFF + #212121 + #F5F5F5 + #616161 + #9E9E9E + + + #4CAF50 + #F44336 + #FF9800 + #9E9E9E + + + #CCFFFFFF + #80000000 + #F5F5F5 + + + #FFFFFF + #212121 + diff --git a/sensorhub-android-app/res/values/dimens.xml b/sensorhub-android-app/res/values/dimens.xml index 32b94bb7..f813aab0 100644 --- a/sensorhub-android-app/res/values/dimens.xml +++ b/sensorhub-android-app/res/values/dimens.xml @@ -1,7 +1,25 @@ - - + 0dp 0dp + + 16dp + 8dp + 24dp + + + 12dp + 2dp + 12dp + + + 12dp + + + 4dp + + + 12dp + 12dp diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 3cab0500..556cb4d5 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -1,6 +1,6 @@ - OpenSensorHub SmartHub + OpenSensorHub Please configure and start SmartHub using the Options Menu Settings Start SmartHub diff --git a/sensorhub-android-app/res/values/styles.xml b/sensorhub-android-app/res/values/styles.xml index 6ce89c7b..6172c89d 100644 --- a/sensorhub-android-app/res/values/styles.xml +++ b/sensorhub-android-app/res/values/styles.xml @@ -1,20 +1,61 @@ - - - - + + + + + + + + + diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java index 993ba96b..b214732a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java @@ -2,11 +2,16 @@ import android.content.Context; import android.content.Intent; -import androidx.appcompat.app.AppCompatActivity; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; -import android.util.Log; +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 @@ -14,8 +19,12 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_status); + // Set up toolbar with back navigation + MaterialToolbar toolbar = findViewById(R.id.status_toolbar); + setSupportActionBar(toolbar); + toolbar.setNavigationOnClickListener(v -> onBackPressed()); + Intent intent = getIntent(); - Context appContext = getApplicationContext(); String sosStatus = intent.getStringExtra("sosService"); String consSysStatus = intent.getStringExtra("conSysService"); @@ -23,16 +32,45 @@ protected void onCreate(Bundle savedInstanceState) { String sensorStatus = intent.getStringExtra("androidSensorStatus"); String sensorStorageStatus = intent.getStringExtra("sensorStorageStatus"); - TextView sosStatusView = (TextView) findViewById(R.id.sos_service_state); - TextView conSysStatusView = (TextView) findViewById(R.id.consys_service_state); - TextView httpStatusView = (TextView) findViewById(R.id.http_service_state); - TextView sensorStatusView = (TextView) findViewById(R.id.sensor_service_state); - TextView storageStatusView = (TextView) findViewById(R.id.storage_service_state); + // Set status text + TextView sosStatusView = findViewById(R.id.sos_service_state); + TextView conSysStatusView = findViewById(R.id.consys_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); httpStatusView.setText(httpStatus); sensorStatusView.setText(sensorStatus); storageStatusView.setText(sensorStorageStatus); + + // Color the status indicator dots + setStatusDotColor(findViewById(R.id.sos_status_dot), sosStatus); + setStatusDotColor(findViewById(R.id.consys_status_dot), consSysStatus); + 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)); } -} \ No newline at end of file +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 8966a540..b8311ab4 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -18,7 +18,6 @@ import android.Manifest; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -53,6 +52,9 @@ import android.widget.EditText; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import net.opengis.swe.v20.DataBlock; @@ -131,7 +133,7 @@ import javax.net.ssl.X509TrustManager; -public class MainActivity extends Activity implements TextureView.SurfaceTextureListener, Flow.Subscriber +public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener, Flow.Subscriber { public static final String ACTION_BROADCAST_RECEIVER = "org.sensorhub.android.BROADCAST_RECEIVER"; public static final String ANDROID_SENSORS_MODULE_ID = "ANDROID_SENSORS"; @@ -685,6 +687,11 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + + // Set up Material Toolbar + MaterialToolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + mainInfoArea = findViewById(R.id.main_info); videoInfoArea = findViewById(R.id.video_info); @@ -727,10 +734,24 @@ public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); optionsMenu = menu; + updateToggleButton(); return true; } + private void updateToggleButton() { + if (optionsMenu == null) return; + MenuItem toggleItem = optionsMenu.findItem(R.id.action_toggle); + if (toggleItem == null) return; + if (oshStarted) { + toggleItem.setIcon(R.drawable.ic_stop); + toggleItem.setTitle(R.string.action_stop); + } else { + toggleItem.setIcon(R.drawable.ic_play); + toggleItem.setTitle(R.string.action_start); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) @@ -744,24 +765,26 @@ public boolean onOptionsItemSelected(MenuItem item) startActivity(new Intent(this, UserSettingsActivity.class)); return true; } - else if (id == R.id.action_start) + else if (id == R.id.action_toggle) { - if (boundService != null && boundService.getSensorHub() == null) - showRunNamePopup(); - return true; - } - else if (id == R.id.action_stop) - { - stopListeningForEvents(); - stopRefreshingStatus(); - sostClients.clear(); - conSysClients.clear(); - if (boundService != null) - boundService.stopSensorHub(); - mainInfoArea.setBackgroundColor(0xFFFFFFFF); - oshStarted = false; - newStatusMessage("SensorHub Stopped"); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + if (!oshStarted) { + // Start + if (boundService != null && boundService.getSensorHub() == null) + showRunNamePopup(); + } else { + // Stop + stopListeningForEvents(); + stopRefreshingStatus(); + sostClients.clear(); + conSysClients.clear(); + if (boundService != null) + boundService.stopSensorHub(); + mainInfoArea.setBackgroundColor(getResources().getColor(R.color.md_theme_surface, getTheme())); + oshStarted = false; + updateToggleButton(); + newStatusMessage("SensorHub Stopped"); + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } return true; } else if (id == R.id.action_about) @@ -827,7 +850,7 @@ protected void showMeshtasticDialog() { EditText messageInput = dialogView.findViewById(R.id.msg_input); - AlertDialog.Builder builder = new AlertDialog.Builder(this); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle("Send Meshtastic Message"); builder.setView(dialogView); @@ -910,7 +933,7 @@ public void onClick(DialogInterface dialog, int whichButton) boundService.startSensorHub(sensorhubConfig, showVideo); if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(0x80FFFFFF); + mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); while(boundService.getSensorHub() == null){ System.out.println("Waiting for BoundService Hub to start..."); @@ -1304,7 +1327,7 @@ public void onReceive(Context context, Intent intent) { sostClients.clear(); boundService.startSensorHub(sensorhubConfig, showVideo); if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(0x80FFFFFF); + mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); EventBus shEventBus = (EventBus) boundService.getSensorHub().getEventBus(); // shEventBus.newSubscription() @@ -1344,7 +1367,7 @@ protected void onResume() startRefreshingStatus(); if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(0x80FFFFFF); + mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); } } @@ -1520,6 +1543,7 @@ public void onNext(Event e) { if (!oshStarted && ((ModuleEvent) e).getType() == ModuleEvent.Type.LOADED) { oshStarted = true; + runOnUiThread(this::updateToggleButton); startRefreshingStatus(); return; } diff --git a/sensorhub-android-lib/res/values-v11/styles.xml b/sensorhub-android-lib/res/values-v11/styles.xml deleted file mode 100644 index 3c02242a..00000000 --- a/sensorhub-android-lib/res/values-v11/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/sensorhub-android-lib/res/values-v14/styles.xml b/sensorhub-android-lib/res/values-v14/styles.xml deleted file mode 100644 index a91fd037..00000000 --- a/sensorhub-android-lib/res/values-v14/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/sensorhub-android-service/src/org/sensorhub/android/OkHttpClientWrapper.java b/sensorhub-android-service/src/org/sensorhub/android/OkHttpClientWrapper.java index 3d4ea094..5e63652a 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/OkHttpClientWrapper.java +++ b/sensorhub-android-service/src/org/sensorhub/android/OkHttpClientWrapper.java @@ -21,8 +21,7 @@ import com.google.gson.JsonSyntaxException; import com.google.gson.stream.JsonReader; -import org.sensorhub.impl.service.consys.client.ConSysApiClientConfig; -import org.sensorhub.impl.service.consys.client.TokenHandler; +import org.sensorhub.impl.service.consys.client.ITokenHandler; import org.sensorhub.impl.service.consys.client.http.IHttpClient; import org.sensorhub.impl.service.consys.resource.ResourceFormat; @@ -35,14 +34,10 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.TimeUnit; import java.util.function.Function; import okhttp3.Call; import okhttp3.Callback; -import okhttp3.ConnectionPool; -import okhttp3.Credentials; -import okhttp3.Dispatcher; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -53,27 +48,41 @@ public class OkHttpClientWrapper implements IHttpClient, Closeable { protected OkHttpClient http; - protected TokenHandler tokenHandler; + protected ITokenHandler tokenHandler; + protected String username; + protected char[] password; - public OkHttpClientWrapper() { + public OkHttpClientWrapper() {} + @Override + public void setUsername(String username) { + this.username = username; + rebuildHttpClient(); } @Override - public void setConfig(ConSysApiClientConfig config) { - shutdownClient(); + public void setPassword(char[] password) { + this.password = password; + rebuildHttpClient(); + } + + @Override + public void setTokenHandler(ITokenHandler tokenHandler) { + this.tokenHandler = tokenHandler; + } - if (config.conSysOAuth.oAuthEnabled) { - tokenHandler = new TokenHandler(config.conSysOAuth); + protected void rebuildHttpClient() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (username != null && !username.isEmpty()) { + var finalPwd = password != null ? new String(password) : ""; + builder.authenticator((route, response) -> { + String credential = okhttp3.Credentials.basic(username, finalPwd); + return response.request().newBuilder() + .header("Authorization", credential) + .build(); + }); } - this.http = new OkHttpClient.Builder().authenticator((route, response) -> { - final String finalPwd = config.conSys.password != null ? new String(config.conSys.password) : ""; - - String credential = Credentials.basic(config.conSys.user, finalPwd); - return response.request().newBuilder() - .header(HttpHeaders.AUTHORIZATION, credential) - .build(); - }).build(); + this.http = builder.build(); } @Override diff --git a/submodules/osh-core b/submodules/osh-core index b8db019a..a413b4d1 160000 --- a/submodules/osh-core +++ b/submodules/osh-core @@ -1 +1 @@ -Subproject commit b8db019a1e1715c1badaab8433e50b59a6072d24 +Subproject commit a413b4d19c5ec00d6bdef84305d9dd9992bc1a15 From 35a53b5907a0f7e9ecf1e3f3ec418542f4fb0f10 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Mon, 6 Apr 2026 12:29:14 -0500 Subject: [PATCH 02/26] completely redid the ui for app --- sensorhub-android-app/AndroidManifest.xml | 9 +- sensorhub-android-app/build.gradle | 2 + .../res/color/bottom_nav_selector.xml | 5 + .../res/color/switch_thumb_selector.xml | 5 + .../res/color/switch_track_selector.xml | 5 + .../res/drawable/bg_status_chip.xml | 5 + .../res/drawable/ic_home.xml | 10 + .../res/drawable/ic_sensors.xml | 10 + sensorhub-android-app/res/drawable/logo.png | Bin 0 -> 30201 bytes .../res/layout/activity_main.xml | 102 +- .../res/layout/fragment_dashboard.xml | 122 ++ .../res/layout/fragment_sensors.xml | 22 + .../res/layout/preference_item.xml | 28 + .../res/layout/preference_list_item.xml | 26 + .../res/layout/preference_switch_item.xml | 45 + .../res/menu/bottom_nav_menu.xml | 18 + sensorhub-android-app/res/menu/main.xml | 28 +- sensorhub-android-app/res/values/colors.xml | 106 +- sensorhub-android-app/res/values/strings.xml | 21 +- sensorhub-android-app/res/values/styles.xml | 239 ++- sensorhub-android-app/res/xml/pref_audio.xml | 29 - .../res/xml/pref_general.xml | 144 -- .../res/xml/pref_headers.xml | 20 - .../res/xml/pref_kestrel.xml | 9 - .../res/xml/pref_sensors.xml | 315 +-- .../res/xml/pref_settings.xml | 180 ++ sensorhub-android-app/res/xml/pref_video.xml | 37 - .../sensorhub/android/DashboardFragment.java | 487 +++++ .../org/sensorhub/android/MainActivity.java | 1107 +++-------- .../android/SensorHubServiceProvider.java | 23 + .../sensorhub/android/SensorsFragment.java | 315 +++ .../sensorhub/android/SettingsFragment.java | 232 +++ .../android/UserSettingsActivity.java | 1690 ++++++++--------- .../android/comm/BluetoothManager.java | 30 +- .../android/comm/ble/BleNetwork.java | 109 +- 35 files changed, 3305 insertions(+), 2230 deletions(-) create mode 100644 sensorhub-android-app/res/color/bottom_nav_selector.xml create mode 100644 sensorhub-android-app/res/color/switch_thumb_selector.xml create mode 100644 sensorhub-android-app/res/color/switch_track_selector.xml create mode 100644 sensorhub-android-app/res/drawable/bg_status_chip.xml create mode 100644 sensorhub-android-app/res/drawable/ic_home.xml create mode 100644 sensorhub-android-app/res/drawable/ic_sensors.xml create mode 100644 sensorhub-android-app/res/drawable/logo.png create mode 100644 sensorhub-android-app/res/layout/fragment_dashboard.xml create mode 100644 sensorhub-android-app/res/layout/fragment_sensors.xml create mode 100644 sensorhub-android-app/res/layout/preference_item.xml create mode 100644 sensorhub-android-app/res/layout/preference_list_item.xml create mode 100644 sensorhub-android-app/res/layout/preference_switch_item.xml create mode 100644 sensorhub-android-app/res/menu/bottom_nav_menu.xml delete mode 100644 sensorhub-android-app/res/xml/pref_audio.xml delete mode 100644 sensorhub-android-app/res/xml/pref_general.xml delete mode 100644 sensorhub-android-app/res/xml/pref_headers.xml delete mode 100644 sensorhub-android-app/res/xml/pref_kestrel.xml create mode 100644 sensorhub-android-app/res/xml/pref_settings.xml delete mode 100644 sensorhub-android-app/res/xml/pref_video.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/SensorHubServiceProvider.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 2e4cc8a6..f90eca25 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -52,14 +52,11 @@ - + android:configChanges="orientation|screenSize" + android:screenOrientation="portrait" + android:exported="false" /> diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 3887dfc8..3c6c56d5 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation 'androidx.navigation:navigation-ui:2.5.3' implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation 'androidx.preference:preference:1.2.0' implementation project(path: ':sensorhub-datastore-h2') implementation project(path: ':sensorhub-service-consys') @@ -29,6 +30,7 @@ dependencies { implementation project(':sensorhub-driver-android') implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.github.tony19:logback-android:3.0.0' + } allprojects { diff --git a/sensorhub-android-app/res/color/bottom_nav_selector.xml b/sensorhub-android-app/res/color/bottom_nav_selector.xml new file mode 100644 index 00000000..68a6dc27 --- /dev/null +++ b/sensorhub-android-app/res/color/bottom_nav_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/color/switch_thumb_selector.xml b/sensorhub-android-app/res/color/switch_thumb_selector.xml new file mode 100644 index 00000000..d733f2a3 --- /dev/null +++ b/sensorhub-android-app/res/color/switch_thumb_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/color/switch_track_selector.xml b/sensorhub-android-app/res/color/switch_track_selector.xml new file mode 100644 index 00000000..ef973a9a --- /dev/null +++ b/sensorhub-android-app/res/color/switch_track_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable/bg_status_chip.xml b/sensorhub-android-app/res/drawable/bg_status_chip.xml new file mode 100644 index 00000000..f5881c76 --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_status_chip.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/ic_home.xml b/sensorhub-android-app/res/drawable/ic_home.xml new file mode 100644 index 00000000..ebccd6ca --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/ic_sensors.xml b/sensorhub-android-app/res/drawable/ic_sensors.xml new file mode 100644 index 00000000..1c73501b --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_sensors.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/logo.png b/sensorhub-android-app/res/drawable/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..40a6fd9fd2c18723916af8426145a951feedd51f GIT binary patch literal 30201 zcmeFYWmFv9wl3VbL*oujV61PE@y-QC^Yg1b8e5AMMO!QCY|1Of!Ryzk!UoV~|) z|C}+t`)_x5byux5=QHPg)?BNqM$L{=R+2_RBtQfJ04TCD5~}aNf$v);JnZ}T(Ggn) z06-q$t)}IwYUBZObapVevNZ#_dO4bb%sj2k0RYd{(rl|%z4GL+H}+@(NI!NWkYR0> zC`xaS;%0TDsi1IYo+-UUiWe#ny%T}w;`w{Pr~6OOKWWX<KakV zIPQK-U*sNyDP)*uqPTGkggNoD{PY7UcNfF@LnWXibz~^#w{>F~p>Y~93&v(D6Wb$2 zXebFnVtGsDY`y1eA7zj(YrjbNpFQvzN>RvF&1-9ZMz6_1dYp5njxd*{OjotGDos~0 zSL9q$wXst#;L){SY58GoQ`f+IwPv2L_f0sj zrk>S-`g&D$ZO=$n_U(#Sy1vJnUxFZ`4%^jW73V3JZJwh{Y03xQyXLv?JShZG++VXD zzTUO2UAF&{o*&R$dHr)wD#7S0$=Dcv;n*3pt4IbaEH*y$ z8ReyOj?N_QBhB~*iZM19MJEq_4AruKh|i75kEC;cQ(4!W5FTcH4In?l>IlI5D*Wx- zLxp{I#vlDjtq=#B2zq2h&~HRY(mlZ2%?VgB_NJktj+BgJT*~ML=O5Ut=NYZrz0%` zAb`_Da)jorc_assi6Da5XxQ6OTUR5XGA;|L{%b3`JGC=qV8J7s-e$UW{x|#9Q#=JI z=@&ct;hZE$xoso<+@DcnUw=aP#C4)nS>u!q0pl$S_XtKjrET4K2O7ZI-G8w496)U> zDh`z-;0~!>B?Y&o6m!OgA5_sTiu7JtlrS7;IQkYF8aFyGJl+X!d`!K5#N!D%X}pju z2wp2bLkliny%Od>=r2l=Z6a1`YnoH)@R%jwuw#VLhM#{wQB}5EJcLh0kN5Ox(SB)% zHa2L&h}8K)GG1V74T+2368G}k;Bcjcc1)8#@q~Hel&VDx_IJA-QMhu&$8af6ggrfr zB^fDYTQt@_XereXEo8r)>{!&bVsDw7!i`yW?HaykK+yGngecITBnnd-6i5ej#~oe^Az0 zN_|+T9URu*@wj4Ry~>RARyI!F{zduEUIuMYpPL=zr9AzSmUx>q)8OyetHBOT#QyL zze}4ps(i2J?NCeyV1rJD&ni_h`mC0tl5o-ut538IT53R$HpKfwi&JfBBOxFaq;pe% zoLFAqx0n+#e`KSEctf=@4XGi`KxftpWX>CFka7PrjZmmmxSuuPnPL>Q%W`v9M`HYm z%N)hL1ronI_a~jKNF(t8;#07|A+@J}F{=|OG??Fx6m;D@rnlSESsA|;h%+J_e~<|>4PiMe$StW(>5?ToS5VTZON+hT<<<1aa7 zVBrq7Etk>jQwT(=AQScphex=Y3>gl+U2_eoF>v{;=yh4#SxdZAK;eu(Ge_b#NXc*y z8#Ar(l2X5`CSp{5=q8!&*S1035~II^Fu^C-9Zbd9v`XBsUK{xsH2@|NSMkYy5b+!>;zSvg;@ zRp0`<=U}(loels61Z9+ppQ0D!jKDg`sAS3$$TOE(KOL5_z(Y)fNbtcN`Uq0WCplUx zEOKSrLu5WnRdi!XoZwv2cySOU1vmf%Wr@Bm0CkFxO@HD9POcgHY4nCH1{-Ts6le6+ z_n?@GKX8|D!LMDF6-7>65_N`v6ZKdeJD!FlM^wM$QUu4+bO3gP+mz`A81x;_L~nwW z!Ft$?$;t2ta3fRtN)pK^u)+ir)ddkSG}I1A6haV}lnXPV+lFk`W=$fY*|PD@&TN`e zpP;*^?b11|prXXAZsa%T40%J79wCME5wv}@;IBfIN31{rqlOcSLOLRsuf``K7f;_x zP{RA&mXh2-vMs~h^L_gBj9$E^Q1aO0RBDv5maHk*R}e;2`g5vx5O*y*luMG2aH*dh zW{eN${4jEvrUa$6^i&f`Dhf37I2ExBubN&EdaMY^?n#>dnM zqNzclOi~C{hE`Na$q?gOS(B#|1YcyG2|2>fOc%J4h3sc zC-659MFPtM4Fn?#DNzCdIP>4qB;lxFH*=G($yiIIF7}rE)?{kj!{>#6g~}6^jz=@f z;c62>mV{>9*pU%47oiFcZ*K>Qe}!^N?E;2!8e@-@5}af^Ijx%*Bn+~6jw;+#NkJAo z8=mA62tHHQAYHXWGn-fZlF~3Oa_4`rq~AZ$8sR4wK)2Xj)?Gw39%Vp2>G%K`iDaP> zvlc%tkJux-N`v6mfW}7C7*U#?&X{R4h1;hRJ+G7xO-G|3vxdBC%MH?kpPjk!xbNFm z*-1W1{fhf?yLTe-Ixfobp%D};0SL4_N>eR#hE`OK9l-va^iY9fPcI-Uww(jX=V}r! z%I-=MVjzVxi5kmh{zD}T2R^F?aXeC96;&MYy;jLg2m7VqD#U+H-yA2A= zu)5DSuS5yQ0uqt}`6hnHykVsPAPiAD@@<>eZLE(Bvnn%74~%X@+sjZEY%EH}Q#9Mr zpvw_35GYV)t5RVc`ccZ?7;N0*2JB%s7^h;|ifbD_i-=Qnq042gSOuzzMq;r+?VObw zD>cDt;2`rnYY0H%9Bi;1!z&dmbaQN#%o_vJ@f%WZDt}<25wbC)u_0Bj7)NOfbxB1^ z1(82dY1HGQRYA2=6E1wn zoI+Qpdn_W!bPWKRvIhkV4Q$h53A(y|6e&yHJ~|a{F|tt3_PK&nmE<=xClM6UQ}qLQ z4;*32`e8sPtMU*ae;OKNJ34+C$*mEhGci@2n;K>^(~y{Qhy_3d^kVZ5WnR1!i&D~)+#>CU;E_=yQiu8gKhE%++Plp2d0tZID+*Cf)o%?swH5fp zh+rWzYwjDSA&?zh4%}#o$=Z`w-u-FHcv#&kkI=FpG%B zY4A(&XC5>zkmh0ESTf9%fyCt)iI!8_+0))xbeUeJKvICa$)Rr~C-@{2k<(6=3v7i} zgx0qIofbwNi<8*2luisQxKAiTRiTO$)BRHuF(X=>7oXb&*0$_AvrAD&**FjxvNTPzt5ems1f`=JoKK_HDT||D+g$@aYY$ z&^4byVQ63V9=yi0kh5ialvK`!=0&smYdws^W3X*5wfpPRF{c9-*M zCG_}x*6kF+Lt%wjsPH^(Rb;@vEv$IdviGiEZ(9lcqcBEVh}AG~By-t0i!j7;4p`je z$GWvrHToEClj!c%0p1 zkGTt;U-v6wQEy@vsH&Zh%Z-j0;&gS1Sayxk%TMvu$5KUzYRJdtVIq5_`oU?hhgvMBDW>w1hdsQZgE=hHwk~lxB#F<>T`GgsJ| z2(>1DFokl{OKXJ$j~!Yom$p&@=~qc8q3TRGk~9n-{L&x^$s{WXVz=mEM%d3i7>t8= zy-{uZJ_g~}$6`r*W1_bNO5l0J9lWg~V4!FLSzwtPQzZ;^nKICE`kOP@TH^}f`T(Ui zi@0(bS_qp-mcb3wKC6R140@__xCFz7GpCc#2~Q7m)zV zJ@7d$`2-KQS7>NGwa$VRnnBDHEOY9{2AUehPp(l%Q<44^6UGqhX+B%A1v6xH{|x-j zif~pKKo@`sqgF39dH}NeCB`O9HG}{SAvx0+w@ZtfDf#hxI+fn4Q*PpK3R&3!N~(!& z6(}`qBs9StppZ4pz*YbtOH$ONdnRyos}X@Xh)ITbYk^3U)7a7sDEP=$BdN_rhb4v| z)43J2^X(k_M#EL+I#BFsdx_?1EuwjnWk^T%nIt71f#TSvICfs|!?sEYBG<0TQx%s6 z8BC?6Sa7461SR<{Cd`!->Z-II*xM2BTrzJSFSPG;<;6!#91ZHQf&myI_w9P5hv*em zJ+@=X-hspc%}I5P_`?Ss=OsWLW6O4XWa}XUR(n`5%_&SXr5tSVSq)B)m)r@gtleFv zwpn<3|3^AAL-$#Gd3`|xKw@=mMHp-W>Jflq&@F2k8w$!A-0cpM4<)TO)RYqJuV3?! z!?YXX_@)zD*k}8-JfszF;P+X4C2Bp3iq5`L5K?420hS=h!tw=l}0R6K%$pu==H9TafcT2e~FBwt6zeXPT zDN5{WxAr=M*P{mp$Qdu%vd_1$mnUwcNGswtjSXGlwQgJXgs>Z%? zR7H1?oP^Yft6NKGj;3xd(`AJCMRq_q6K+7uhq z&g)EHSZd?ND2&WCkenJQ$ef6c_Daf$$WO|P$r%SPC`U1Y0rK*eCd(B!w-}4fp-1Aj zmS{I!p|2uB#iCcqy;4P61HSZ`m%!IDFqU=6iF>N(K(PMlBSB9lFg3_ns;QvGvP-h3 z7#r6PNzVGFG}6DL2SFwVf2=YL649gNU0~uu2^-Z_skiZi$jm8VD3F|+M$Cuf8(gb} z8GusS^E@GVD2n!8$b`qz=D5oX8a8187cq^In+W!+b83sS*HC#4hs{h`8vu$DD;GC> zJ)fHW&7-@&G(wjp6mJ|ZZ;}uv4p@g(STfqG4>z&U$cPYff6nTl5Rfe>W0s1(8>;}h z&S-=noKZNFU`;zi$cNl zfOuIgV%`MaQUc%8ZOlXJm&xHO0%=^?hTII$M@nd`cGF98CYkG4p+P#&hf;IC2@Ui; zx3rjtP7LAu$r3TPDoCiLewqgh+z}z9S7+3%kS*c)TT<4Y0U8jx^-VJ|9@gAUn;?{= zIG7bxvgZqr?D?Hc3*?+t>`*hAL>EVURh{gyl37w3Q+5*D)dwk`c!{89UTN zsh$eqdS8mQgEGf3VkM)z%p#OkeX=kxk-WZ!0}FJcAAY1>oTL6`5b8Jd%>r?1YzxV% z@o7+Mr?Noi8b0wUoSFomzJ?*xX7Nc0tou(r?Mn28Y~YcT85x7-g!CG(nguAes5mfi zMoLJEbTVQCp863HSEDYWfy~>CQ}pF5DS<4!^F)lVfoPv;;K#0O#Zrm@IC<=&D3HYk zdik3=Q*HIjGQD=;9=N@P_72?;4;X@dQ#Q4`P)ARCGI)qH1o`^$P2UghGvs&$Ofybs zt7=N-(idbNNU>5+`^7Zq6KXVz<7){%I8zRl5i6sL$4#3fNlBnvACNCxq>SsMDJx$i zvAAA-C3nq^6>8Nd3DgO|Jk%bNBq~O-;;~e;dPG12vwUVqnfS*TMr05OZDDS(tzmH{ z>GrKJwjhIr6|bc-&Rxv6syuCe=~53HUzHIM<;WWL6(VDQxF=SlmK&~mzcNIAzMJy= z18y=Jz6%(~sGAXvMNm@$BM14Sb~6}`>H0@1${i91rL|pYc}5by3CcP5J&9{Y%#M69 z>6l05kSrID2EN~e>$Lit!u809DdA_&~W5bir43l8#9U7h)!s+~FD5JZ*HRnKuB z#+|5ZYY3o)Hz>hW9VpgE?~NmvR&lGXC3Wsry}0wS?lTy^Y|aKNWvc_)*0F#OnLVer`^j23X4i7m=^`)5PHbUS@;?-~T(Y|dpyG9HY1*(JK zDJ=@iNNWXf7qGn;eSBYy^9=MXf zfsbwfuvyWcu^G>9mN%kKmKN>TWtyttkekXI%CdEms1FTipB0gK${<%WgqV+;puTPiI3ttf`{ zwybGezW@QM@Q8vvuNC6oY{`IE5E!-@4+|geQGhF!BQ8+b_s=4YZ?6f%60I#(d8qIb z`OEpcx`UdM41^8^7X79WbGM zkaEnorcy zC!Ibm;*LNoR^Di!#b*a4Bbopc^fH4H@(~t)XONagOF+EeSGn6VHv@SNh4^;RRW%V= zTsO?pnaB(a*%2-kI;%+T$eTC?63}u@I{VWs0~K=j2SV7OCs)gAWtV`XhL#<4C0`W? zT6AglD{^XAN|XcRpv^4=<1y7TRzbX`->7OzV@z{S_Koh|T&hv@4EAt&_r#Lu;cFK& zg`94YoX=@N;O}c1Hcz`@<_VUksMpJ!=0-!jOoS&Tt2=C;-zr|vD}^gDCxmsW1YiXk z?bx7c6{Gah@CMxzr7AT3+vd>`Ty}+Uy&*`YqlI0_6xVs6VzVm%@v{rdRy$p&b!;ex z&~Ix7MxeT2oZ?hzq&Ra)p_N4DXFhydT{rj3iXCF5Ts(noZE3@qJy51X3hJ^h%Qu$WR?iyfl+U=zxNE6|O9aPSPK7jr&1HPSilTiu3cB&Az2IfA9@ zjG34z-%5ddhX~0;mwuW7#FdeBi{^Dkj2L2D1*^R1?y!;8=33>?Dpa$z7galL`M^Vy zV@FNsXP2e$TfdpAL6aYQciLuV??% zV4%XzVDkl4&G(hus_Yro+?9SLgp?w-nDCt_>GS;{;x^>TiwBq2zORW*!}e0u)G1GG z@HG#*e^%C*$XO#M`96sHz9szfbmXIpCy6u$w9i^*2>%HYJKi-aBziQ@zOZYvafRl3 z3%+!Cx!jP>96(-|&(~_9P{T=T1QCwIcjl*g3%fO@^XY>krClkKK=Ap%?7BfXj|61M z2dG7ut|c->UrF;pUCF1~-wL*K#ZX{4EK&p5kGD>#o)lOWt9EY)ofOK1$>cu+RW^QnX;|rC_f=9Rqh==`Byp!gnhwF^gTy1s7nxCPnZ|cg9yA0k z;7^$^kX3uR&If0c@o%Yk;n4gRCZ=DUE#L$TBizuow16ZxNuQ_?j}nB=nByGt{6LNKEcI72+J<%=qg0_iG2LTnF$uLnS}h$k3|Q$5CDRs*aZJA z5*B|H?{4X^ReX1NKR~o-!k!lzavOV z)zLRvV(5u~90Et8X1gh-<0rfv;wSC0VhV!~r4%1knu$&4p82;~c74nD@aTfE?!xhg zNexL|ZwimEQyyeN=Td6@sH1(M(`jBP#@x9R^_4)Akdep&bm z!QkzU5?;%mzj__5u;g0Od`uh9=DMtrDwr*UXDubCV}S(-12(t z7GL@TmFpq>rKX#q(X$VzZhCz75#35oCO0UO=7r%3KPZ(mLltf_pb4pxTD{9K23*+S z7WsIxK9XWHX4BTx_CWFZJ6}5+7bJ8@XaKLZBWfp~Av^wf#``mHqcuFZ3`HvIOYA0A z&7xOB9fc+eE2IwvkP{_e`XJ(eU zIJ}!%D$ZSGX#MRI*Y{@(-ZI6xl5&VZ7oxh(3Udd^THRhqIq2mu%um(U$0d&fd~qwN zTt^6wx08KJ$>$1|vJ}>D^~Wp%yT~otEwbg`VpQ(DyCnG`Mz3Wdtbd8&)uw!E5YwD` z8Na@8AQgN$)ttNr?g32nJ6k+)i8p7Y(7p=M@Ka1P74hN$3=k~ zay{{-dy(FumB%G`vI+P+wTKdPMFZY=xGPSh|tIh&y( zBvVVrRxOr{ie%%CMJEe>1*+Xu*)!+nL46;X6&1A;R}!=;EANL+O-bc{ON;xJ;=lP8 z$Ng%7Ykhx05&!h*IWQj@hiYyT0DuUy5*JsN6&L@nlAQNKoE*O-L771jyzs5Ev}iTH zWziuraktsvib#`MF?70AokDfE6#2ITS@GoEU7B_rqG#aK@up=7KF(f!rteb2fd9Q+jB$EiG?0_(ISS*jsDe|6*@un2oBLaglM{Lzn}SOVXc z1buybW3Cx4JA|csRax-2F{AbrxEXd)b~6>_kGeh(8JWqF2~m@99|sR5=%wrX@98;f zZD*pBXJ=B!17k{=(mFFfqkQ(v;;E`2I6&e}$DY>BS+LG+xg7h1M`uYtp;yisOM&wx zltW^)qBxk2VaRexhBc}1S7=1Q=ymxRWlA-qR6NXlftd62cED9hpBVjC!%sRku>km9T2FfL9-ZU^*&vlZJiEpi_K5>P+e8dL-@gT zWbd_81Xk}gQd)`%d?pTdOh%>-#%4^Oc8>2gQ~-d0u&1MuiH(^n$k@!n%3ctB+1U#Q zS(yrgH8~Yo6dlFQEUjd`oy|UaE2)`y+q^F%U|}Id0Z+bn06Q~RBao+^t-TAMry%%m zT)y}HznYoBpua_2Z3Mwuipn5y2WK-72NMSq3!|i`l{*_)2oWUUY--M@Dk1exi1#Bw zu%)Z3BOf!fhldA~2RoC4vjsCNFE1}M3mY>V8{@kKql=fltC1(8y$ktY5dXlCFmo|+ zwsLf}awi*M@7Z|hPQY5CkyXpQwR+x{JnsR^^0t(o1s zsLQ)m)_*f8Evu;f-x_~WU}0tF__x+O+5aZ#YGwXEWc{0OfA#zw&c8bHuKwS+|0exk zvHvanE~Ti*C*fe?_LqCI5`y5r?DLsAm{^(e{k_Y{YQ)OJV#>kDWo~TB$YI3I%gAfY z%g$)R&dFkK!ee5>%fkCFP_p(eu15AIW`9AwgELvZRZmHQ|$TaJDmgA5JSfBMUQTM|+FEJN_b^PgGe}5X{EJ z@;@!gwnnbz?+Suo1uJ_u&;RLAv$8Y$=xX#Ao2*m4aVdm`e zo{4{9va&F-{{#20Vc~lZ=ABrhzw-1Q;BSrhSop-9&5T?foYfp0Yz4u8xdi%4^Y8Qq z3H)PFWUO4?CA|L1_D5?9Q|Vo*joMF1p@t@wtPk=|1jcWBG)_dcPHdHI|o_0KEP6F6b^#d2c~*l+ktp0FW^M+8_W~Ie71laIUh7l5j^b zFlej<@T1a<000OeD0ekV-MMo2&`1{u=2VKnzGSZe$Q~^2bQd}E+igY(VILh(N!1zqSA0QwnkqWkl(LAHR z+qvS;)MX8A6!ntXhB@wO*ZbdcvfQ_X=N6>Z%x8F*$oWQ`!Z(R>0u!lHZlH{;>kRyx z?dJp?Na88Ef4JIm@lT0>0Lnh!yONS+UlB)+0zu}14&1!LeLQM%qavBfm8prFR6B6v zYsP4LxA}_MZm3X!NqWUbP=okz{VULb-7;wU%WNjp6ajgejJL#Zk31RipKtcX5n1qu zMXjF?(D>!jUd=u#PPX#NFFo4ih-hItB^2+2ajc z;YRuCV<)6yUkKY(7JP#phvMPu|FZ{UO{$s;lyfGPV#Xa?-sEG}3&;)NVrtm>^!qt{ zi`a5bf6R@x)va`ys$P%XeYtBOhuSSA-S=^dxQ$5|CzfIftEu+;@qo;;1k;Y~_G<{* zLk|+c-$)`|AxRvvV)~wpT4m9RPT2;|AmV6(Qn;8pzGuM z%^22fPhYW zUm>w)e|Q!G(-$AyHy3#`f;!w}BI9K)-n8fH7f&~p=FWb3XKR zS7>F4;@uv@*q4=!)hoCoWbFP~LIAh}-w8Oq>$@QJoT|&I9SVgm7a8Ec6-bXjFs5pj zZw}b5%$7ok{54!)csG+*iJa|zZRwPrw*R{!?%1xi*G^VB)T0^p(=OyB1D#^N{`ME4 zZ|5xF&d&FVS&1+Vk>@BSDdtQhu_x(>)-_S(+IhT)-Y{FvDTg_Xy66-*Q0ubWF2n2w<@a0I4 z*Uf+if6Cp|%cf|+-_UE7m}~<42fxT14dR9zc%Ge2b$(M%z!02&9fzUVBjo;3>eD-6V-tf}_MKZqIAlM^CRC`0Ni-Z+q zp#>a~FVc$)TYMu31Q4J(Mp@ZiWx}>7Y7_Vi2ZY{XS{)drn;xOMD|$cdpt3KNeCStU zr{zk?v@dLMw;^$bZ+!3{rYUy5Ba%087PSQdQDM^u6-`ig`Jnj9LQWY(Dj*}_A)AM9 z7x!I;sGNU}wd)}nEE|vpJyHN}llKYJnKFI#3w0Yp@& zloRfxvg8-vJsGxRwF&?EKhvrw716NOR^vHHUm#zd6Rg>j-a_{I8e zDef5I`tnu+&Ya?|P(sD=PJXSwydIf9Z(qXBI4Lm9((vMk9C+Qm zKpFSebg?a{UiBwb){_u|g1eObrN}e)6IO49vNtV6Bfe*e_CnsvM%ixt`pxg2MN`9x zi&ehTwQ?q0!=HvmVga8PW{|f~?4_uGU()y9Pw$jvke`$nx=1k}G{KAf-ehOz7RP+; zr=9euA|&zMlPNw>2kK@lvVrgHqYaErun+?;Y$LCzJv1+DKDJMBvAUi-_!}X;f=Qrc34u}3e18yK>-TfltnE;n0O5;5tkB2WteD=rnGtbj z5T_F~VaAz1-{lwqu@^l3Zy+}9mtcMtg*s4D=neH-2XT%=_tLKaxk7aydoJ=WV6qLZ z#R|=V@1`j-hH*kUjNV_^W|I87pZlb;tLb+v-P8tRWa(a)>#OU!zKN30TNdpPBfX?F zZx`&H1QNPu`KyAVm=(!;`Z$)z(vEaz7*-{F-yE%DeodfGClry(nl3=$wSE_3|B}Hj z^N$^wKGfs_WHwzi18{N6gK4ceW~BIelGd(SKeo<-OFypq<4bvcAIVfqS1 z$(-8Bz;)5SX4%GFOY)yds0h5L#>c&Hb?5$UynUNew@$XKI_IAxIev8wFW7Ksqky=U z6>Huo01q{u)e3~40H54yE#5TCe#(iAriUIh~D3 ztjiFXGzdnD1GspXhS!q~Tjc?A$hxCEE7U&c|hHJ-q(Ov5XPm_-^ow5E@!xZlq1yX-! zjw!B;>JKeW4BmI@o8bAr8z^69O0Xl+R90V`VEq|=!*X>FuWDIuLA9&9wf4es+0qHx z7de9Uk5j%Gj|~KX&_Y|AznZG=LMjSX#WtWGAd)vDf8EvWIqu=2@RJSLAZUGj9mZ_y zBX0t~3IMi#HL!xX+Fu|Km51H`{R`cxu+I@;Cj&NItbsyH)*CLVS(;+?Eg?JS+b$J9 z5$B|$o6+4%TZ~QHo3*alOwwFKHRMs&LpcmULt(&xaj*ILe2mBE{SR=@J^{F_tjoUw)ipMk{*>7Qhz+E5nBj`xa+Je1bhVAfFsm=WJet?p$cwnF5 zv(_Zb7Nb>s&uy;_8nKo(riR^SR1z#QDL-&Jyvp#1Yb}oPDT&SkwqdTjo~-NuIy4v4 z+iVg8C7HMHGEXa!+(E}+l}O?brZ`RS{jR8RCf{*{eaLHWRXaWvZ19?G@frIn_!L^5HRvWCY~8U6=(H1=_5f`202(k?9EWDlBSc&QBX9?`JF-yE zaIlX8zhFX4ktc5A;y#`iGjbYD)j4dvJ(sL+P2s;{rpu91(;Wu&ua!P}WC09e>JT8X zC0B(f=$U(*wKj*B&`uVf=QmHhOX`7Cq~$|Gx)6W}w9CmS!ec%E1$(1j?}i?y>*Mu5 z2FU`NY_ij-{@PGn8r(JYh=k8KMS%~8C@I4HGQXcgs@jnm;wpqU)2JU)5g?I8p^G8$ zut9+e}rep!B8H^)t4GR8Rij1q`>mEp(UL3h}Y z*Cc?Dpb!;sYR7(Y_U-eUXdqo(PxG$UU?U;yUH6jB41TUqaZJiE=@=ZfiE!0=?S;fg13-8d`B3vH(dK&o3S21hm5o?!c=r?y3}liSV_K{ZPB7 zP`uU5&QSPxbePC9CrOpuAv3o_aEgJUP&K?2wGZc6yITT1^{q@lzYY;WQ2cWJ8V5Q3 zO0WN@)?u=pyIygVh$xz&}d4NACkive~DMs8Jb+pGaQGWByUE8G@N!?LLZQv^)Fb}fCFXSR4que*( zX6DihnXgtp=|iB56sr3zei$cuf$cp(Xo7thehmyyhzJdZXdpP!F$zOZ3d^1e%wkT^ zrTwyP&kLPEF+d;VC=V7${%e;!$}tLz^jC(2s#!Ni2`3j zj11i<<`RDNcocutf%?|g%$h-k>Z~K;Cpaw_;P z(^XGPv(|!mU8QfR0}Txg#s(olt{1)A>!+8!8fSZMM0RK=3F-++IV{T5cbaO; zn%HQH2x9|tJ^(D?`sKM3KI7uTpAl#|;S$O>ex!4D(CrcsOk~$9 zh}fX;Q}ikZN!df?Nc@+iTFxMhG)MqYBr(!Q#^Lf7EGcxeI9t}rp+ar zkbqTzNZ25)PPAkZ&_~2f3>ip8p|Zp^QlzoYJzm!JP(yu2Yeb6z+_}Ol8AN~xK(=fX z-Kz&(x_Z@}#5RU7o@Y!pJw4@D)PvhrF1NbvFyb*hPrg}Ws2$8+!}+MekhOa-y9fxH zNhMS_bvuRffdaumJ5&>E2OXxJK@1%cfdaj34>}bp6e<)#!nOatTUc~-O7d)L3(kf~ zz9S4!fi!xCmd6RaG7s|cHOy1MYve{B*cZ`TLN^hiAQ0!lft%w{5PBwoG*`EF%%Ca} z(^hw#qI_F^Xp;(AfU=aM>Nu0FBXJYsbpSZ-uQfrfI9Z_-`TCa66j2yT&r;a(HzQql z2pYzUgWiW?fL>O1`a`tmyRTT=jXbG9cn~l-G(z&hJ&znHH7T*Z31_5%9m2T~Xnqv( zz%KH_yUnq)?Sjf;9xN%&0ya9Hg>P<0bk_8w6n?hbCsj(a9A+ zNeA5%A}{J7EiS^p9NRY-B58@^drq)s)W&Ul6Dch9yF#rS7Vw4CuxC^uw3!E2!VZ)L zc5C`!W72E%f95$^zBYjoGmoGA`t!ZKR{Hfz@3g^*d0A9c&OJsS#!JeuMok!M`Y)IT zDCymoG$5Fgw9k|X62o{ayZM4$k9g>wg7;Gq78paKoT%VJ4}Q>Ggg^*kH;@eIa|Zzr zqq)aISY@}py>lq;4#TFtb zn$TeS?S6B-2#-FVW*Dh!zAG7|KH1}BW z!NdNxZPW**H^fxdIT6|*CrhOH&)-G#ukm7Es{2k70O463rAlZyE*Vp`%4MHmNAX$Y zm)tgh(`+F96Atz_q%KiuDsUi#+|=^jB^AdA6XwwV z08$BTUKCA(KK6pKTx}yz>|Cp0|<4Y`N#~T zqBp}W5Fyhm3mwHr6XJBtcgWgu&?((@M3I64@MPz}Er}2gZA7qORwr7D3Kg%P(sv?K&m?-#(ZTe% z9Z~4HDgiPLaS=40;e4W^R}q4xI}%nurEujGA!ET;k$cOxkTq&N&AZ5cd?kZ%?xDr|Y2EgB30cGP0 za2;6*SF`~*_z}dMK~Toc1%rq~-BJbi zib%0=H00r9VK&r49Jde{FcdUuK#m->7mz@fsY`Frj;yN)_xL+oeuyY0I>e< z>Hb_p`aKMI>;P!Z?T4+w2$;VR%D4sa_UR8#s21KmZ@^c%0bUk{TTCWkz+fI&3*znt zWx`z0{4p?&tOHE_v;Y7cU`;J>&0YjfTrf*EbQ1s^G^Y^qti_Ng%z%)c3u)9v5c>3o z8IM3`0^BuQ+H(Vh08}8<)OpMEd+Mr?+Dcc$3HlG5i(UX3=ys1q6VL7x^~>lBp5cce zx*$ZGK}%Lc(yHL{DsZ`EC?bVK2`EVlT5dvp>k;r+1nx_&hwIvh5PSWnfEqb13pGo3 zKr1-}Y4k*b+pd10r+_V08uAXz<7%N!bOToS2lPg241ojq!5FZ zCiRE70uaY8f;@2!#DYN}k_@w{4(gUQ&~~l|Z*4^2hkpWEYe0j?APN*l{V^EXBOx@e zvwaWHE1>iNhyrlye5G}^(mGXToHr7H7?{|}_jmhH-%<~K@0$?K7-U(7A|*0=iX=c5 zC`3vU;a504V-9u2-vJJ|FIxg!_YmT*K6qLrfxdku%=*tm9zVAo07AcskfvP)Y4}7? zUj|rfBeY%Xplw+JvvfPSW&j2Ue7ykzP?sBokVJB->Hv(Xi=Z^EOZgtfL*2Ix%FXz9 zX+sd5yOG-)aPo5*0EuxyENT4Nu8=ni@%p2H5rH5Gki|rGjv^+AF9`|aQ<8|Zb&8E} z4)xt62o@{>uK6M2ul)?%P)}{eh6;7-dU&q54)WZuLo6B#vEN{D&R~=uM10!{7>7Ot zk4C^v0IXw8JRSuY2a(;N1fY~aXsrZuXCwnu9mc&RQfb`4j0}`E)S}~TWxD>zDXKs` z*NFe;>xL^}C~!X6Ci}s{rJw{5X`+%>785{_1do@2~ub;%CWiPm?15|)(-VNZPW@tO!hqm=K@TM|wgLQNi=oC;ez+({*moMS7 z6F{I8TvNfR8wBwbhrV~VD-#`OBb*W-tsQXANq|{}*%=6NcP9xT2;i}LP(qRbph?b8 z7=8)SPfqNTb0EI-5fG0L%B1;Vp;l-gJdU1LTM@+vdHzjs%~=E?GY4FY1KdRH`EP@@ z)qoo~@n>=lm?j`9$x~+u1J(oqXdr3qcd$nvzv4ss=32C?c)&Rq2=XhUzx_O)?Z-!` z3~q!#34tIGNMeHcB299?RcP6rfDdPg{^u9a4(*0(_O%cvF6sUPB^TuRw;=G-r{G(S?rV^3MLn=S3ut zAl`1(rT`#r80{P)&;;S_#HXZ(kv>C1^r?qn)KtQK`E3x2W}aYW6w;(C;s5^Q@P78I zfF4KW zG{TR44aScDB4OD9JRX8xydOgTU=UBAlQx0~0KynR<<8GbDRdQ(+9mz`kN~_(M&=e5 zBtVd~n%lXbkdun{v)W>WpM4a>?T7n@2LQ>_u^4v-T%Wlg{(FB7all}x8{b0YiMye1 zdafg)XrrFC{X-C!7xYBZWlsuy6)_*Cv2-NQ1|~ zDA|tqbN9hK^j7z)h6sVae>klw|o!2+wKF4gupcw#O(*?gnhLD03ZNK zL_t*7dY&&Z_N@n3;}C{+!ww)6M3fR0N;Wx91qxy~qz40PTH|dFqv(V+fu{=q%6=w7wKX#qeNjZV@U zA14b0`}^80S)H=I&KG7xTOs;#5=UY<2jc)swr$}j*-=NFbOrq1{td+Z0z}__7SU&Z z2p+13w(=?HM-M}uehp~vaXohA^|B+NIr)Gq`}i;Q`T-DdH3lO4Az%C@c)$C15N34R zO)bviZx`2xI^;pxMwF)|9ZW1!r}*b{0&bmVF@uY3#E9JV@2xHSD%5l$s3#pT(;6=8 z@0sTBE4s$X23aH-D^ZYhxUNXS^#0dBpCbN?Rp~uIRF5m z&tOQmKL(Q38%D4V{Mi0ZyVMfa8}>&vJq-ZZ@%F!`03b{697pHn=gvVJ?&6hvCJgif zi0CGA+nQ@<9#CEExz|c-m)`U0p(xTfdh8DI$(^(E=?`WNE%NBXY^q@q(J+fgct4ow z2}lfv0)eAJ%7n>cFd2ivI82rR0!R0VPZS?qH$4Dl!bQ-F_aOGlufc=0o#O-5`w?6D zIy_h10%^(@pl^J-2LRX%u4@qbk2x*?#3ADWLLiNt0i&)G(dYjH^c@UdwKMg1aG<6! zR)LPT_V%`V$DN(jZ2$-ar2Oaeeco2`2&-VXaT5~Uq=6}X3nx&Cfxa|`X-bI9yeYr$ z?PKroe{1gFw;n=jZ$Bd)B1tm9v*Z4m8TnM1ENECrG+auW_;7A;H>}K^2yQBV~|17gkCo;ess>uwmP4FHUT(L{^y;{kwV^o94<|49UOHZ6y`{LkQq*5&K9 zRnI~iI~~f5t6}VaAH3yQ_w0bSdYDc15b}%Cx{frb5U%U*f--6n0FL;|7oo0x7A(#o z6@3SYR{}q&PRoH9(>fMU^_rCt( z!8icvl3{+=w`UF>Ji!;Z+Z~|`M=8D+-}=(T+phZ8j)Ul^O>3`L&y)ziXHFr#^P+*n zv?-+Q$@BnCL4r1>;3w^_XQgZX@BRd*mul)o5 zZ~P3(C3hhH>QA~Y12yBTaS5vVK$u*%ak*)@P!=cVrUs_1TV7JX`yX2>qU9me-bmWDqPlU%*9!X^#OU}9 zf@WZZ#Ie=g2fEn|%}9d4i|>XwU3 zao7Z~b)8jBMSbjGaZI<*HnPttFmRkXN!^kFwGUs?pI7s56Pd&+r1;%)|FE?YiTZP^ z@X*RRV^^(;YhT#>e=BaDG1M0o=KW;OuUJ@j>jFum%6St*D)*h|DsWAUY2E>%=ph2xw*xh$5wDJ$2 znPvh65`)Po80E=&z>a*9Fnm5dH{1s>6VaD{4YO=h5B)lh__8M5Hnn}77LOl)z697bt;d&n7elrJ4C;nl>L3ErxaiOD^f4jARq*=$hmX>E0 zxpOZbV7eX{ptuBtlP)LW;370b;zy%k>uOAOwECPFxe%^2U{xxV! zJrs$&H_eqX!v35)9K^`nA{>;np&B8?4HKFnKo>Yz4A-2 z+C3*1Bn;LezWObAuK66q@z=rF_R5I?aCA4g$>6^177&jQM)^U+-uN?E_2FcN5Csin zfHl_u;1H)=0k-MYPRgsbRj>ZdhND&)n0-869O;jU_1p*~G0fr=XyL|kbPRK_CUX&+ z-f6Ete|Y(!1DD*GQzgYnAp|hfjph(DcGnx)#;uL5+kW|8aaB!>S?bv8@tZDtm99G| z2OvDUx%7jlFP(6ctdl-=i+Ffb8A=MLK`mW@xM4yy7&MVVrW68AB#p%3m%;PdZ-U2y zh`#s`cylpM1Ve9G9#MkQJzFUxBvnDe$i8bs`shrnA(PCZtsMi5*1 zU-&+EC#0!2LR;H~TPSb~)S6p5#~O$jL84Jn7(gV; zLn9&~3MVk%JXE)DO=*ZFx!i;F=hy58jD0v`2#MAw*6bhaZUNBpIX!Zd3?)yF5 zvlc+zy&3B6&7l6Av>{=kU|7OG&dh;XdH~QGI>r9iA8KB;vsJYi$3XhQl*f&~oG27b z{=KoDa|xx~0CFU0icL(z0f3n8%cI}EWUwHE4q+049~C!R$s0?@+egJ)5+rs4f?IQc z?Bid8Pi(KPE7xL=Ie!UNbxdt72Ol&a5zRnUGofY<1HJrfFw`ib%YF^kQjF7jnFeC- zF9XfUfiyL>j!+mn6TWZ$7GgmWqVGJ9*uNfxSyu(3ctG5lodM9VC`mCl*uK{~`K?XO zYWYv!J9se3j2x*0Ez;|aei}#e=o{aFQBw(d>ZR?~L(;VC;rr^3KtvId|NI8ps;9s?18ZwZbOjCccM34j zyuL7MtDvv^hwT`X6@K8xYg?+{+85F+M$&D28MXo7xE?^alR@uR)USq*92O!N_`NAZ zzSh?#INz6&aXS3{*+bkB`U8kC55*(T#~HT+#UgyzMs)jVx1fXQ_Cd?`W=NK=8-MR; zF!X+HBpmSCI%JyP#?|#sx-HZsMXnp^mh*oZgmJAZHWG?@-L4YyDKX@BLP9DSw zS3+5EC%iX&9jvt;k$?RV=Hb;HrBf^y+l6M-#>`nbOzYtjG54t&Q5ljmGRk z4_|%fMbomv;(vIQ_lx-0zn46}<&8FtTf?9^I_Btx9GvZJbmRQ|aRNkRKAAHgbYBh~yTcrD88=#Rm{6P1X%s)xxCfQ@-i`?*z}8$BH3ev=H*B z$&iMPhf%Q~u@`>=-dde9`iek=Ywlv0m50Epb|>pcB`8T^}i$CcdmTrFJ zqS3Q|JVRezW2)OZ&|2tq_sTHEOQmLVIoG+QW$@*{+I-~E@{lRok`z{OBH|#tmL_x6 zRSugw;tuUn<;JDkmae#Q`h;vQU1fh4(F}~*@eU4*orgB}APDiJpvgXf5Cupixe+|k zz3X&{1J>LGAvX_3@piaY1~31plYCZVEb`B_ zu`oFjh5nMfKL-9=QHK1 ze5Ne%MClb&!*DJl)ao|=a{sD*zkg;&1GTw~aOf)g6zz2V?M~F>4{=c1@ z4m8B<=hJ9%F`CVIvlHLMsZY!#0Lb>*2=jZ$o(a7(W{8Yr+Mz;B6It~~Ar2UWmaM@* za~Z%{2l3n02I(jz;@t-TQM~Zp^}moujz?_O>tMkaNW&&V-}@mD3w8nkHvxI}RS8d^ z*D&a7UInk&)~Q`QW8B<)xMkUr*Y_Muy3f{N>@a}lB=MV*V6evHcB@$==_UZ|!I!Ef%Q7O45KY;8<7Y-eJL;w6=DF*G6ZeT=s&{$OV z9|lso4&LZ7cqFS$ENO2OOW;sSDU*s4;NTW?B`>^pJPdinIK);hh5F9ppoP;AxaVQ0 z>z6@W{Wox(gKHdI2bjtM>W1$JkHe@af&SK?0ll?D-zRsDbIx~^w7ot5`E46)A#y#* z=}kzqiM3kAwjp&g>IrM1gdT1*IZi29ilnf;$8-dDoKdP%)wpRVi7hW_j`quu_6^VW zO^`V0)J#fltw&Z%4cLg;h~x|fYd!{UYMn(oC#fnSodghNcyE6Q@~DZ3uU-oEy~hEQ zgEiGa8aD?*pF(KcmL+-wn~7e*!pQkh7TgSd<9jgHJO%K0r;#y?akjO%b=lP~?bvK^ zcx-wX1#^k9S0h3nVVW7C$LNEe|p(8J#LJ-yy7ZH_=RcTLF87_NZ! za{el+#^F155I!P6AG;80pg+u(DsV&Xs3S}nV@cXGfbGu_0q$ELgff0Q;_Kgny5cc_ z@nn-|26%cw9yc9Ec?nqaG0?$tAYXbb2oa#a|1@~*{)k9ML4u$ zQopRMfFKs7IFFVq)Yb@J*&r#67|b=01dw-snew)}w8Mbpz&1od>G-5|R4-GVK<=mn4g zAp7;}$1l7%GwTQK-`eHiNCK9d4{hWOXt}+?3>~z-9Q4>82+q&4_^Igozg;(69Gc^k6@f@~ zA(gE!Z!sTPSIKQ4Pyl!r56R5^>%4JKyA3)#NQ}y;X@>GdWdyMWjM77jf@fsI96Slg z9{}!h0RjaC8H6*q#!LmF6y||-fS`at1Tj<)B7ry6fferruQ~vj+6hO;syYj9ENg!2 zuDAE?tIsK6L?>032g2z)TLQ+<{eSkpQPaPv30ZhZ@6Q-KYT>-#kTq|vgZ2t4c z($&u%ZZy!*+Kt;nyg^5#udNH>Vdg^Tyc6oVz?D5?{(bM zZv@~XTEoY#x$^Rg!NlH~y+i2QK?fCGr=WW?K z=u|xVz(xIYzcO;bmvgx^8$2Y-@e)s#kYzL~13In%q#q7V@_D+qJ~BQ0NiG z0U5I36-AokmMBSxb$0)>A#+D%`=_+1diCI^K6);VN96R1v@}67tE4VuD}Hh2OiY}@ zsMymH2>RFoQ1g@d{lyl%3JreKhxLCySoVz{tvJfj(GTBci#u>f0IK~6T0`7qZyv5K zUNoYYkS$1~z(Mjn$`FGZ+c(yP!Va#p7gIQE6D-a%+H|H@gmt|*sOo!~V%qMen0~M| zZrnJmcUC{2dx*(7d*evW{$`EYi(IXgb=>w{A#~I>w%S}fyP+3#5dVbf-JV|bEaZM7 zhsXmw#(TkqrjJ^hzxUwEGLFuZFnAXRlf8y8Sf{e!H&^a?v5gqJ0YEZ|=gXt|FG`*j zWwsP`IQgDN0CfOrl6Q6TZ*B7bt!bT2cFVjn9IpicNuaI?z5Lt}2@TmkyFU3@)sD}u zPZQex*u&;$n*<=oemt+A)QZ1>!4>qFkIZ>`%O>8DeY6@OT4hl7>|)fmY4%?EMrGJA z$Mo@3j>zyWARN>y3B6V5>o(Pdg6J5o5VLWAJk9@isztP?wx{>;n^W^MFA*sb4>gC^ zt*s8Wp`%*ba^;$BcWW9W&z>vM(?s@8d(K2W+qmByr&n{0WpV}0uNGIoYVwrU=a%87 zrl~8ibBSfuTOncXYs(Mqy!6Ytn=>HJ6ilLgW@!F|KW(qBvPCgr`)tz@R(9T^BLXT# zbVD~eR|P_pqAahNB)ApK38ACj(P_=6D}6iVvGC_201}tV7l8w+z*qm|y;3#lvs*@( zy#O+ub<7+95ZgDIbE8I6-_DT4nczTwmunOyc!%+nddCH2nR)=nFT%ga+f>3SHk zL^zKh1ptpo^9X^#dHZut&SoEnOLwlT0HT`=potyH^hueHV2ZQ0z~~0??VD{3%66O9 znE_5t z_6=RI6IH#si{0G}-6JG_9d`Y2xjfMX-soPTcF7nIE> zK)^FpVb*`HnS9q}MSd$2Xf>!((ztCY_xEQO2)~>+>Ps|&--CgI01aIKzc2iEZy7qy zEOJMn#_3`72*8Y^BH;R&N(ZO3oIltUr$jb4P3o69Og2cNY>>X=vI2{~ zIH7M~LV>ToEf$NE2X$^Y5N{ftBRp}X{E zKC-^DrKBnHgI`=c^6uVDzM1OqnLzwp57{ZGAixWy_6wHhngOR{33pma7eJ3k+ z&#FL>ZL_TETptn@LH@p8!TXC#hmV?+oiQgzlqQNM$rT9kPzsSTm?n6{q`WrBjP3i| z!YjY=&c5QBsA(M$v!{ElgO*mIMw>(Y_rw(>0PyY&6WpV70+$LPOB6~!2Lf4Rtg<<- z?>*8S+4#t&;;n0HBlb!ldnYe;rhss^Rs~zd9QLLZK6KOt$^gXMr}Xg;&-A)76p==B zQ!8$ZMjqQ#9kkguqQhmhq>H^vnX~4;CkHHvED%)QHFvtm@F+N$$_aE+Mmc9em#}*5ajuQ*8H>+@VG<9~=>{eGT z`x@)-oi{-+uo?hs)bv}2|8w0|bhOa%Cnc7v$Sq3GUpMLMVY%L0I1pcbwDHa*Z|vEI zt_xc}u{r?(XaAM1TX?!-=k~|W3Z9h89i&#^>L)||7p@s24Dowk0LS1~)3|f!Kh~{H z15ObDZ8aEMdZ1?i4I}ac0hcs7&*vJnv$AQ$(XhtTMH4@*aey8LiqLs7hP_X|y>!f8 zI-agQ-N~G;C66r}HIo|nACqF^Ez7q4sv@krZNzt;^fh9P>sb;xGQA+{az&&A`uSYD zo)Gnd+T0aS0SMqcPm{V<5k(l4ia0LJ%VwnG3TN{;;Izxfl zzOTJf`R7|I!e5&-a94&$oHaPdI|sln$6C9p6a3Tyz}f(U&ZSru2n3{0vZPZR44qrf zp3Ei_aEt=LLxxdib2DX|e{VPfO9X&uGFA*=mR}ZzxqJQBMHk5rokI_Sk|hZ+ z?2*`hZ@gt{KUpM`0U#JR4%=wZRk6#*o;f^M0dOKPd>lkixtdh%T@dIpA*27gXxwK> z=TvVBIDb8}hz`#7{)7yEc~9?-zw0S77cy6;(a zEm+(Pe>s7$tF>pfyX(ZQ9?X~K(e}o z8H_isKi2%cYnSaVMMqMLCyhn>*qjJXUo-9VA`*Bu0)YY$p50&DIxwKD?jLaXRs=eL z5F!!~i4x)>fRLOu&R>mE#ORi0&#!~65!;CFQTI# zu^&LjW0wpaHlbb@SKXKU9X)L#PC)BVVlEKvvI#Pb3(0 z0RX3_2JH0;fg}*TwxE#RBXAyOv2(uF1k%|7^kdHnTu=e04-h9BqTxL0Yz2z!*-2+B z{f}J*TmZo7m7K>rMs5X%>?!JWjUYeKYGLsLEl)c51au^&;2^Yy)H4hAWA650+xE_6 zr>*_#1ps`qAh3^SvjZTG)M_O=0PKB?oGorX(V~zGM&Rk$zjWQiL>u?pn?l>i=pL^% z+ywxfqlO?&bHgDe(%rxrI2&*Q03Xj@I$geW38Lert1kfHJn|4o7q$b=B;hYyXGTR_ ixGr25t_#;$bp3zus*dGx^yzp20000 + - + android:background="@color/toolbar_bg" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + > + app:title="" + app:titleEnabled="false" + app:titleCentered="false"> - - - - - - - + - - - - + - + + - + + - - + app:layout_constraintTop_toBottomOf="@id/appBarLayout" + app:layout_constraintBottom_toTopOf="@id/bottom_nav" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> - + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml new file mode 100644 index 00000000..b0399792 --- /dev/null +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/fragment_sensors.xml b/sensorhub-android-app/res/layout/fragment_sensors.xml new file mode 100644 index 00000000..90421d75 --- /dev/null +++ b/sensorhub-android-app/res/layout/fragment_sensors.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/preference_item.xml b/sensorhub-android-app/res/layout/preference_item.xml new file mode 100644 index 00000000..47167543 --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_item.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/preference_list_item.xml b/sensorhub-android-app/res/layout/preference_list_item.xml new file mode 100644 index 00000000..82176640 --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_list_item.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/preference_switch_item.xml b/sensorhub-android-app/res/layout/preference_switch_item.xml new file mode 100644 index 00000000..24ec3c5c --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_switch_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/menu/bottom_nav_menu.xml b/sensorhub-android-app/res/menu/bottom_nav_menu.xml new file mode 100644 index 00000000..081fc3e4 --- /dev/null +++ b/sensorhub-android-app/res/menu/bottom_nav_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/sensorhub-android-app/res/menu/main.xml b/sensorhub-android-app/res/menu/main.xml index f87d2ead..e57c949e 100644 --- a/sensorhub-android-app/res/menu/main.xml +++ b/sensorhub-android-app/res/menu/main.xml @@ -3,21 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" tools:context="org.sensorhub.android.MainActivity"> - - - - - - - + + diff --git a/sensorhub-android-app/res/values/colors.xml b/sensorhub-android-app/res/values/colors.xml index fc6cb078..d04f4a14 100644 --- a/sensorhub-android-app/res/values/colors.xml +++ b/sensorhub-android-app/res/values/colors.xml @@ -1,53 +1,73 @@ - - - #FF6D00 + #D35400 #FFFFFF - #FFE0B2 - #3E1C00 - #E65100 - - - #212121 - #FFFFFF - #424242 - #FFFFFF - - - #FF9100 + + #24160A + #E6B89C + + #C24E00 + + #9A9A9A + #0A0A0A + + #242424 + #F5F5F5 + + #B84A0D #FFFFFF - #FFF3E0 - #3E2700 - - - #D32F2F - #FFFFFF - #FFCDD2 - #410002 - - - #FFFFFF - #212121 - #FFFFFF - #212121 - #F5F5F5 - #616161 - #9E9E9E - - + + #1C0A00 + #E6B89C + + #CF6679 + #1C0006 + + #4A0011 + #FFB3BA + + #0A0A0A + + #F5F5F5 + + #141414 + #F5F5F5 + + #1C1C1C + #9A9A9A + + #2E2E2E + + #0A0A0A + #141414 + #1C1C1C + #242424 + + #F5F5F5 + #9A9A9A + #5A5A5A + + + #D35400 + #C24E00 + #1AD35400 + + #141414 + #F5F5F5 + + #141414 + #D35400 + #6A6A6A + #4CAF50 #F44336 #FF9800 - #9E9E9E + #5A5A5A + + #1AFFFFFF + #99000000 - - #CCFFFFFF - #80000000 - #F5F5F5 + #1C1C1C - - #FFFFFF - #212121 - + \ No newline at end of file diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 556cb4d5..82fb066a 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -1,7 +1,7 @@ OpenSensorHub - Please configure and start SmartHub using the Options Menu + Configure settings and tap the play button to start SmartHub Settings Start SmartHub Stop SmartHub @@ -17,6 +17,7 @@ Meshtastic Sensor Angel Sensor Flirone Sensor + JPEG H264 @@ -25,6 +26,20 @@ VP8 + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + Quaternion Euler @@ -236,6 +251,10 @@ + + Sensors + Home + Settings Enter a message! Enter a message! Enter the destination Node ID (integer) diff --git a/sensorhub-android-app/res/values/styles.xml b/sensorhub-android-app/res/values/styles.xml index 6172c89d..80a26efd 100644 --- a/sensorhub-android-app/res/values/styles.xml +++ b/sensorhub-android-app/res/values/styles.xml @@ -1,61 +1,244 @@ - + + - - - - + + @color/surface_base + @color/surface_low + false + false + + + @style/AppToolbar + @style/AppCard + @style/AppButton + @style/AppTextInput - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_audio.xml b/sensorhub-android-app/res/xml/pref_audio.xml deleted file mode 100644 index f23302f1..00000000 --- a/sensorhub-android-app/res/xml/pref_audio.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/sensorhub-android-app/res/xml/pref_general.xml b/sensorhub-android-app/res/xml/pref_general.xml deleted file mode 100644 index 9249b2e3..00000000 --- a/sensorhub-android-app/res/xml/pref_general.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_headers.xml b/sensorhub-android-app/res/xml/pref_headers.xml deleted file mode 100644 index c5f5f977..00000000 --- a/sensorhub-android-app/res/xml/pref_headers.xml +++ /dev/null @@ -1,20 +0,0 @@ - - -
-
-
-
- - - - - - diff --git a/sensorhub-android-app/res/xml/pref_kestrel.xml b/sensorhub-android-app/res/xml/pref_kestrel.xml deleted file mode 100644 index 54b96166..00000000 --- a/sensorhub-android-app/res/xml/pref_kestrel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 552ae3ed..ba6dafb6 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -1,337 +1,424 @@ - + - - + - + android:title="Accelerometer Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Gyroscope Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Magnetometer Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Orientation Data (Quaternions)" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Orientation Data (Euler Angles)" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="GPS Location Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Network Location Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + + android:title="Video Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + + + + + + + + + android:title="Video Roll Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Audio Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> + + + + + - + + + android:title="Meshtastic Data" + android:layout="@layout/preference_switch_item" /> - + android:summary="Tap to select or enter device address" + android:layout="@layout/preference_list_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="Polar Heart Rate Data" + android:layout="@layout/preference_switch_item" /> - + android:summary="Tap to select or enter device address" + android:layout="@layout/preference_list_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item" /> - + android:title="Kestrel Weather Meter" + android:layout="@layout/preference_switch_item" /> - - + android:summary="Tap to select or enter device address" + android:layout="@layout/preference_list_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="TruPulse Range Finder Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@string/trupulse_datasource_default" + android:layout="@layout/preference_list_item"/> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - - + android:summary="Tap to select or enter device address" + android:layout="@layout/preference_list_item" /> - + android:title="Use Simulated TruPulse Data" + android:layout="@layout/preference_switch_item" /> - + android:title="Angel Sensor Health Data" + android:layout="@layout/preference_switch_item" /> + android:title="Angel Sensor Bluetooth Address" + android:layout="@layout/preference_item"/> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item" /> - + android:title="FLIR One Thermal Camera Data" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:title="STE RadPager Data" + android:layout="@layout/preference_switch_item" /> - - - - - + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml new file mode 100644 index 00000000..cba775b9 --- /dev/null +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -0,0 +1,180 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_video.xml b/sensorhub-android-app/res/xml/pref_video.xml deleted file mode 100644 index 96080f4b..00000000 --- a/sensorhub-android-app/res/xml/pref_video.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java new file mode 100644 index 00000000..23fd3ff1 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -0,0 +1,487 @@ +package org.sensorhub.android; + +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.SurfaceTexture; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import android.graphics.drawable.GradientDrawable; +import androidx.core.content.ContextCompat; + +import org.sensorhub.api.event.Event; +import org.sensorhub.api.module.ModuleEvent; +import org.sensorhub.impl.client.sost.SOSTClient; +import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; +import org.sensorhub.impl.event.EventBus; +import org.sensorhub.impl.module.ModuleRegistry; +import org.sensorhub.impl.sensor.android.AndroidSensorsConfig; +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; +import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; +import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig.VideoPreset; +import org.sensorhub.impl.service.consys.client.ConSysApiClientModule; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.Flow; + + +public class DashboardFragment extends Fragment implements TextureView.SurfaceTextureListener, Flow.Subscriber +{ + private TextView mainInfoArea; + private TextView videoInfoArea; + private TextureView textureView; + private MaterialCardView videoStatusCard; + private MaterialButton btnToggleVideo; + private View videoStatusDot; + private FloatingActionButton fab; + private Handler displayHandler; + private Runnable displayCallback; + private StringBuffer mainInfoText = new StringBuffer(); + private StringBuffer videoInfoText = new StringBuffer(); + private Flow.Subscription subscription; + private SensorHubServiceProvider provider; + private boolean videoPreviewVisible = false; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + provider = (SensorHubServiceProvider) requireActivity(); + displayHandler = new Handler(Looper.getMainLooper()); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_dashboard, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + mainInfoArea = view.findViewById(R.id.main_info); + videoInfoArea = view.findViewById(R.id.video_info); + + textureView = view.findViewById(R.id.video); + textureView.setSurfaceTextureListener(this); + + videoStatusCard = view.findViewById(R.id.video_status_card); + btnToggleVideo = view.findViewById(R.id.btn_toggle_video); + videoStatusDot = view.findViewById(R.id.video_status_dot); + + btnToggleVideo.setOnClickListener(v -> toggleVideoPreview()); + + fab = view.findViewById(R.id.fab_toggle); + fab.setOnClickListener(v -> { + if (!provider.isOshStarted()) { + if (provider.getBoundService() != null && provider.getBoundService().getSensorHub() == null) + showRunNamePopup(); + } else { + stopHub(); + } + }); + + updateFabIcon(); + } + + @Override + public void onResume() { + super.onResume(); + if (provider.isOshStarted()) { + startRefreshingStatus(); + updateVideoStatusCard(); + } + } + + @Override + public void onPause() { + stopRefreshingStatus(); + super.onPause(); + } + + private void updateFabIcon() { + if (fab == null) return; + if (provider.isOshStarted()) { + fab.setImageResource(R.drawable.ic_stop); + } else { + fab.setImageResource(R.drawable.ic_play); + } + } + + private void stopHub() { + stopRefreshingStatus(); + provider.stopSensorHub(); + updateFabIcon(); + hideVideoPreview(); + videoStatusCard.setVisibility(View.GONE); + newStatusMessage("SensorHub Stopped"); + requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // ==================== Run Name Popup ==================== + + protected synchronized void showRunNamePopup() { + MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireContext()); + alert.setTitle("Run Name"); + alert.setMessage("Please enter the name for this run"); + + TextInputLayout inputLayout = new TextInputLayout(requireContext()); + inputLayout.setBoxBackgroundMode(TextInputLayout.BOX_BACKGROUND_OUTLINE); + inputLayout.setHint("Run Name"); + + TextInputEditText input = new TextInputEditText(inputLayout.getContext()); + input.getText().append("Run-"); + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); + input.getText().append(formatter.format(new Date())); + inputLayout.addView(input); + + int padding = (int) (24 * getResources().getDisplayMetrics().density); + FrameLayout container = new FrameLayout(requireContext()); + container.setPadding(padding, 0, padding, 0); + container.addView(inputLayout); + alert.setView(container); + + alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + String runName = input.getText().toString(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + provider.updateConfig(prefs, runName); + + AndroidSensorsConfig androidSensorConfig = (AndroidSensorsConfig) provider.getSensorhubConfig().get("ANDROID_SENSORS"); + VideoEncoderConfig videoConfig = androidSensorConfig.videoConfig; + + boolean cameraInUse = (androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera); + boolean improperVideoSettings = (videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length); + + if (cameraInUse && improperVideoSettings) { + showVideoConfigErrorPopup(); + newStatusMessage("Video Config Error: Check Settings"); + } else { + newStatusMessage("Starting SensorHub..."); + provider.getSostClients().clear(); + provider.getConSysClients().clear(); + provider.startSensorHub(); + + SensorHubService service = provider.getBoundService(); + + while (service.getSensorHub() == null) { + System.out.println("Waiting for BoundService Hub to start..."); + } + while (service.getSensorHub().getEventBus() == null) { + System.out.println("Waiting for BoundService Hub EventBus to start..."); + } + + EventBus shEvtBus = (EventBus) service.getSensorHub().getEventBus(); + shEvtBus.newSubscription() + .withTopicID(ModuleRegistry.EVENT_GROUP_ID) + .subscribe(DashboardFragment.this); + } + } + }); + + alert.setNegativeButton("Cancel", (dialog, whichButton) -> {}); + alert.show(); + } + + protected void showVideoConfigErrorPopup() { + String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("OpenSensorHub") + .setMessage(message) + .setPositiveButton("OK", (dialog, id) -> {}) + .show(); + } + + // ==================== Status Display ==================== + + protected void startRefreshingStatus() { + if (displayCallback != null) return; + + displayCallback = new Runnable() { + public void run() { + displayStatus(); + mainInfoArea.setText(Html.fromHtml(mainInfoText.toString())); + videoInfoArea.setText(Html.fromHtml(videoInfoText.toString())); + displayHandler.postDelayed(this, 1000); + } + }; + displayHandler.post(displayCallback); + } + + protected void stopRefreshingStatus() { + if (displayCallback != null) { + displayHandler.removeCallbacks(displayCallback); + displayCallback = null; + } + } + + protected synchronized void displayStatus() { + mainInfoText.setLength(0); + + // SOST Client errors/status + for (SOSTClient client : provider.getSostClients()) { + Map dataStreams = client.getDataStreams(); + boolean showError = (client.getCurrentError() != null); + boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); + + if (showError || showMsg) { + mainInfoText.append("

" + client.getName() + ":
"); + if (showMsg) + mainInfoText.append(client.getStatusMessage() + "
"); + if (showError) { + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage().trim(); + if (!errorMsg.endsWith(".")) + errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + mainInfoText.append("" + errorMsg + ""); + } + mainInfoText.append("

"); + } + } + + // ConSys Client errors/status + for (ConSysApiClientModule client : provider.getConSysClients()) { + Map dataStreams = client.getDataStreams(); + boolean showError = (client.getCurrentError() != null); + boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); + + if (showError || showMsg) { + mainInfoText.append("

" + client.getName() + ":
"); + if (showMsg) + mainInfoText.append(client.getStatusMessage() + "
"); + if (showError) { + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage().trim(); + if (!errorMsg.endsWith(".")) + errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + mainInfoText.append("" + errorMsg + ""); + } + mainInfoText.append("

"); + } + } + + // Stream statuses + mainInfoText.append("

"); + for (SOSTClient client : provider.getSostClients()) { + mainInfoText.append("SOS-T Client

"); + Map dataStreams = client.getDataStreams(); + long now = System.currentTimeMillis(); + for (Entry stream : dataStreams.entrySet()) { + mainInfoText.append("" + stream.getKey() + " : "); + long lastEventTime = stream.getValue().lastEventTime; + long dt = now - lastEventTime; + if (lastEventTime == Long.MIN_VALUE) + mainInfoText.append("NO OBS"); + else if (dt > stream.getValue().measPeriodMs) + mainInfoText.append("NOK (" + dt + "ms ago)"); + else + mainInfoText.append("OK (" + dt + "ms ago)"); + if (stream.getValue().errorCount > 0) { + mainInfoText.append(" ("); + mainInfoText.append(stream.getValue().errorCount); + mainInfoText.append(")"); + } + mainInfoText.append("
"); + } + } + + for (ConSysApiClientModule client : provider.getConSysClients()) { + mainInfoText.append("ConSysApi Client

"); + Map dataStreams = client.getDataStreams(); + long now = System.currentTimeMillis(); + for (Entry stream : dataStreams.entrySet()) { + mainInfoText.append("" + stream.getKey() + " : "); + long lastEventTime = stream.getValue().lastEventTime; + long dt = now - lastEventTime; + if (lastEventTime == Long.MIN_VALUE) + mainInfoText.append("NO OBS"); + else if (dt > stream.getValue().measPeriodMs) + mainInfoText.append("NOK (" + dt + "ms ago)"); + else + mainInfoText.append("OK (" + dt + "ms ago)"); + if (stream.getValue().errorCount > 0) { + mainInfoText.append(" ("); + mainInfoText.append(stream.getValue().errorCount); + mainInfoText.append(")"); + } + mainInfoText.append("
"); + } + } + mainInfoText.append("

"); + + if (mainInfoText.length() > 5) + mainInfoText.setLength(mainInfoText.length() - 5); + mainInfoText.append("

"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + MainActivity activity = (MainActivity) requireActivity(); + boolean serveOrStore = activity.shouldServe(prefs) || activity.shouldStore(prefs); + if (provider.getSostClients().isEmpty() && serveOrStore) { + mainInfoText.append("No Sensors Set to Push Remotely"); + } + if (provider.getConSysClients().isEmpty() && serveOrStore) { + mainInfoText.append("No Sensors Set to Push Remotely"); + } + + // video info — update status card + AndroidSensorsDriver sensors = provider.getAndroidSensors(); + SensorHubService service = provider.getBoundService(); + if (sensors != null && service != null && service.hasVideo()) { + try { + VideoEncoderConfig config = sensors.getConfiguration().videoConfig; + VideoPreset preset = config.presets[config.selectedPreset]; + videoInfoText.setLength(0); + videoInfoText.append(config.codec).append(", ") + .append(preset.width).append("x").append(preset.height).append(", ") + .append(config.frameRate).append(" fps, ") + .append(preset.selectedBitrate).append(" kbits/s"); + } catch (Exception e) { + // ignore display errors + } + updateVideoStatusCard(); + } + } + + protected synchronized void newStatusMessage(String msg) { + mainInfoText.setLength(0); + mainInfoText.append(msg); + displayHandler.post(() -> mainInfoArea.setText(mainInfoText.toString())); + } + + // ==================== Video ==================== + + private void updateVideoStatusCard() { + SensorHubService service = provider.getBoundService(); + boolean hasVideo = service != null && service.hasVideo(); + + videoStatusCard.setVisibility(hasVideo ? View.VISIBLE : View.GONE); + + if (hasVideo && videoInfoText.length() > 0) { + videoInfoArea.setText(videoInfoText.toString()); + } + + // Update the status dot color (green = streaming) + if (videoStatusDot != null && videoStatusDot.getBackground() instanceof GradientDrawable) { + GradientDrawable dot = (GradientDrawable) videoStatusDot.getBackground(); + int color = ContextCompat.getColor(requireContext(), + hasVideo ? R.color.status_started : R.color.status_unknown); + dot.setColor(color); + } + } + + private void toggleVideoPreview() { + videoPreviewVisible = !videoPreviewVisible; + if (videoPreviewVisible) { + textureView.setVisibility(View.VISIBLE); + btnToggleVideo.setText("Hide"); + mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme())); + showVideo(); + } else { + hideVideoPreview(); + } + } + + private void hideVideoPreview() { + videoPreviewVisible = false; + textureView.setVisibility(View.GONE); + if (btnToggleVideo != null) btnToggleVideo.setText("Show"); + mainInfoArea.setBackgroundColor(0x00000000); + } + + protected void showVideo() { + SensorHubService service = provider.getBoundService(); + if (service != null && service.getVideoTexture() != null) { + if (textureView.getSurfaceTexture() != service.getVideoTexture()) + textureView.setSurfaceTexture(service.getVideoTexture()); + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { + if (videoPreviewVisible) showVideo(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {} + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {} + + // ==================== Event Subscriber ==================== + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(10); + } + + @Override + 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); + startRefreshingStatus(); + subscription.request(10); + return; + } + else if (e.getSource() instanceof AndroidSensorsDriver) { + provider.setAndroidSensors((AndroidSensorsDriver) e.getSource()); + } + else if (e.getSource() instanceof SOSTClient && ((ModuleEvent) e).getType() == ModuleEvent.Type.STATE_CHANGED) { + if (((ModuleEvent) e).getNewState() == org.sensorhub.api.module.ModuleEvent.ModuleState.INITIALIZING) { + provider.getSostClients().add((SOSTClient) e.getSource()); + } + } + else if (e.getSource() instanceof ConSysApiClientModule && ((ModuleEvent) e).getType() == ModuleEvent.Type.STATE_CHANGED) { + if (((ModuleEvent) e).getNewState() == org.sensorhub.api.module.ModuleEvent.ModuleState.INITIALIZING) { + provider.getConSysClients().add((ConSysApiClientModule) e.getSource()); + } + } + } + subscription.request(10); + } + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onComplete() {} +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index b8311ab4..e983b6de 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -29,31 +29,31 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.graphics.SurfaceTexture; import android.location.LocationManager; import android.location.LocationProvider; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; -import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Settings.Secure; -import android.text.Html; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.TextureView; import android.view.View; import android.view.WindowManager; import android.widget.EditText; -import android.widget.TextView; +import android.os.PowerManager; import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; +import androidx.navigation.ui.NavigationUI; + import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import net.opengis.swe.v20.DataBlock; @@ -65,21 +65,17 @@ import org.sensorhub.api.command.CommandData; import org.sensorhub.api.command.IStreamingControlInterface; import org.sensorhub.api.common.BigId; -import org.sensorhub.api.event.Event; import org.sensorhub.api.module.IModule; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.api.module.ModuleConfig; -import org.sensorhub.api.module.ModuleEvent; import org.sensorhub.api.sensor.SensorConfig; import org.sensorhub.impl.client.sost.SOSTClient; -import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; import org.sensorhub.impl.client.sost.SOSTClientConfig; +import org.sensorhub.impl.module.ModuleRegistry; import org.sensorhub.impl.datastore.h2.MVObsSystemDatabaseConfig; import org.sensorhub.impl.datastore.view.ObsSystemDatabaseViewConfig; -import org.sensorhub.impl.event.EventBus; import org.sensorhub.impl.module.InMemoryConfigDb; import org.sensorhub.impl.module.ModuleClassFinder; -import org.sensorhub.impl.module.ModuleRegistry; import org.sensorhub.impl.sensor.android.AndroidSensorsConfig; import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; import org.sensorhub.impl.sensor.android.audio.AudioEncoderConfig; @@ -112,7 +108,6 @@ import java.net.URISyntaxException; import java.net.URL; import java.security.cert.X509Certificate; -import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -120,10 +115,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.Flow; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -133,7 +125,7 @@ import javax.net.ssl.X509TrustManager; -public class MainActivity extends AppCompatActivity implements TextureView.SurfaceTextureListener, Flow.Subscriber +public class MainActivity extends AppCompatActivity implements SensorHubServiceProvider { public static final String ACTION_BROADCAST_RECEIVER = "org.sensorhub.android.BROADCAST_RECEIVER"; public static final String ANDROID_SENSORS_MODULE_ID = "ANDROID_SENSORS"; @@ -141,14 +133,8 @@ public class MainActivity extends AppCompatActivity implements TextureView.Surfa public static final Date TRUPULSE_SENSOR_LAST_UPDATED = ANDROID_SENSORS_LAST_UPDATED; private static final Logger log = LoggerFactory.getLogger(MainActivity.class); - TextView mainInfoArea; - TextView videoInfoArea; SensorHubService boundService; IModuleConfigRepository sensorhubConfig; - Handler displayHandler; - Runnable displayCallback; - StringBuffer mainInfoText = new StringBuffer(); - StringBuffer videoInfoText = new StringBuffer(); boolean oshStarted = false; ArrayList sostClients = new ArrayList<>(); ArrayList conSysClients = new ArrayList<>(); @@ -162,15 +148,8 @@ public class MainActivity extends AppCompatActivity implements TextureView.Surfa String deviceID; String runName; - private Flow.Subscription subscription; - Flow.Subscriber mainActivity = this; private BroadcastReceiver broadcastReceiver; - // Request codes for permissions - final int FINE_LOC_RC = 101; - final int CAMERA_RC = 102; - final int AUDIO_RC = 103; - enum Sensors { Android, TruPulse, @@ -185,13 +164,11 @@ enum Sensors { Kestrel } - private final ServiceConnection sConn = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { boundService = ((SensorHubService.LocalBinder) service).getService(); -// boundService.initSensorhub(); } public void onServiceDisconnected(ComponentName className) @@ -200,13 +177,60 @@ public void onServiceDisconnected(ComponentName className) } }; + // ==================== SensorHubServiceProvider ==================== + + @Override + public SensorHubService getBoundService() { return boundService; } + + @Override + public boolean isOshStarted() { return oshStarted; } + + @Override + public void setOshStarted(boolean started) { this.oshStarted = started; } + + @Override + public IModuleConfigRepository getSensorhubConfig() { return sensorhubConfig; } + + @Override + public ArrayList getSostClients() { return sostClients; } + + @Override + public ArrayList getConSysClients() { return conSysClients; } + + @Override + public AndroidSensorsDriver getAndroidSensors() { return androidSensors; } + + @Override + public void setAndroidSensors(AndroidSensorsDriver driver) { this.androidSensors = driver; } + + @Override + public boolean getShowVideo() { return showVideo; } + + @Override + public void startSensorHub() { + if (boundService != null && sensorhubConfig != null) { + boundService.startSensorHub(sensorhubConfig, showVideo); + } + } + + @Override + public void stopSensorHub() { + sostClients.clear(); + conSysClients.clear(); + if (boundService != null) + boundService.stopSensorHub(); + oshStarted = false; + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } - protected void updateConfig(SharedPreferences prefs, String runName) + // ==================== Config ==================== + + @Override + public void updateConfig(SharedPreferences prefs, String runName) { deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); sensorhubConfig = new InMemoryConfigDb(new ModuleClassFinder()); - //get ip, port, user, password String host = prefs.getString("ip_address", "").trim(); String port = prefs.getString("port", "").trim(); String user = prefs.getString("username", null); @@ -224,8 +248,6 @@ protected void updateConfig(SharedPreferences prefs, String runName) if (port.isEmpty()) port = "8585"; -// String sensorhubEndpoint = "/sensorhub"; - String newUrl = (isTLSEnabled ? "https://" : "http://") + host + ":" + port + endpointPath; try { @@ -240,11 +262,9 @@ protected void updateConfig(SharedPreferences prefs, String runName) throw new RuntimeException(e); } - // disable SSL check if requested boolean disableSslCheck = prefs.getBoolean("sos_disable_ssl_check", false); if (disableSslCheck) { - // Create a trust manager that does not validate certificate chains TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { @@ -260,7 +280,6 @@ public void checkServerTrusted( } }; - // Install the all-trusting trust manager try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); @@ -282,7 +301,6 @@ public boolean verify(String arg0, SSLSession arg1) { String tokenEndpoint = prefs.getString("token_endpoint", "").trim(); String clientSecret = prefs.getString("client_secret", "").trim(); - // get device name String deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); String deviceName = prefs.getString("device_name", null); if (deviceName == null || deviceName.length() < 2) @@ -304,8 +322,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.activateNetworkLocation = prefs.getBoolean("netloc_enabled", false); sensorsConfig.enableCamera = prefs.getBoolean("cam_enabled", false); sensorsConfig.selectedCameraId = Integer.parseInt(prefs.getString("camera_select", "0")); - /*if (sensorsConfig.activateBackCamera || sensorsConfig.activateFrontCamera) - showVideo = true;*/ if (sensorsConfig.enableCamera) showVideo = true; @@ -313,7 +329,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.videoConfig.codec = prefs.getString("video_codec", VideoEncoderConfig.JPEG_CODEC); sensorsConfig.videoConfig.frameRate = Integer.parseInt(prefs.getString("video_framerate", "30")); - // selected preset or AUTO mode String selectedPreset = prefs.getString("video_preset", "0"); if ("AUTO".equals(selectedPreset)) { sensorsConfig.videoConfig.autoPreset = true; @@ -324,7 +339,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.videoConfig.selectedPreset = Integer.parseInt(selectedPreset); } - // video preset list int resIdx = 1; ArrayList presetList = new ArrayList<>(); while (prefs.contains("video_size" + resIdx)) @@ -353,16 +367,14 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.runName = runName; sensorsConfig.uidExtension = prefs.getString("uid_extension", "0"); - - // START SOS Config ************************************************************************ - // Setup HTTPServerConfig for enabling more complete node functionality + // HTTP Server HttpServerConfig serverConfig = new HttpServerConfig(); serverConfig.proxyBaseUrl = ""; serverConfig.httpPort = 8585; serverConfig.autoStart = true; sensorhubConfig.add(serverConfig); - // We don't need android context unless we're doing IPC things + // SOS Service SOSServiceConfig sosConfig = new SOSServiceConfig(); sosConfig.moduleClass = SOSService.class.getCanonicalName(); sosConfig.id = "SOS_SERVICE"; @@ -371,11 +383,11 @@ public boolean verify(String arg0, SSLSession arg1) { sosConfig.enableTransactional = true; sosConfig.exposedResources = new ObsSystemDatabaseViewConfig(); - //Connected systems service + // Connected Systems Service ConSysApiServiceConfig conSysApiService = new ConSysApiServiceConfig(); conSysApiService.moduleClass = ConSysApiService.class.getCanonicalName(); conSysApiService.id = "CON_SYS_SERVICE"; - conSysApiService.name= "Connected Systems API Service"; + conSysApiService.name = "Connected Systems API Service"; conSysApiService.autoStart = true; conSysApiService.enableTransactional = true; conSysApiService.exposedResources = new ObsSystemDatabaseViewConfig(); @@ -386,54 +398,31 @@ public boolean verify(String arg0, SSLSession arg1) { conSysOAuthConfig.clientID = clientId; conSysOAuthConfig.clientSecret = clientSecret; - - // Push Sensors Config sensorhubConfig.add(sensorsConfig); - if (isPushingSensor(Sensors.Android)) { if (isClientEnabled) { - System.out.println("Connected Systems Client enabled"); addCSApiConfig(sensorsConfig, user, password, conSysOAuthConfig); - } else { - System.out.println("SOST Client enabled"); addSosTConfig(sensorsConfig, user, password); } - } - if(shouldStore(prefs)) { + if (shouldStore(prefs)) { File dbFile = new File(getApplicationContext().getFilesDir() + "/db/"); dbFile.mkdirs(); MVObsSystemDatabaseConfig basicStorageConfig = new MVObsSystemDatabaseConfig(); basicStorageConfig.moduleClass = "org.sensorhub.impl.persistence.h2.MVObsStorageImpl"; basicStorageConfig.storagePath = dbFile.getAbsolutePath() + "/${STORAGE_ID}.dat"; basicStorageConfig.autoStart = true; - -// sosConfig.newStorageConfig = basicStorageConfig; - -// StreamStorageConfig androidStreamStorageConfig = createStreamStorageConfig(androidSensorsConfig); -// addStorageConfig(androidSensorsConfig, androidStreamStorageConfig); - - /* File dbFile = new File(getApplicationContext().getFilesDir() + "/db/"); - dbFile.mkdirs(); - - MVStorageConfig storageConfig = new MVStorageConfig(); - storageConfig.setStorageIdentifier("OSH_CONNECT_OBS");*/ } -// SensorDataProviderConfig androidDataProviderConfig = createDataProviderConfig(androidSensorsConfig); -// addSosServerConfig(sosConfig, androidDataProviderConfig); - // END SOS CONFIG ************************************************************************** - // TruPulse sensor boolean enabled = prefs.getBoolean("trupulse_enabled", false); if (enabled) { TruPulseConfig trupulseConfig = new TruPulseConfig(); - // add target geolocation processing if GPS is enabled if (sensorsConfig.activateGpsLocation) { String gpsOutputName = null; @@ -454,7 +443,6 @@ public boolean verify(String arg0, SSLSession arg1) { ((TruPulseWithGeolocConfig)trupulseConfig).locationOutputName = gpsOutputName; } - trupulseConfig.id = "TRUPULSE_SENSOR"; trupulseConfig.name = "TruPulse Range Finder [" + deviceName + "]"; trupulseConfig.autoStart = true; @@ -464,30 +452,26 @@ public boolean verify(String arg0, SSLSession arg1) { btConf.protocol.deviceName = prefs.getString("trupulse_device_address", ""); if (prefs.getBoolean("trupulse_simu", false)) btConf.moduleClass = SimulatedDataStream.class.getCanonicalName(); - else{ + else { btConf.moduleClass = BluetoothCommProvider.class.getCanonicalName(); trupulseConfig.connection.connectTimeout = 100000; trupulseConfig.connection.reconnectAttempts = 10; } trupulseConfig.commSettings = btConf; - - sensorhubConfig.add(trupulseConfig); } // STE Rad Pager sensor enabled = prefs.getBoolean("ste_radpager_enabled", false); - if(enabled){ + if (enabled) { STERadPagerConfig steRadPagerConfig = new STERadPagerConfig(); steRadPagerConfig.id = "STE_RADPAGER_SENSOR"; steRadPagerConfig.name = "STE Rad Pager [" + deviceName + "]"; steRadPagerConfig.autoStart = true; steRadPagerConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; - sensorhubConfig.add(steRadPagerConfig); } - // Meshtastic Sensor enabled = prefs.getBoolean("meshtastic_enabled", false); if (enabled) @@ -499,12 +483,10 @@ public boolean verify(String arg0, SSLSession arg1) { meshtasticConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; meshtasticConfig.device_name = prefs.getString("meshtastic_device_address", ""); meshtasticConfig.uid_extension = prefs.getString("uid_extension", ""); - - sensorhubConfig.add(meshtasticConfig); } - // polar heart Sensor + // Polar heart Sensor enabled = prefs.getBoolean("polar_enabled", false); if (enabled) { PolarConfig polarConfig = new PolarConfig(); @@ -514,13 +496,10 @@ public boolean verify(String arg0, SSLSession arg1) { polarConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; polarConfig.device_name = prefs.getString("polar_device_address", ""); polarConfig.uid_extension = prefs.getString("uid_extension", ""); - - sensorhubConfig.add(polarConfig); } -// // Kestrel Weather - + // Kestrel Weather enabled = prefs.getBoolean("kestrel_enabled", false); if (enabled) { BleConfig bleConf = new BleConfig(); @@ -537,86 +516,13 @@ public boolean verify(String arg0, SSLSession arg1) { kestrelConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; kestrelConfig.networkID = bleConf.id; kestrelConfig.deviceAddress = prefs.getString("kestrel_device_address", ""); - sensorhubConfig.add(kestrelConfig); } - // FLIR One Edge sensor -// enabled = prefs.getBoolean("flirone_enabled", false); -// if (enabled) -// { -// -// // perhaps do a wireless comm module -// FlirOneConfig flironeConfig = new FlirOneConfig(); -// flironeConfig.id = "FLIRONE_EDGE_SENSOR"; -// flironeConfig.name = "FLIR One Thermal Edge Camera [" + deviceName + "]"; -// flironeConfig.autoStart = true; -//// flironeConfig.androidContext = this.getApplicationContext(); -//// flironeConfig.camPreviewTexture = boundService.getVideoTexture(); -// showVideo = true; -// sensorhubConfig.add(flironeConfig); -// } - - // AngelSensor -// enabled = prefs.getBoolean("angel_enabled", false); -// if (enabled) -// { -// BleConfig bleConf = new BleConfig(); -// bleConf.id = "BLE"; -// bleConf.moduleClass = BleNetwork.class.getCanonicalName(); -// bleConf.androidContext = this.getApplicationContext(); -// bleConf.autoStart = true; -// sensorhubConfig.add(bleConf); -// -// AngelSensorConfig angelConfig = new AngelSensorConfig(); -// angelConfig.id = "ANGEL_SENSOR"; -// angelConfig.name = "Angel Sensor [" + deviceName + "]"; -// angelConfig.autoStart = true; -// angelConfig.networkID = bleConf.id; -// //angelConfig.btAddress = "00:07:80:79:04:AF"; // mike -// //angelConfig.btAddress = "00:07:80:03:0E:0A"; // alex -// angelConfig.btAddress = prefs.getString("angel_address", null); -// sensorhubConfig.add(angelConfig); - -/** - // FLIR One sensor - enabled = prefs.getBoolean("flirone_enabled", false); - if (enabled) - { - FlirOneCameraConfig flironeConfig = new FlirOneCameraConfig(); - flironeConfig.id = "FLIRONE_SENSOR"; - flironeConfig.name = "FLIR One Camera [" + deviceName + "]"; - flironeConfig.autoStart = true; - flironeConfig.androidContext = this.getApplicationContext(); - flironeConfig.camPreviewTexture = boundService.getVideoTexture(); - showVideo = true; - sensorhubConfig.add(flironeConfig); - addSosTConfig(flironeConfig, sosUser, sosPwd); - } - - // DJI Drone - /*enabled = prefs.getBoolean("dji_enabled", false); - if (enabled) - { - DjiConfig djiConfig = new DjiConfig(); - djiConfig.id = "DJI_DRONE"; - djiConfig.name = "DJI Aircraft [" + deviceName + "]"; - djiConfig.autoStart = true; - djiConfig.androidContext = this.getApplicationContext(); - djiConfig.camPreviewTexture = boundService.getVideoTexture(); - showVideo = true; - sensorhubConfig.add(djiConfig); - addSosTConfig(djiConfig, sosUser, sosPwd); - }*/ - - if(isApiServiceEnabled){ - // add connected sys service - System.out.println("Connected Systems Service enabled"); + if (isApiServiceEnabled) { sensorhubConfig.add(conSysApiService); } - if(isSosServiceEnabled){ - // add sos service - System.out.println("SOS Service enabled"); + if (isSosServiceEnabled) { sensorhubConfig.add(sosConfig); } } @@ -627,7 +533,7 @@ protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) return; SOSTClientConfig sosConfig = new SOSTClientConfig(); sosConfig.id = sensorConf.id + "_SOST"; - sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "");// + "SOS-T Client"; + sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", ""); sosConfig.autoStart = true; sosConfig.sos.remoteHost = clientURL.getHost(); sosConfig.sos.remotePort = clientURL.getPort() < 0 ? clientURL.getDefaultPort() : clientURL.getPort(); @@ -661,26 +567,12 @@ protected void addCSApiConfig(SensorConfig sensorConf, String apiUser, String ap consysConfig.connection.connectTimeout = 10000; consysConfig.connection.reconnectAttempts = 9; consysConfig.httpClientImplClass = OkHttpClientWrapper.class.getCanonicalName(); - consysConfig.dataSourceSelector = new ObsSystemDatabaseViewConfig(); - consysConfig.conSysOAuth = oAuthConfig; - sensorhubConfig.add(consysConfig); } - private void requestBatteryOptimizationExemption() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (!pm.isIgnoringBatteryOptimizations(getPackageName())) { - Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - } - } - - } @SuppressLint("HandlerLeak") @Override protected void onCreate(Bundle savedInstanceState) @@ -692,140 +584,111 @@ protected void onCreate(Bundle savedInstanceState) MaterialToolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); - mainInfoArea = findViewById(R.id.main_info); - videoInfoArea = findViewById(R.id.video_info); - - // listen to texture view lifecycle - TextureView textureView = findViewById(R.id.video); - textureView.setSurfaceTextureListener(this); + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); + } + + // Set up Navigation + Fragment homeFragment = new DashboardFragment(); + Fragment sensorsFragment = new SensorsFragment(); + Fragment settingsFragment = new SettingsFragment(); + BottomNavigationView bottomNav = findViewById(R.id.bottom_nav); + + bottomNav.setOnNavigationItemSelectedListener(item -> { + switch (item.getItemId()) { + case R.id.dashboard: + setCurrentFragment(homeFragment); + break; + case R.id.sensors: + setCurrentFragment(sensorsFragment); + break; + case R.id.settings: + setCurrentFragment(settingsFragment); + break; + } + return true; + }); + // Show dashboard on initial load + setCurrentFragment(homeFragment); + bottomNav.setSelectedItemId(R.id.dashboard); hasBluetoothPermissions(); - checkForPermissions(); - // bind to SensorHub service + + // Bind to SensorHub service Intent intent = new Intent(this, SensorHubService.class); - startService(intent); // ADD THIS LINE + startService(intent); bindService(intent, sConn, Context.BIND_AUTO_CREATE); - // handler to refresh sensor status in UI - displayHandler = new Handler(Looper.getMainLooper()); - setupBroadcastReceivers(); requestBatteryOptimizationExemption(); - - // Due to changes with OSH, it may be best to create and start the hub immediately - // This allows us access to the module registry created by default -// boundService.initSensorhub(); } - private boolean hasBluetoothPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED - && checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED; - } - return true; // Older versions handled by existing checks + private void setCurrentFragment(Fragment fragment) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.flFragment, fragment) + .commit(); } - Menu optionsMenu; - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); - optionsMenu = menu; - updateToggleButton(); - + // may need to change icon color here instead ?? lets find out return true; } - private void updateToggleButton() { - if (optionsMenu == null) return; - MenuItem toggleItem = optionsMenu.findItem(R.id.action_toggle); - if (toggleItem == null) return; - if (oshStarted) { - toggleItem.setIcon(R.drawable.ic_stop); - toggleItem.setTitle(R.string.action_stop); - } else { - toggleItem.setIcon(R.drawable.ic_play); - toggleItem.setTitle(R.string.action_start); - } - } - - @Override public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); - if (id == R.id.action_settings) + if (id == R.id.action_about) { - startActivity(new Intent(this, UserSettingsActivity.class)); - return true; - } - else if (id == R.id.action_toggle) - { - if (!oshStarted) { - // Start - if (boundService != null && boundService.getSensorHub() == null) - showRunNamePopup(); - } else { - // Stop - stopListeningForEvents(); - stopRefreshingStatus(); - sostClients.clear(); - conSysClients.clear(); - if (boundService != null) - boundService.stopSensorHub(); - mainInfoArea.setBackgroundColor(getResources().getColor(R.color.md_theme_surface, getTheme())); - oshStarted = false; - updateToggleButton(); - newStatusMessage("SensorHub Stopped"); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - } + showAboutPopup(); return true; } - else if (id == R.id.action_about) + else if (id == R.id.action_meshtastic) { - showAboutPopup(); - } - else if (id == R.id.action_meshtastic) { - showMeshtasticDialog(); + return true; } - else if(id == R.id.action_status) - { + // maybe need to add the activity status back in here for the services + else if(id == R.id.action_status) { Intent statusIntent = new Intent(this, AppStatusActivity.class); - if(boundService.sensorhub != null) { + + if (boundService != null && boundService.sensorhub != null) { ModuleRegistry moduleRegistry = boundService.sensorhub.getModuleRegistry(); Collection> modules = moduleRegistry.getLoadedModules(); for (IModule module : modules) { var moduleConf = module.getConfiguration(); - String status = module.getCurrentState().name(); - - switch (((ModuleConfig) moduleConf).id) { - 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 "ANDROID_SENSORS": - statusIntent.putExtra("androidSensorStatus", status); - break; - case "ANDROID_SENSORS#storage": - statusIntent.putExtra("sensorStorageStatus", status); - break; + + 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 "ANDROID_SENSORS": + statusIntent.putExtra("androidSensorStatus", status); + break; + case "ANDROID_SENSORS#storage": + statusIntent.putExtra("sensorStorageStatus", status); + break; + } } } - } - else { + } else { statusIntent.putExtra("sosService", "N/A"); statusIntent.putExtra("conSysService", "N/A"); statusIntent.putExtra("httpStatus", "N/A"); @@ -833,31 +696,40 @@ else if(id == R.id.action_status) statusIntent.putExtra("sensorStorageStatus", "N/A"); } -// statusIntent.putExtra("boundService", boundService); - - startActivity(statusIntent); return true; } - return super.onOptionsItemSelected(item); } - protected void showMeshtasticDialog() { + @Override + protected void onDestroy() + { + if (broadcastReceiver != null) { + unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + if (boundService != null) { + unbindService(sConn); + boundService = null; + } + super.onDestroy(); + } + // ==================== Dialogs ==================== + + protected void showMeshtasticDialog() { LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_meshtastic, null); EditText messageInput = dialogView.findViewById(R.id.msg_input); + EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle("Send Meshtastic Message"); builder.setView(dialogView); - EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); - - builder.setPositiveButton("Send", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String msg = messageInput.getText().toString(); @@ -870,7 +742,6 @@ public void onClick(DialogInterface dialog, int id) { } }); - builder.setNegativeButton("Cancel", null); builder.show(); } @@ -892,84 +763,15 @@ private void sendMeshtasticMessage(String message, String nodeId) throws IOExcep .build(); textMessageControl.submitCommand(cmd); - } - - protected synchronized void showRunNamePopup() { - AlertDialog.Builder alert = new AlertDialog.Builder(this); - - alert.setTitle("Run Name"); - alert.setMessage("Please enter the name for this run"); - - // Set an EditText view to get user input - final EditText input = new EditText(this); - input.getText().append("Run-"); - SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); - input.getText().append(formatter.format(new Date())); - alert.setView(input); - - alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() - { - public void onClick(DialogInterface dialog, int whichButton) - { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - String runName = input.getText().toString(); - - - updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); - - AndroidSensorsConfig androidSensorConfig = (AndroidSensorsConfig) sensorhubConfig.get("ANDROID_SENSORS"); - VideoEncoderConfig videoConfig = androidSensorConfig.videoConfig; - - boolean cameraInUse = (androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera); - boolean improperVideoSettings = (videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length); - - if (cameraInUse && improperVideoSettings) { - showVideoConfigErrorPopup(); - newStatusMessage("Video Config Error: Check Settings"); - } else { - newStatusMessage("Starting SensorHub..."); - sostClients.clear(); - conSysClients.clear(); - boundService.startSensorHub(sensorhubConfig, showVideo); - - if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); - - while(boundService.getSensorHub() == null){ - System.out.println("Waiting for BoundService Hub to start..."); - } - System.out.println("BoundService SensorHub Started..."); - while(boundService.getSensorHub().getEventBus() == null){ - System.out.println("Waiting for BoundService Hub EventBus to start..."); - } - System.out.println("BoundService SensorHub EventBus Started..."); - EventBus shEvtBus = (EventBus) boundService.getSensorHub().getEventBus(); - - shEvtBus.newSubscription() - .withTopicID(ModuleRegistry.EVENT_GROUP_ID) - .subscribe(mainActivity); - } - - } - }); - - alert.setNegativeButton("Cancel", (dialog, whichButton) -> { - }); - - alert.show(); } - protected void showAboutPopup() { String version = "?"; - try - { + try { PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); version = pInfo.versionName; - } - catch (PackageManager.NameNotFoundException e) - { + } catch (PackageManager.NameNotFoundException e) { } String message = "A software platform for building smart sensor networks and the Internet of Things\n\n"; @@ -982,274 +784,10 @@ protected void showAboutPopup() { alert.show(); } - protected void showVideoConfigErrorPopup() { - String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; - - AlertDialog.Builder alert = new AlertDialog.Builder(this); - alert.setTitle("OpenSensorHub"); - alert.setMessage(message); - alert.setPositiveButton("OK", (dialog, id) -> { - // user accepted - }); - alert.show(); - } + // ==================== Utilities ==================== - - protected void startRefreshingStatus() { - if (displayCallback != null) - return; - - // handler to display async messages in UI - displayCallback = new Runnable() - { - public void run() - { - displayStatus(); - mainInfoArea.setText(Html.fromHtml(mainInfoText.toString())); - videoInfoArea.setText(Html.fromHtml(videoInfoText.toString())); - displayHandler.postDelayed(this, 1000); - } - }; - - displayHandler.post(displayCallback); - } - - - protected void stopRefreshingStatus() - { - if (displayCallback != null) - { - displayHandler.removeCallbacks(displayCallback); - displayCallback = null; - } - } - - - protected synchronized void displayStatus() { - - boolean needsRestart = false; - - mainInfoText.setLength(0); - - // first display error messages if any - for (SOSTClient client: sostClients) - { - Map dataStreams = client.getDataStreams(); - boolean showError = (client.getCurrentError() != null); - boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); - - if (showError || showMsg) - { - mainInfoText.append("

" + client.getName() + ":
"); - if (showMsg) - mainInfoText.append(client.getStatusMessage() + "
"); - if (showError) - { - Throwable errorObj = client.getCurrentError(); - String errorMsg = errorObj.getMessage().trim(); - if (!errorMsg.endsWith(".")) - errorMsg += ". "; - if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) - errorMsg += errorObj.getCause().getMessage(); - mainInfoText.append("" + errorMsg + ""); - } - mainInfoText.append("

"); - - } - } - - - for (ConSysApiClientModule client: conSysClients) - { - Map dataStreams = client.getDataStreams(); - boolean showError = (client.getCurrentError() != null); - boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); - - if (showError || showMsg) - { - mainInfoText.append("

" + client.getName() + ":
"); - if (showMsg) - mainInfoText.append(client.getStatusMessage() + "
"); - if (showError) - { - Throwable errorObj = client.getCurrentError(); - String errorMsg = errorObj.getMessage().trim(); - if (!errorMsg.endsWith(".")) - errorMsg += ". "; - if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) - errorMsg += errorObj.getCause().getMessage(); - mainInfoText.append("" + errorMsg + ""); - } - mainInfoText.append("

"); - } - - log.debug("[CONSYS CLIENT CONNECTION]", client.isConnected()); - } - - // then display streams status - mainInfoText.append("

"); - for (SOSTClient client: sostClients) - { - mainInfoText.append("SOS-T Client"); - mainInfoText.append("

"); - - Map dataStreams = client.getDataStreams(); - long now = System.currentTimeMillis(); - - for (Entry stream : dataStreams.entrySet()) - { - mainInfoText.append("" + stream.getKey() + " : "); - - long lastEventTime = stream.getValue().lastEventTime; - long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) - mainInfoText.append("NO OBS"); - else if (dt > stream.getValue().measPeriodMs) - mainInfoText.append("NOK (" + dt + "ms ago)"); - else - mainInfoText.append("OK (" + dt + "ms ago)"); - - if (stream.getValue().errorCount > 0) - { - mainInfoText.append(" ("); - mainInfoText.append(stream.getValue().errorCount); - mainInfoText.append(")"); - } - - mainInfoText.append("
"); - } - - } - - for (ConSysApiClientModule client: conSysClients) - { - mainInfoText.append("ConSysApi Client"); - mainInfoText.append("

"); - - Map dataStreams = client.getDataStreams(); - long now = System.currentTimeMillis(); - - for (Entry stream : dataStreams.entrySet()) - { - mainInfoText.append("" + stream.getKey() + " : "); - - long lastEventTime = stream.getValue().lastEventTime; - long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) - mainInfoText.append("NO OBS"); - else if (dt > stream.getValue().measPeriodMs) - mainInfoText.append("NOK (" + dt + "ms ago)"); - else - mainInfoText.append("OK (" + dt + "ms ago)"); - - if (stream.getValue().errorCount > 0) - { - mainInfoText.append(" ("); - mainInfoText.append(stream.getValue().errorCount); - mainInfoText.append(")"); - } - - mainInfoText.append("
"); - } - } - mainInfoText.append("

"); - - if (mainInfoText.length() > 5) - mainInfoText.setLength(mainInfoText.length()-5); // remove last
- mainInfoText.append("

"); - - // Notify we are running when no data is being pushed - boolean serveOrStore = shouldServe(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)) || shouldStore(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)); - if(sostClients.isEmpty() && serveOrStore){ - mainInfoText.append("No Sensors Set to Push Remotely"); - } - - if(conSysClients.isEmpty() && serveOrStore){ - mainInfoText.append("No Sensors Set to Push Remotely"); - } - - // show video info - if (androidSensors != null && boundService.hasVideo()) - { -// TODO: Fix crash resulting from this (620) - try { - VideoEncoderConfig config = androidSensors.getConfiguration().videoConfig; - VideoPreset preset = config.presets[config.selectedPreset]; - videoInfoText.setLength(0); - videoInfoText.append("") - .append(config.codec).append(", ") - .append(preset.width).append("x").append(preset.height).append(", ") - .append(config.frameRate).append(" fps, ") - .append(preset.selectedBitrate).append(" kbits/s") - .append(""); - }catch (Exception e){ - log.error("Exception thrown trying to disaply video", e.getMessage()); - } - } - - } - - protected synchronized void newStatusMessage(String msg) - { - mainInfoText.setLength(0); - appendStatusMessage(msg); - } - - - protected synchronized void appendStatusMessage(String msg) - { - mainInfoText.append(msg); - - displayHandler.post(new Runnable() - { - public void run() - { - mainInfoArea.setText(mainInfoText.toString()); - } - }); - } - - - protected void startListeningForEvents() { - if (boundService == null || boundService.getSensorHub() == null){ - - } - - // TODO: Implement a listener that can sub to the status of the hub -// boundService.getSensorHub().getModuleRegistry().registerListener(this); - - } - - - protected void stopListeningForEvents() - { - if (boundService == null || boundService.getSensorHub() == null){ - - } - - // TODO: Unsub the listener here -// boundService.getSensorHub().getModuleRegistry().unregisterListener(this); - } - - - - protected void showVideo() - { - if (boundService.getVideoTexture() != null) - { - TextureView textureView = (TextureView) findViewById(R.id.video); - if (textureView.getSurfaceTexture() != boundService.getVideoTexture()) - textureView.setSurfaceTexture(boundService.getVideoTexture()); - } - } - - - protected void hideVideo() - { - } - - private boolean isPushingSensor(Sensors sensor) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + boolean isPushingSensor(Sensors sensor) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (Sensors.Android.equals(sensor)) { if (prefs.getBoolean("accel_enabled", false) @@ -1276,24 +814,21 @@ private boolean isPushingSensor(Sensors sensor) { if (prefs.getBoolean("cam_enabled", false) && prefs.getStringSet("cam_options", Collections.emptySet()).contains("PUSH_REMOTE")) return true; - if(prefs.getBoolean("audio_enabled", false) + if (prefs.getBoolean("audio_enabled", false) && prefs.getStringSet("audio_options", Collections.emptySet()).contains("PUSH_REMOTE")) return true; } else if (Sensors.TruPulse.equals(sensor) || Sensors.TruPulseSim.equals(sensor)) { return prefs.getBoolean("trupulse_enabled", false) && prefs.getStringSet("trupulse_options", Collections.emptySet()).contains("PUSH_REMOTE"); - } else if(Sensors.BLELocation.equals(sensor)){ + } else if (Sensors.BLELocation.equals(sensor)) { return prefs.getBoolean("ble_enable", false) && prefs.getStringSet("ble_options", Collections.emptySet()).contains("PUSH_REMOTE"); - } - else if (Sensors.Meshtastic.equals(sensor)) { + } else if (Sensors.Meshtastic.equals(sensor)) { return prefs.getBoolean("meshtastic_enabled", false) && prefs.getStringSet("meshtastic_options", Collections.emptySet()).contains("PUSH_REMOTE"); - } - else if (Sensors.PolarHRMonitor.equals(sensor)) { + } else if (Sensors.PolarHRMonitor.equals(sensor)) { return prefs.getBoolean("polar_enabled", false) && prefs.getStringSet("polar_options", Collections.emptySet()).contains("PUSH_REMOTE"); - } - else if (Sensors.Kestrel.equals(sensor)) { + } else if (Sensors.Kestrel.equals(sensor)) { return prefs.getBoolean("kestrel_enabled", false) && prefs.getStringSet("kestrel_options", Collections.emptySet()).contains("PUSH_REMOTE"); } @@ -1301,146 +836,23 @@ else if (Sensors.Kestrel.equals(sensor)) { return false; } - - private void setupBroadcastReceivers() { - broadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String origin = intent.getStringExtra("src"); - if (!context.getPackageName().equalsIgnoreCase(origin)) { - String sosEndpointUrl = intent.getStringExtra("sosEndpointUrl"); - String name = intent.getStringExtra("name"); - String sensorId = intent.getStringExtra("sensorId"); - ArrayList properties = intent.getStringArrayListExtra("properties"); - - if (sosEndpointUrl == null || name == null || sensorId == null || properties.size() == 0) { - return; - } - - // register and "start" new sensor, data stream doesn't begin until someone requests data; - try { - boundService.stopSensorHub(); - Thread.sleep(2000); - Log.d("OSHApp", "Starting SensorHub Again"); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); - sostClients.clear(); - boundService.startSensorHub(sensorhubConfig, showVideo); - if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); - - EventBus shEventBus = (EventBus) boundService.getSensorHub().getEventBus(); -// shEventBus.newSubscription() -// .withTopicID(ModuleRegistry.EVENT_GROUP_ID) -// .subscribe(); - } catch (InterruptedException e) { - Log.e("OSHApp", "Error Loading Proxy Sensor", e); - } - + boolean shouldServe(SharedPreferences prefs) { + Map prefMap = prefs.getAll(); + for (Map.Entry pref : prefMap.entrySet()) { + if (pref.getValue() instanceof HashSet) { + if (((HashSet) pref.getValue()).contains("FETCH_LOCAL")) { + return true; } } - }; - IntentFilter filter = new IntentFilter(); - filter.addAction(ACTION_BROADCAST_RECEIVER); - - registerReceiver(broadcastReceiver, filter); - } - - @Override - protected void onStart() - { - super.onStart(); - } - - - @Override - protected void onResume() - { - super.onResume(); - - TextureView textureView = (TextureView) findViewById(R.id.video); - textureView.setSurfaceTextureListener(this); - - if (oshStarted) - { - startListeningForEvents(); - startRefreshingStatus(); - - if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, getTheme())); } - } - - - @Override - protected void onPause() - { - stopListeningForEvents(); - stopRefreshingStatus(); - hideVideo(); - super.onPause(); - } - - - @Override - protected void onStop() - { - stopListeningForEvents(); - stopRefreshingStatus(); - super.onStop(); - } - - - @Override - protected void onDestroy() - { -// stopService(new Intent(this, SensorHubService.class)); - - if (broadcastReceiver != null) { - unregisterReceiver(broadcastReceiver); - broadcastReceiver = null; - } - - // this should stop it from stopping sensorhub and allow it to stay connected when the app closes/ phone shuts off - if (boundService != null) { - unbindService(sConn); - boundService = null; - } - super.onDestroy(); - } - - - @Override - public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) - { - showVideo(); - } - - - @Override - public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) - { - } - - - @Override - public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) - { return false; } - - @Override - public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) - { - } - - private boolean shouldServe(SharedPreferences prefs){ + boolean shouldStore(SharedPreferences prefs) { Map prefMap = prefs.getAll(); - for(Map.Entry pref : prefMap.entrySet()){ - if(pref.getValue() instanceof HashSet) { - if(((HashSet) pref.getValue()).contains("FETCH_LOCAL")) { - Log.d(TAG, "shouldServe: TRUE"); + for (Map.Entry pref : prefMap.entrySet()) { + if (pref.getValue() instanceof HashSet) { + if (((HashSet) pref.getValue()).contains("STORE_LOCAL")) { return true; } } @@ -1448,143 +860,102 @@ private boolean shouldServe(SharedPreferences prefs){ return false; } - private boolean shouldStore(SharedPreferences prefs){ - Map prefMap = prefs.getAll(); - for(Map.Entry pref : prefMap.entrySet()){ - if(pref.getValue() instanceof HashSet) { - if(((HashSet) pref.getValue()).contains("STORE_LOCAL")) { - Log.d(TAG, "shouldStore: TRUE"); - return true;} + private void requestBatteryOptimizationExemption() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (!pm.isIgnoringBatteryOptimizations(getPackageName())) { + Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.parse("package:" + getPackageName())); + startActivity(intent); } } - return false; } - private void checkForPermissions(){ + private boolean hasBluetoothPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + return checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED + && checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED + && checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED; + } + return true; + } + + private void checkForPermissions() { List permissions = new ArrayList<>(); - // Check for necessary permissions - if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.ACCESS_FINE_LOCATION); - } - if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.CAMERA); - } - if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.RECORD_AUDIO); - } - if (checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.BLUETOOTH); - } - if (checkSelfPermission(Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.BLUETOOTH_ADMIN); - } - if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.BLUETOOTH_CONNECT); - } - if (checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.BLUETOOTH_SCAN); - } - if (checkSelfPermission(Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND); - } - if (checkSelfPermission(Manifest.permission.CHANGE_WIFI_STATE) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.CHANGE_WIFI_STATE) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.CHANGE_WIFI_STATE); - } - if (checkSelfPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - } - if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.POST_NOTIFICATIONS); - } - if (checkSelfPermission(Manifest.permission.FOREGROUND_SERVICE) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.FOREGROUND_SERVICE) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.FOREGROUND_SERVICE); - } - if (checkSelfPermission(Manifest.permission.WAKE_LOCK) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.WAKE_LOCK) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.WAKE_LOCK); - } - if (checkSelfPermission(Manifest.permission.INTERNET) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.INTERNET) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.INTERNET); - } - if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE); - } - if (checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.READ_PHONE_STATE); - } - if (checkSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) == PackageManager.PERMISSION_DENIED) { + if (checkSelfPermission(Manifest.permission.ACCESS_NETWORK_STATE) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.ACCESS_NETWORK_STATE); - } - // Does app actually need storage permissions now? String[] permARR = new String[permissions.size()]; permARR = permissions.toArray(permARR); - if(permARR.length >0) { + if (permARR.length > 0) { requestPermissions(permARR, 100); } } - @Override - public void onSubscribe(Flow.Subscription subscription) { - this.subscription = subscription; - System.out.println("MainActivity Subscribed..."); - subscription.request(10); - } - - @Override - public void onNext(Event e) { - System.out.println("Event of : " + e); - - System.out.println(e.getSource()); - if (e instanceof ModuleEvent) - { - - // start refreshing status on first module loaded - if (!oshStarted && ((ModuleEvent) e).getType() == ModuleEvent.Type.LOADED) - { - oshStarted = true; - runOnUiThread(this::updateToggleButton); - startRefreshingStatus(); - return; - } + private void setupBroadcastReceivers() { + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String origin = intent.getStringExtra("src"); + if (!context.getPackageName().equalsIgnoreCase(origin)) { + String sosEndpointUrl = intent.getStringExtra("sosEndpointUrl"); + String name = intent.getStringExtra("name"); + String sensorId = intent.getStringExtra("sensorId"); + ArrayList properties = intent.getStringArrayListExtra("properties"); - // detect when Android sensor driver is started - else if (e.getSource() instanceof AndroidSensorsDriver) - { - this.androidSensors = (AndroidSensorsDriver)e.getSource(); - } + if (sosEndpointUrl == null || name == null || sensorId == null || properties.size() == 0) { + return; + } - // detect when SOS-T modules are connected - else if (e.getSource() instanceof SOSTClient && ((ModuleEvent)e).getType() == ModuleEvent.Type.STATE_CHANGED) - { - switch (((ModuleEvent)e).getNewState()) - { - case INITIALIZING: - sostClients.add((SOSTClient)e.getSource()); - break; - } - } - else if (e.getSource() instanceof ConSysApiClientModule && ((ModuleEvent)e).getType() == ModuleEvent.Type.STATE_CHANGED) - { - switch (((ModuleEvent)e).getNewState()) - { - case INITIALIZING: - conSysClients.add((ConSysApiClientModule)e.getSource()); - break; + try { + boundService.stopSensorHub(); + Thread.sleep(2000); + Log.d("OSHApp", "Starting SensorHub Again"); + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); + sostClients.clear(); + boundService.startSensorHub(sensorhubConfig, showVideo); + } catch (InterruptedException e) { + Log.e("OSHApp", "Error Loading Proxy Sensor", e); + } } } - } - - subscription.request(10); - } - - @Override - public void onError(Throwable throwable) { - - } - - @Override - public void onComplete() { - + }; + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_BROADCAST_RECEIVER); + registerReceiver(broadcastReceiver, filter); } -} \ No newline at end of file +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorHubServiceProvider.java b/sensorhub-android-app/src/org/sensorhub/android/SensorHubServiceProvider.java new file mode 100644 index 00000000..cafaec36 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorHubServiceProvider.java @@ -0,0 +1,23 @@ +package org.sensorhub.android; + +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 java.util.ArrayList; + +public interface SensorHubServiceProvider { + SensorHubService getBoundService(); + boolean isOshStarted(); + void setOshStarted(boolean started); + IModuleConfigRepository getSensorhubConfig(); + ArrayList getSostClients(); + ArrayList getConSysClients(); + AndroidSensorsDriver getAndroidSensors(); + void setAndroidSensors(AndroidSensorsDriver driver); + boolean getShowVideo(); + void updateConfig(android.content.SharedPreferences prefs, String runName); + void startSensorHub(); + void stopSensorHub(); +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java new file mode 100644 index 00000000..1818f218 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -0,0 +1,315 @@ +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", "accel_options"}, + {"gyro_enabled", "gyro_options"}, + {"mag_enabled", "mag_options"}, + {"orient_quat_enabled", "orient_quat_options"}, + {"orient_euler_enabled","orient_euler_options"}, + {"gps_enabled", "gps_options"}, + {"netloc_enabled", "netloc_options"}, + {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_preset", "camera_select"}, + {"video_roll_enabled", "video_roll_options"}, + {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"}, + {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"}, + {"polar_enabled", "polar_device_address", "polar_options"}, + {"kestrel_enabled", "kestrel_device_address", "kestrel_options"}, + {"trupulse_enabled", "trupulse_datasource", "trupulse_options", "trupulse_device_address", "trupulse_simu"}, + {"angel_enabled", "angel_address", "angel_options"}, + {"flirone_enabled", "flir_options"}, + {"ste_radpager_enabled","ste_radpager_options"}, + }; + + /** Keys of Preferences that use the Bluetooth device picker dialog */ + private static final String[] BT_DEVICE_PREF_KEYS = { + "meshtastic_device_address", + "polar_device_address", + "kestrel_device_address", + "trupulse_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); + + // Wire up switch visibility toggling + 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; + }); + } + + // Populate video and audio preference lists dynamically + setupVideoPreferences(); + setupAudioPreferences(); + + // Wire up Bluetooth device picker for all BLE device preferences + setupBluetoothDevicePickers(); + } + + // ==================== Bluetooth Device Picker ==================== + + private void setupBluetoothDevicePickers() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + + for (String key : BT_DEVICE_PREF_KEYS) { + Preference pref = findPreference(key); + if (pref == null) continue; + + // Show the currently saved address in the summary + 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); + } + } + + // Add manual entry option at the end + names.add("Enter name or address manually..."); + addresses.add(null); + + String[] displayNames = names.toArray(new String[0]); + + new AlertDialog.Builder(requireContext()) + .setTitle("Select Device") + .setItems(displayNames, (dialog, which) -> { + if (addresses.get(which) == null) { + // Manual entry + showManualAddressDialog(prefKey); + } else { + saveDeviceAddress(prefKey, addresses.get(which), displayNames[which]); + } + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showManualAddressDialog(String prefKey) { + EditText input = new EditText(requireContext()); + input.setInputType(InputType.TYPE_CLASS_TEXT); + input.setHint("e.g. Ballistic or AA:BB:CC:DD:EE:FF"); + + // Load current value if any + 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("Enter Device Name or Address") + .setMessage("Enter a device name (e.g. \"Ballistic\") or MAC address. Names are matched from the start, case-insensitive.") + .setView(container) + .setPositiveButton("OK", (dialog, which) -> { + String address = input.getText().toString().trim(); + if (!address.isEmpty()) { + saveDeviceAddress(prefKey, address, address); + } + }) + .setNegativeButton("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); + } + } + + // ==================== Video Preferences ==================== + + 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 + try { + Camera 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); + camera.release(); + } catch (Exception e) { + frameRateList.add("30"); + resList.add("640x480"); + } + + ListPreference frameRatePrefList = findPreference("video_framerate"); + if (frameRatePrefList != null) { + frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); + frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); + } + + // Preset list + ListPreference selectedPresetList = findPreference("video_preset"); + if (selectedPresetList != null) { + ArrayList presetNames = new ArrayList<>(); + ArrayList presetIndexes = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + presetNames.add("Video Preset #" + (i + 1)); + presetIndexes.add(String.valueOf(i)); + } + presetNames.add("Auto select"); + presetIndexes.add("AUTO"); + selectedPresetList.setEntries(presetNames.toArray(new String[0])); + selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); + } + } + + private void updateCameraSettings(int cameraId) { + try { + frameRateList.clear(); + resList.clear(); + Camera 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); + camera.release(); + + ListPreference frameRatePrefList = findPreference("video_framerate"); + if (frameRatePrefList != null) { + frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); + frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); + } + } catch (Exception e) { + Log.e("SensorsFragment", "Error updating camera settings", e); + } + } + + // ==================== Audio Preferences ==================== + + 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])); + } + } + + // ==================== Helpers ==================== + + 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 new file mode 100644 index 00000000..2aa6df71 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -0,0 +1,232 @@ +package org.sensorhub.android; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.EditTextPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.SwitchPreferenceCompat; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/* + * Fragment for settings preferences + */ +public class SettingsFragment extends PreferenceFragmentCompat { + + private static final String PREF_SAVED_SERVERS = "saved_servers_set"; + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.pref_settings, rootKey); + + setupSavedServers(); + setupOAuthToggle(); + } + + // ==================== Saved Servers ==================== + + private void setupSavedServers() { + Preference selectPref = findPreference("saved_servers"); + Preference savePref = findPreference("save_current_server"); + Preference removePref = findPreference("remove_saved_server"); + + if (selectPref != null) { + updateSavedServersSummary(selectPref); + selectPref.setOnPreferenceClickListener(p -> { + showSelectServerDialog(); + return true; + }); + } + + if (savePref != null) { + savePref.setOnPreferenceClickListener(p -> { + saveCurrentServer(); + return true; + }); + } + + if (removePref != null) { + removePref.setOnPreferenceClickListener(p -> { + showRemoveServerDialog(); + return true; + }); + } + } + + private List getSavedServers() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + Set serverSet = prefs.getStringSet(PREF_SAVED_SERVERS, new HashSet<>()); + return new ArrayList<>(serverSet); + } + + private void putSavedServers(Set servers) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + prefs.edit().putStringSet(PREF_SAVED_SERVERS, new HashSet<>(servers)).apply(); + } + + private String getDisplayName(String entry) { + // entry format: "ip|port|name" + String[] parts = entry.split("\\|", 3); + if (parts.length >= 3) return parts[2] + " (" + parts[0] + ":" + parts[1] + ")"; + return entry; + } + + private void updateSavedServersSummary(Preference pref) { + List servers = getSavedServers(); + if (servers.isEmpty()) { + pref.setSummary("No saved servers"); + } else { + pref.setSummary(servers.size() + " saved server(s)"); + } + } + + private void saveCurrentServer() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + String name = prefs.getString("server_name", "").trim(); + String ip = prefs.getString("ip_address", "").trim(); + String port = prefs.getString("port", "").trim(); + + if (ip.isEmpty() || port.isEmpty()) { + Toast.makeText(requireContext(), "Server address and port are required", Toast.LENGTH_SHORT).show(); + return; + } + + if (name.isEmpty()) { + name = ip + ":" + port; + } + + String entry = ip + "|" + port + "|" + name; + + Set servers = new HashSet<>(getSavedServers()); + + // Check for duplicate ip:port + String finalIp = ip; + String finalPort = port; + servers.removeIf(s -> { + String[] parts = s.split("\\|", 3); + return parts.length >= 2 && parts[0].equals(finalIp) && parts[1].equals(finalPort); + }); + + servers.add(entry); + putSavedServers(servers); + + Preference selectPref = findPreference("saved_servers"); + if (selectPref != null) updateSavedServersSummary(selectPref); + + Toast.makeText(requireContext(), "Server saved: " + name, Toast.LENGTH_SHORT).show(); + } + + private void showSelectServerDialog() { + List servers = getSavedServers(); + if (servers.isEmpty()) { + Toast.makeText(requireContext(), "No saved servers", Toast.LENGTH_SHORT).show(); + return; + } + + String[] displayNames = new String[servers.size()]; + for (int i = 0; i < servers.size(); i++) { + displayNames[i] = getDisplayName(servers.get(i)); + } + + new AlertDialog.Builder(requireContext()) + .setTitle("Select Server") + .setItems(displayNames, (dialog, which) -> { + String[] parts = servers.get(which).split("\\|", 3); + if (parts.length < 3) return; + + EditTextPreference ipPref = findPreference("ip_address"); + EditTextPreference portPref = findPreference("port"); + EditTextPreference namePref = findPreference("server_name"); + + if (ipPref != null) ipPref.setText(parts[0]); + if (portPref != null) portPref.setText(parts[1]); + if (namePref != null) namePref.setText(parts[2]); + + Toast.makeText(requireContext(), "Loaded: " + parts[2], Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showRemoveServerDialog() { + List servers = getSavedServers(); + if (servers.isEmpty()) { + Toast.makeText(requireContext(), "No saved servers to remove", Toast.LENGTH_SHORT).show(); + return; + } + + String[] displayNames = new String[servers.size()]; + boolean[] checked = new boolean[servers.size()]; + for (int i = 0; i < servers.size(); i++) { + displayNames[i] = getDisplayName(servers.get(i)); + checked[i] = false; + } + + new AlertDialog.Builder(requireContext()) + .setTitle("Remove Saved Servers") + .setMultiChoiceItems(displayNames, checked, (dialog, which, isChecked) -> + checked[which] = isChecked + ) + .setPositiveButton("Remove", (dialog, which) -> { + Set remaining = new HashSet<>(); + for (int i = 0; i < servers.size(); i++) { + if (!checked[i]) remaining.add(servers.get(i)); + } + putSavedServers(remaining); + + Preference selectPref = findPreference("saved_servers"); + if (selectPref != null) updateSavedServersSummary(selectPref); + + int removed = servers.size() - remaining.size(); + Toast.makeText(requireContext(), removed + " server(s) removed", Toast.LENGTH_SHORT).show(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + // ==================== Client Mode & OAuth ==================== + + private void setupOAuthToggle() { + SwitchPreferenceCompat clientMode = findPreference("enable_client"); + SwitchPreferenceCompat oauth = findPreference("o_auth_enabled"); + + Preference token = findPreference("token_endpoint"); + Preference clientId = findPreference("client_id"); + Preference secret = findPreference("client_secret"); + + if (clientMode != null) { + boolean isConSys = clientMode.isChecked(); + setVisibility(isConSys, oauth); + setVisibility(isConSys && oauth != null && oauth.isChecked(), token, clientId, secret); + + clientMode.setOnPreferenceChangeListener((pref, value) -> { + boolean enabled = (Boolean) value; + setVisibility(enabled, oauth); + setVisibility(enabled && oauth != null && oauth.isChecked(), token, clientId, secret); + return true; + }); + } + + if (oauth != null) { + oauth.setOnPreferenceChangeListener((pref, value) -> { + boolean isEnabled = (Boolean) value; + setVisibility(isEnabled, token, clientId, secret); + 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/UserSettingsActivity.java b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java index d9a621d6..a8724f24 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java @@ -1,848 +1,848 @@ -/***************************** BEGIN LICENSE BLOCK *************************** - - The contents of this file are subject to the Mozilla Public License, v. 2.0. - If a copy of the MPL was not distributed with this file, You can obtain one - at http://mozilla.org/MPL/2.0/. - - Software distributed under the License is distributed on an "AS IS" basis, - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License - for the specific language governing rights and limitations under the License. - - Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. - ******************************* END LICENSE BLOCK ***************************/ - -package org.sensorhub.android; - -import android.Manifest; -import android.annotation.TargetApi; -import android.app.AlertDialog; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanResult; -import android.content.DialogInterface; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.hardware.Camera; -import android.net.wifi.WifiManager; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.preference.EditTextPreference; -import android.preference.ListPreference; -import android.preference.Preference; -import android.preference.PreferenceActivity; -import android.preference.PreferenceFragment; -import android.preference.PreferenceManager; -import android.preference.PreferenceScreen; -import android.text.InputType; -import android.util.Log; -import android.widget.BaseAdapter; - -import androidx.annotation.RequiresPermission; -import androidx.core.app.ActivityCompat; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.URL; -import java.net.UnknownHostException; -import java.nio.ByteOrder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - - -public class UserSettingsActivity extends PreferenceActivity { - - private static final Logger log = LoggerFactory.getLogger(UserSettingsActivity.class); - - @Override - public void onBuildHeaders(List
target) { - loadHeadersFromResource(R.xml.pref_headers, target); - } - - - /* - * A preference value change listener that updates the preference's summary to reflect its new value. - */ - private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object value) { - String stringValue = value.toString(); - - if (preference instanceof ListPreference listPreference) { - int index = listPreference.findIndexOfValue(stringValue); - preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null); - } else if (preference.getKey().startsWith("video_res")) { - PreferenceScreen presetSettings = (PreferenceScreen) preference; - String frameSize = ((ListPreference) presetSettings.getPreference(0)).getValue(); - String minBitrate = ((EditTextPreference) presetSettings.getPreference(1)).getText(); - String maxBitrate = ((EditTextPreference) presetSettings.getPreference(2)).getText(); - presetSettings.setSummary(frameSize + " @ " + minBitrate + "-" + maxBitrate + " kbits/s"); - ((BaseAdapter) presetSettings.getRootAdapter()).notifyDataSetChanged(); - } else { - preference.setSummary(stringValue); - } - - // detect errors - if (preference.getKey().equals("sos_uri")) { - try { - URL url = new URL(value.toString()); - if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) - throw new Exception("SOS URL must be HTTP or HTTPS"); - } catch (Exception e) { - AlertDialog.Builder dlgAlert = new AlertDialog.Builder(preference.getContext()); - dlgAlert.setMessage("Invalid SOS URL"); - dlgAlert.setTitle(e.getMessage()); - dlgAlert.setPositiveButton("OK", null); - dlgAlert.setCancelable(true); - dlgAlert.create().show(); - } - } - - return true; - } - }; - - - /* - * Binds a preference's summary to its value. More specifically, when the - * preference's value is changed, its summary (line of text below the - * preference title) is updated to reflect the value. The summary is also - * immediately updated upon calling this method. The exact display format is - * dependent on the type of preference. - * - * @see #sBindPreferenceSummaryToValueListener - */ - private static void bindPreferenceSummaryToValue(Preference preference) { - // Set the listener to watch for value changes. - preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); - - // for preference screens, call listener when screen is closed - if (preference instanceof PreferenceScreen) { - preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - ((PreferenceScreen) preference).getDialog().setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, ""); - } - }); - return true; - } - }); - } - - // Trigger the listener immediately with the preference's current value. - sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, PreferenceManager.getDefaultSharedPreferences(preference.getContext()).getString(preference.getKey(), "")); - } - - - /* - * Fragment for general preferences - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class GeneralPreferenceFragment extends PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_general); - bindPreferenceSummaryToValue(findPreference("device_name")); - bindPreferenceSummaryToValue(findPreference("ip_address")); - bindPreferenceSummaryToValue(findPreference("port")); - bindPreferenceSummaryToValue(findPreference("endpoint_path")); - bindPreferenceSummaryToValue(findPreference("username")); - bindPreferenceSummaryToValue(findPreference("password")); - - - WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); - int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); - - // Convert little-endian to big-endianif needed - 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 = "Unable to get IP Address"; - } - - Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); - ipAddressLabel.setSummary(ipAddressString); - - - SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - - Preference oAuthEnabled = getPreferenceScreen().findPreference("o_auth_enabled"); - Preference tokenEndpoint = getPreferenceScreen().findPreference("token_endpoint"); - Preference clientID = getPreferenceScreen().findPreference("client_id"); - Preference clientSecret = getPreferenceScreen().findPreference("client_secret"); - - tokenEndpoint.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); - clientID.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); - clientSecret.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); - - oAuthEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - tokenEndpoint.setEnabled((boolean) newValue); - clientID.setEnabled((boolean) newValue); - clientSecret.setEnabled((boolean) newValue); - return true; - }); - } - } - - - /* - * Fragment for sensor preferences - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class SensorPreferenceFragment extends PreferenceFragment { - - List scannedEntries = new ArrayList<>(); - List scannedEntryValues = new ArrayList<>(); - Set scannedDevices = new HashSet<>(); - - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_sensors); - bindPreferenceSummaryToValue(findPreference("uid_extension")); - bindPreferenceSummaryToValue(findPreference("angel_address")); - - SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); - - Preference accelerometerEnable = getPreferenceScreen().findPreference("accel_enabled"); - Preference accelerometerOptions = getPreferenceScreen().findPreference("accel_options"); - accelerometerOptions.setEnabled(prefs.getBoolean(accelerometerEnable.getKey(), false)); - accelerometerEnable.setOnPreferenceChangeListener((preference, newValue) -> { - accelerometerOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference gyroEnabled = getPreferenceScreen().findPreference("gyro_enabled"); - Preference gyroOptions = getPreferenceScreen().findPreference("gyro_options"); - gyroOptions.setEnabled(prefs.getBoolean(gyroEnabled.getKey(), false)); - gyroEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - gyroOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference magEnabled = getPreferenceScreen().findPreference("mag_enabled"); - Preference magOptions = getPreferenceScreen().findPreference("mag_options"); - magOptions.setEnabled(prefs.getBoolean(magEnabled.getKey(), false)); - magEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - magOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference orientQuatEnabled = getPreferenceScreen().findPreference("orient_quat_enabled"); - Preference orientQuatOptions = getPreferenceScreen().findPreference("orient_quat_options"); - orientQuatOptions.setEnabled(prefs.getBoolean(orientQuatEnabled.getKey(), false)); - orientQuatEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - orientQuatOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference orientEulerEnabled = getPreferenceScreen().findPreference("orient_euler_enabled"); - Preference orientEulerOptions = getPreferenceScreen().findPreference("orient_euler_options"); - orientEulerOptions.setEnabled(prefs.getBoolean(orientEulerEnabled.getKey(), false)); - orientEulerEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - orientEulerOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference gpsEnabled = getPreferenceScreen().findPreference("gps_enabled"); - Preference gpsOptions = getPreferenceScreen().findPreference("gps_options"); - gpsOptions.setEnabled(prefs.getBoolean(gpsEnabled.getKey(), false)); - gpsEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - gpsOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference netlocEnabled = getPreferenceScreen().findPreference("netloc_enabled"); - Preference netlocOptions = getPreferenceScreen().findPreference("netloc_options"); - netlocOptions.setEnabled(prefs.getBoolean(netlocEnabled.getKey(), false)); - netlocEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - netlocOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference camEnabled = getPreferenceScreen().findPreference("cam_enabled"); - Preference camOptions = getPreferenceScreen().findPreference("cam_options"); - camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); - camEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - camOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference videoRollEnabled = getPreferenceScreen().findPreference("video_roll_enabled"); - Preference videoRollOptions = getPreferenceScreen().findPreference("video_roll_options"); - videoRollOptions.setEnabled(prefs.getBoolean(videoRollEnabled.getKey(), false)); - videoRollEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - videoRollOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference audioEnabled = getPreferenceScreen().findPreference("audio_enabled"); - Preference audioOptions = getPreferenceScreen().findPreference("audio_options"); - camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); - audioEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - audioOptions.setEnabled((boolean) newValue); - return true; - }); - - Preference trupulseEnabled = getPreferenceScreen().findPreference("trupulse_enabled"); - Preference trupulseOptions = getPreferenceScreen().findPreference("trupulse_options"); - Preference trupulseDatasource = getPreferenceScreen().findPreference("trupulse_datasource"); - ListPreference trupulseListPref = (ListPreference) getPreferenceScreen().findPreference("trupulse_device_address"); - - trupulseOptions.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); - trupulseDatasource.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); - trupulseEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - trupulseOptions.setEnabled((boolean) newValue); - trupulseDatasource.setEnabled((boolean) newValue); - trupulseListPref.setEnabled((boolean) newValue); - return true; - }); - - Preference meshtasticEnabled = getPreferenceScreen().findPreference("meshtastic_enabled"); - Preference meshtasticOptions = getPreferenceScreen().findPreference("meshtastic_options"); - - ListPreference meshDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("meshtastic_device_address"); - - - Preference polarEnabled = getPreferenceScreen().findPreference("polar_enabled"); - Preference polarOptions = getPreferenceScreen().findPreference("polar_options"); - ListPreference polarDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("polar_device_address"); - polarEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - polarOptions.setEnabled((boolean) newValue); - polarDeviceListPref.setEnabled((boolean) newValue); - return true; - }); - - - Preference kestrelEnabled = getPreferenceScreen().findPreference("kestrel_enabled"); - Preference kestrelOptions = getPreferenceScreen().findPreference("kestrel_options"); -// bindPreferenceSummaryToValue(findPreference("kestrel_device_name")); -// bindPreferenceSummaryToValue(findPreference("kestrel_serial")); - - ListPreference kestrelDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("kestrel_device_address"); - - kestrelEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - kestrelOptions.setEnabled((boolean) newValue); - kestrelDeviceListPref.setEnabled((boolean) newValue); - return true; - }); - - - Preference scanPref = findPreference("scan_ble_devices"); - - scanPref.setOnPreferenceClickListener(preference -> { - startBleScan(); - return true; - }); - - - BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); - if (btAdapter != null && btAdapter.isEnabled()) { - -// if (!scannedEntries.isEmpty()) { -// kestrelDeviceListPref.setEntries(scannedEntries.toArray(new CharSequence[0])); -// kestrelDeviceListPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); +///***************************** BEGIN LICENSE BLOCK *************************** +// +// The contents of this file are subject to the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one +// at http://mozilla.org/MPL/2.0/. +// +// Software distributed under the License is distributed on an "AS IS" basis, +// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +// for the specific language governing rights and limitations under the License. +// +// Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. +// ******************************* END LICENSE BLOCK ***************************/ +// +//package org.sensorhub.android; +// +//import android.Manifest; +//import android.annotation.TargetApi; +//import android.app.AlertDialog; +//import android.bluetooth.BluetoothAdapter; +//import android.bluetooth.BluetoothDevice; +//import android.bluetooth.le.BluetoothLeScanner; +//import android.bluetooth.le.ScanCallback; +//import android.bluetooth.le.ScanResult; +//import android.content.DialogInterface; +//import android.content.SharedPreferences; +//import android.content.pm.PackageManager; +//import android.hardware.Camera; +//import android.net.wifi.WifiManager; +//import android.os.Build; +//import android.os.Bundle; +//import android.os.Handler; +//import android.os.Looper; +//import android.preference.EditTextPreference; +//import android.preference.ListPreference; +//import android.preference.Preference; +//import android.preference.PreferenceActivity; +//import android.preference.PreferenceFragment; +//import android.preference.PreferenceManager; +//import android.preference.PreferenceScreen; +//import android.text.InputType; +//import android.util.Log; +//import android.widget.BaseAdapter; +// +//import androidx.annotation.RequiresPermission; +//import androidx.core.app.ActivityCompat; +// +//import org.slf4j.Logger; +//import org.slf4j.LoggerFactory; +// +//import java.math.BigInteger; +//import java.net.InetAddress; +//import java.net.URL; +//import java.net.UnknownHostException; +//import java.nio.ByteOrder; +//import java.util.ArrayList; +//import java.util.Arrays; +//import java.util.HashSet; +//import java.util.List; +//import java.util.Set; +// +// +//public class UserSettingsActivity extends PreferenceActivity { +// +// private static final Logger log = LoggerFactory.getLogger(UserSettingsActivity.class); +// +// @Override +// public void onBuildHeaders(List
target) { +// loadHeadersFromResource(R.xml.pref_headers, target); +// } +// +// +// /* +// * A preference value change listener that updates the preference's summary to reflect its new value. +// */ +// private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { +// @Override +// public boolean onPreferenceChange(Preference preference, Object value) { +// String stringValue = value.toString(); +// +// if (preference instanceof ListPreference listPreference) { +// int index = listPreference.findIndexOfValue(stringValue); +// preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null); +// } else if (preference.getKey().startsWith("video_res")) { +// PreferenceScreen presetSettings = (PreferenceScreen) preference; +// String frameSize = ((ListPreference) presetSettings.getPreference(0)).getValue(); +// String minBitrate = ((EditTextPreference) presetSettings.getPreference(1)).getText(); +// String maxBitrate = ((EditTextPreference) presetSettings.getPreference(2)).getText(); +// presetSettings.setSummary(frameSize + " @ " + minBitrate + "-" + maxBitrate + " kbits/s"); +// ((BaseAdapter) presetSettings.getRootAdapter()).notifyDataSetChanged(); +// } else { +// preference.setSummary(stringValue); +// } +// +// // detect errors +// if (preference.getKey().equals("sos_uri")) { +// try { +// URL url = new URL(value.toString()); +// if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) +// throw new Exception("SOS URL must be HTTP or HTTPS"); +// } catch (Exception e) { +// AlertDialog.Builder dlgAlert = new AlertDialog.Builder(preference.getContext()); +// dlgAlert.setMessage("Invalid SOS URL"); +// dlgAlert.setTitle(e.getMessage()); +// dlgAlert.setPositiveButton("OK", null); +// dlgAlert.setCancelable(true); +// dlgAlert.create().show(); +// } +// } +// +// return true; +// } +// }; +// +// +// /* +// * Binds a preference's summary to its value. More specifically, when the +// * preference's value is changed, its summary (line of text below the +// * preference title) is updated to reflect the value. The summary is also +// * immediately updated upon calling this method. The exact display format is +// * dependent on the type of preference. +// * +// * @see #sBindPreferenceSummaryToValueListener +// */ +// private static void bindPreferenceSummaryToValue(Preference preference) { +// // Set the listener to watch for value changes. +// preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); +// +// // for preference screens, call listener when screen is closed +// if (preference instanceof PreferenceScreen) { +// preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { +// @Override +// public boolean onPreferenceClick(Preference preference) { +// ((PreferenceScreen) preference).getDialog().setOnCancelListener(new DialogInterface.OnCancelListener() { +// @Override +// public void onCancel(DialogInterface dialog) { +// sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, ""); +// } +// }); +// return true; +// } +// }); +// } +// +// // Trigger the listener immediately with the preference's current value. +// sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, PreferenceManager.getDefaultSharedPreferences(preference.getContext()).getString(preference.getKey(), "")); +// } +// +// +// /* +// * Fragment for general preferences +// */ +// @TargetApi(Build.VERSION_CODES.HONEYCOMB) +// public static class GeneralPreferenceFragment extends PreferenceFragment { +// @Override +// public void onCreate(Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// addPreferencesFromResource(R.xml.pref_general); +// bindPreferenceSummaryToValue(findPreference("device_name")); +// bindPreferenceSummaryToValue(findPreference("ip_address")); +// bindPreferenceSummaryToValue(findPreference("port")); +// bindPreferenceSummaryToValue(findPreference("endpoint_path")); +// bindPreferenceSummaryToValue(findPreference("username")); +// bindPreferenceSummaryToValue(findPreference("password")); +// +// +// WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); +// int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); +// +// // Convert little-endian to big-endianif needed +// 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 = "Unable to get IP Address"; +// } +// +// Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); +// ipAddressLabel.setSummary(ipAddressString); +// +// +// SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); +// +// Preference oAuthEnabled = getPreferenceScreen().findPreference("o_auth_enabled"); +// Preference tokenEndpoint = getPreferenceScreen().findPreference("token_endpoint"); +// Preference clientID = getPreferenceScreen().findPreference("client_id"); +// Preference clientSecret = getPreferenceScreen().findPreference("client_secret"); +// +// tokenEndpoint.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); +// clientID.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); +// clientSecret.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); +// +// oAuthEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// tokenEndpoint.setEnabled((boolean) newValue); +// clientID.setEnabled((boolean) newValue); +// clientSecret.setEnabled((boolean) newValue); +// return true; +// }); +// } +// } +// +// +// /* +// * Fragment for sensor preferences +// */ +// @TargetApi(Build.VERSION_CODES.HONEYCOMB) +// public static class SensorPreferenceFragment extends PreferenceFragment { +// +// List scannedEntries = new ArrayList<>(); +// List scannedEntryValues = new ArrayList<>(); +// Set scannedDevices = new HashSet<>(); +// +// +// @Override +// public void onCreate(Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// addPreferencesFromResource(R.xml.pref_sensors); +// bindPreferenceSummaryToValue(findPreference("uid_extension")); +// bindPreferenceSummaryToValue(findPreference("angel_address")); +// +// SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); +// +// Preference accelerometerEnable = getPreferenceScreen().findPreference("accel_enabled"); +// Preference accelerometerOptions = getPreferenceScreen().findPreference("accel_options"); +// accelerometerOptions.setEnabled(prefs.getBoolean(accelerometerEnable.getKey(), false)); +// accelerometerEnable.setOnPreferenceChangeListener((preference, newValue) -> { +// accelerometerOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference gyroEnabled = getPreferenceScreen().findPreference("gyro_enabled"); +// Preference gyroOptions = getPreferenceScreen().findPreference("gyro_options"); +// gyroOptions.setEnabled(prefs.getBoolean(gyroEnabled.getKey(), false)); +// gyroEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// gyroOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference magEnabled = getPreferenceScreen().findPreference("mag_enabled"); +// Preference magOptions = getPreferenceScreen().findPreference("mag_options"); +// magOptions.setEnabled(prefs.getBoolean(magEnabled.getKey(), false)); +// magEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// magOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference orientQuatEnabled = getPreferenceScreen().findPreference("orient_quat_enabled"); +// Preference orientQuatOptions = getPreferenceScreen().findPreference("orient_quat_options"); +// orientQuatOptions.setEnabled(prefs.getBoolean(orientQuatEnabled.getKey(), false)); +// orientQuatEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// orientQuatOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference orientEulerEnabled = getPreferenceScreen().findPreference("orient_euler_enabled"); +// Preference orientEulerOptions = getPreferenceScreen().findPreference("orient_euler_options"); +// orientEulerOptions.setEnabled(prefs.getBoolean(orientEulerEnabled.getKey(), false)); +// orientEulerEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// orientEulerOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference gpsEnabled = getPreferenceScreen().findPreference("gps_enabled"); +// Preference gpsOptions = getPreferenceScreen().findPreference("gps_options"); +// gpsOptions.setEnabled(prefs.getBoolean(gpsEnabled.getKey(), false)); +// gpsEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// gpsOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference netlocEnabled = getPreferenceScreen().findPreference("netloc_enabled"); +// Preference netlocOptions = getPreferenceScreen().findPreference("netloc_options"); +// netlocOptions.setEnabled(prefs.getBoolean(netlocEnabled.getKey(), false)); +// netlocEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// netlocOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference camEnabled = getPreferenceScreen().findPreference("cam_enabled"); +// Preference camOptions = getPreferenceScreen().findPreference("cam_options"); +// camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); +// camEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// camOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference videoRollEnabled = getPreferenceScreen().findPreference("video_roll_enabled"); +// Preference videoRollOptions = getPreferenceScreen().findPreference("video_roll_options"); +// videoRollOptions.setEnabled(prefs.getBoolean(videoRollEnabled.getKey(), false)); +// videoRollEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// videoRollOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference audioEnabled = getPreferenceScreen().findPreference("audio_enabled"); +// Preference audioOptions = getPreferenceScreen().findPreference("audio_options"); +// camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); +// audioEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// audioOptions.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference trupulseEnabled = getPreferenceScreen().findPreference("trupulse_enabled"); +// Preference trupulseOptions = getPreferenceScreen().findPreference("trupulse_options"); +// Preference trupulseDatasource = getPreferenceScreen().findPreference("trupulse_datasource"); +// ListPreference trupulseListPref = (ListPreference) getPreferenceScreen().findPreference("trupulse_device_address"); +// +// trupulseOptions.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); +// trupulseDatasource.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); +// trupulseEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// trupulseOptions.setEnabled((boolean) newValue); +// trupulseDatasource.setEnabled((boolean) newValue); +// trupulseListPref.setEnabled((boolean) newValue); +// return true; +// }); +// +// Preference meshtasticEnabled = getPreferenceScreen().findPreference("meshtastic_enabled"); +// Preference meshtasticOptions = getPreferenceScreen().findPreference("meshtastic_options"); +// +// ListPreference meshDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("meshtastic_device_address"); +// +// +// Preference polarEnabled = getPreferenceScreen().findPreference("polar_enabled"); +// Preference polarOptions = getPreferenceScreen().findPreference("polar_options"); +// ListPreference polarDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("polar_device_address"); +// polarEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// polarOptions.setEnabled((boolean) newValue); +// polarDeviceListPref.setEnabled((boolean) newValue); +// return true; +// }); +// +// +// Preference kestrelEnabled = getPreferenceScreen().findPreference("kestrel_enabled"); +// Preference kestrelOptions = getPreferenceScreen().findPreference("kestrel_options"); +//// bindPreferenceSummaryToValue(findPreference("kestrel_device_name")); +//// bindPreferenceSummaryToValue(findPreference("kestrel_serial")); +// +// ListPreference kestrelDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("kestrel_device_address"); +// +// kestrelEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// kestrelOptions.setEnabled((boolean) newValue); +// kestrelDeviceListPref.setEnabled((boolean) newValue); +// return true; +// }); +// +// +// Preference scanPref = findPreference("scan_ble_devices"); +// +// scanPref.setOnPreferenceClickListener(preference -> { +// startBleScan(); +// return true; +// }); +// +// +// BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); +// if (btAdapter != null && btAdapter.isEnabled()) { +// +//// if (!scannedEntries.isEmpty()) { +//// kestrelDeviceListPref.setEntries(scannedEntries.toArray(new CharSequence[0])); +//// kestrelDeviceListPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); +//// } else { +//// kestrelDeviceListPref.setEnabled(false); +//// kestrelDeviceListPref.setSummary("No BLE devices found"); +//// } +// +// +// if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { +// return; +// } +// Set bondedDevices = btAdapter.getBondedDevices(); +// +// List entries = new ArrayList<>(); +// List entryValues = new ArrayList<>(); +// +// for (BluetoothDevice device : bondedDevices) { +// String name = device.getName(); +// String mac = device.getAddress(); +// entries.add(name != null ? name + " (" + mac + ")" : mac); +// entryValues.add(mac); +// } +// +// if (!entries.isEmpty()) { +// meshDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); +// meshDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); +// +// trupulseListPref.setEntries(entries.toArray(new CharSequence[0])); +// trupulseListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); +// +// polarDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); +// polarDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); // } else { -// kestrelDeviceListPref.setEnabled(false); -// kestrelDeviceListPref.setSummary("No BLE devices found"); +// meshDeviceListPref.setEnabled(false); +// meshDeviceListPref.setSummary("No paired Bluetooth devices found"); +// +// trupulseListPref.setEnabled(false); +// trupulseListPref.setSummary("No paired Bluetooth devices found"); +// +// polarDeviceListPref.setEnabled(false); +// polarDeviceListPref.setSummary("No paired Bluetooth devices found"); // } - - - if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { - return; - } - Set bondedDevices = btAdapter.getBondedDevices(); - - List entries = new ArrayList<>(); - List entryValues = new ArrayList<>(); - - for (BluetoothDevice device : bondedDevices) { - String name = device.getName(); - String mac = device.getAddress(); - entries.add(name != null ? name + " (" + mac + ")" : mac); - entryValues.add(mac); - } - - if (!entries.isEmpty()) { - meshDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); - meshDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); - - trupulseListPref.setEntries(entries.toArray(new CharSequence[0])); - trupulseListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); - - polarDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); - polarDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); - } else { - meshDeviceListPref.setEnabled(false); - meshDeviceListPref.setSummary("No paired Bluetooth devices found"); - - trupulseListPref.setEnabled(false); - trupulseListPref.setSummary("No paired Bluetooth devices found"); - - polarDeviceListPref.setEnabled(false); - polarDeviceListPref.setSummary("No paired Bluetooth devices found"); - } - } - - meshtasticOptions.setEnabled(prefs.getBoolean(meshtasticEnabled.getKey(), false)); - meshtasticEnabled.setOnPreferenceChangeListener((preference, newValue) -> { - meshtasticOptions.setEnabled((boolean) newValue); - return true; - }); - -// Preference bleEnable = getPreferenceScreen().findPreference("ble_enabled"); -// Preference bleLocationMethod = getPreferenceScreen().findPreference("ble_loc_method"); -// Preference bleOptions = getPreferenceScreen().findPreference("ble_options"); -// Preference bleConfigURL = getPreferenceScreen().findPreference("ble_config_url"); -// bleLocationMethod.setEnabled(prefs.getBoolean(bleEnable.getKey(), false)); -// bleOptions.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); -// bleConfigURL.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); -// bleEnable.setOnPreferenceChangeListener(((preference, newValue) -> { -// bleLocationMethod.setEnabled((boolean) newValue); -// bleOptions.setEnabled((boolean) newValue); -// bleConfigURL.setEnabled((boolean) newValue); +// } +// +// meshtasticOptions.setEnabled(prefs.getBoolean(meshtasticEnabled.getKey(), false)); +// meshtasticEnabled.setOnPreferenceChangeListener((preference, newValue) -> { +// meshtasticOptions.setEnabled((boolean) newValue); // return true; -// })); - - // TODO: introduce FLIR and ANGEL sensors - } - - public void startBleScan() { - BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); - if (btAdapter == null || !btAdapter.isEnabled()) - return; - - scannedEntries.clear(); - scannedEntryValues.clear(); - scannedDevices.clear(); - - - Preference scanBlePref = findPreference("scan_ble_devices"); - - BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner(); - - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { - return; - } - } - - ScanCallback scanCallback = new ScanCallback() { - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) - @Override - public void onScanResult(int callbackType, ScanResult result) { - BluetoothDevice device = result.getDevice(); - String name = device.getName(); - String address = device.getAddress(); - - if (name == null && !scannedEntryValues.contains(address)) { - name = "Unnamed Device"; - } - if (!scannedEntryValues.contains(address)) { - scannedEntries.add(name != null ? name + " (" + address + ")" : address); - scannedEntryValues.add(address); - - updateKestrelListPreference(); - } - } - }; - - scanner.startScan(scanCallback); - - new Handler(Looper.getMainLooper()).postDelayed(() -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (ActivityCompat.checkSelfPermission(getContext(), - Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { - return; - } - } - scanner.stopScan(scanCallback); - - if (scanBlePref != null) scanBlePref.setEnabled(true); - - updateKestrelListPreference(); - }, 8000); - - } - - private void updateKestrelListPreference() { - ListPreference kestrelPref = (ListPreference) findPreference("kestrel_device_address"); - - if (kestrelPref == null) return; - - kestrelPref.setEntries(scannedEntries.toArray(new CharSequence[0])); - kestrelPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); - kestrelPref.setEnabled(!scannedEntries.isEmpty()); - - if (scannedEntries.isEmpty()) { - kestrelPref.setSummary("No BLE devices found"); - } else { - kestrelPref.setSummary("Select a device"); - } - } - } - - - /* - * Fragment for video settings - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class VideoPreferenceFragment extends PreferenceFragment { - ArrayList frameRateList = new ArrayList<>(); - ArrayList resList = new ArrayList<>(); - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_video); - - PreferenceScreen videoOptsScreen = getPreferenceScreen(); - - // Create camera selection preference - ArrayList cameras = new ArrayList<>(); - for (int i = 0; i < Camera.getNumberOfCameras(); i++) { - Camera.CameraInfo info = new Camera.CameraInfo(); - Camera.getCameraInfo(i, info); - cameras.add(Integer.toString(i)); - } - ListPreference cameraSelectList = (ListPreference) videoOptsScreen.findPreference("camera_select"); - cameraSelectList.setEntries(cameras.toArray(new String[0])); - cameraSelectList.setEntryValues(cameras.toArray(new String[0])); - bindPreferenceSummaryToValue(cameraSelectList); - videoOptsScreen.addPreference(cameraSelectList); - - bindPreferenceSummaryToValue(findPreference("video_codec")); - // get possible video capture frame rates and sizes - Camera 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); - camera.release(); - - // add list of supported frame rates - ListPreference frameRatePrefList = (ListPreference) videoOptsScreen.findPreference("video_framerate"); - frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); - frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); - bindPreferenceSummaryToValue(findPreference("video_framerate")); - - // add list of configurable presets - ArrayList presetNames = new ArrayList<>(); - ArrayList presetIndexes = new ArrayList<>(); - for (int i = 1; i <= 5; i++) { - PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(videoOptsScreen.getContext()); - prefScreen.setKey("video_res" + i); - String presetName = "Video Preset #" + i; - prefScreen.setTitle(presetName); - presetNames.add(presetName); - presetIndexes.add(String.valueOf(i - 1)); - - ListPreference sizeList = new ListPreference(prefScreen.getContext()); - sizeList.setKey("video_size" + i); - sizeList.setTitle("Frame Size"); - sizeList.setEntries(resList.toArray(new String[0])); - sizeList.setEntryValues(resList.toArray(new String[0])); - bindPreferenceSummaryToValue(sizeList); - prefScreen.addPreference(sizeList); - - EditTextPreference minBitrate = new EditTextPreference(prefScreen.getContext()); - minBitrate.setKey("video_min_bitrate" + i); - minBitrate.setTitle("Min Bitrate (kbits/s)"); - minBitrate.getEditText().setSingleLine(); - minBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - minBitrate.setDefaultValue("3000"); - bindPreferenceSummaryToValue(minBitrate); - prefScreen.addPreference(minBitrate); - - EditTextPreference maxBitrate = new EditTextPreference(prefScreen.getContext()); - maxBitrate.setKey("video_max_bitrate" + i); - maxBitrate.setTitle("Max Bitrate (kbits/s)"); - maxBitrate.getEditText().setSingleLine(); - maxBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - maxBitrate.setDefaultValue("3000"); - bindPreferenceSummaryToValue(maxBitrate); - prefScreen.addPreference(maxBitrate); - - bindPreferenceSummaryToValue(prefScreen); - videoOptsScreen.addPreference(prefScreen); - } - - // add list of selectable presets - ListPreference selectedPresetList = (ListPreference) videoOptsScreen.findPreference("video_preset"); - presetNames.add("Auto select"); - presetIndexes.add("AUTO"); - selectedPresetList.setEntries(presetNames.toArray(new String[0])); - selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); - - // Setup Camera Listener - cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> { - Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue); - updateCameraSettings(Integer.parseInt((String) newValue)); - cameraSelectList.setSummary(newValue.toString()); - return true; - }); - } - - protected void updateCameraSettings(Integer cameraId) { - Camera 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); - camera.release(); - } - } - - - /* - * Fragment for audio settings - */ - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class AudioPreferenceFragment extends PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.pref_audio); - - PreferenceScreen audioOptsScreen = getPreferenceScreen(); - bindPreferenceSummaryToValue(findPreference("audio_codec")); - - // get possible video capture frame rates and sizes - List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); - List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192"); - - // add list of supported sample rates - ListPreference sampleRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_samplerate"); - sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0])); - sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0])); - bindPreferenceSummaryToValue(findPreference("audio_samplerate")); - - // add list of supported bitrates - ListPreference bitRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_bitrate"); - bitRatePrefList.setEntries(bitRateList.toArray(new String[0])); - bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0])); - bindPreferenceSummaryToValue(findPreference("audio_samplerate")); - } - } - - - @TargetApi(Build.VERSION_CODES.HONEYCOMB) - public static class KestrelPreferenceFragment extends PreferenceFragment { - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - addPreferencesFromResource(R.xml.pref_kestrel); - - PreferenceScreen kestrelOptsScreen = getPreferenceScreen(); - - ArrayList presetNames = new ArrayList<>(); - ArrayList presetIndexes = new ArrayList<>(); - - for (int i = 1; i <= 5; i++) { - PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(kestrelOptsScreen.getContext()); - prefScreen.setKey("kestrel_preset" + i); - String presetName = "Gun Profile Preset #" + i; - prefScreen.setTitle(presetName); - presetNames.add(presetName); - presetIndexes.add(String.valueOf(i - 1)); - - addBulletDataFields(prefScreen, i); - addGunFields(prefScreen, i); - addScopeDataFields(prefScreen, i); - - bindPreferenceSummaryToValue(prefScreen); - kestrelOptsScreen.addPreference(prefScreen); - } - - - // add list of selectable presets - ListPreference selectedPresetList = (ListPreference) kestrelOptsScreen.findPreference("kestrel_profile_preset"); - presetNames.add("Auto select"); - presetIndexes.add("AUTO"); - selectedPresetList.setEntries(presetNames.toArray(new String[0])); - selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); - } - - private void addProfileFields(PreferenceScreen preferenceScreen, int index) { -// -// -// -// -// - } - - private void addScopeDataFields(PreferenceScreen prefScreen, int index) { - List unitList = Arrays.asList("inches", "mil", "tmoa", "smoa", "clicks", "cm"); - - ListPreference eUnitList = new ListPreference(prefScreen.getContext()); - eUnitList.setKey("e_unit_" + index); - eUnitList.setTitle("E Units"); - eUnitList.setEntries(unitList.toArray(new String[0])); - eUnitList.setEntryValues(unitList.toArray(new String[0])); - bindPreferenceSummaryToValue(eUnitList); - prefScreen.addPreference(eUnitList); - - ListPreference wUnitList = new ListPreference(prefScreen.getContext()); - wUnitList.setKey("w_unit_" + index); - wUnitList.setTitle("W Units"); - wUnitList.setEntries(unitList.toArray(new String[0])); - wUnitList.setEntryValues(unitList.toArray(new String[0])); - bindPreferenceSummaryToValue(wUnitList); - prefScreen.addPreference(wUnitList); - } - - private void addGunFields(PreferenceScreen prefScreen, int index) { - EditTextPreference muzzleVel = new EditTextPreference(prefScreen.getContext()); - muzzleVel.setKey("muzzle_velocity_" + index); - muzzleVel.setTitle("Muzzle Velocity (fps)"); - muzzleVel.setDialogTitle("Enter the muzzle velocity (fps)"); - muzzleVel.getEditText().setSingleLine(); - muzzleVel.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - muzzleVel.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(muzzleVel); - prefScreen.addPreference(muzzleVel); - - EditTextPreference zeroRange = new EditTextPreference(prefScreen.getContext()); - zeroRange.setKey("zero_range_" + index); - zeroRange.setTitle("Zero Range (m)"); - zeroRange.setDialogTitle("Enter the zero range (m)"); - zeroRange.getEditText().setSingleLine(); - zeroRange.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - zeroRange.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(zeroRange); - prefScreen.addPreference(zeroRange); - - EditTextPreference boreHeight = new EditTextPreference(prefScreen.getContext()); - boreHeight.setKey("bore_height_" + index); - boreHeight.setTitle("Bore Height (in)"); - boreHeight.setDialogTitle("Enter the bore height (in)"); - boreHeight.getEditText().setSingleLine(); - boreHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - boreHeight.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(boreHeight); - prefScreen.addPreference(boreHeight); - - EditTextPreference zeroHeight = new EditTextPreference(prefScreen.getContext()); - zeroHeight.setKey("zero_height_" + index); - zeroHeight.setTitle("Zero Height (in)"); - zeroHeight.setDialogTitle("Enter the zero height (in)"); - zeroHeight.getEditText().setSingleLine(); - zeroHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - zeroHeight.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(zeroHeight); - prefScreen.addPreference(zeroHeight); - - EditTextPreference zeroOffset = new EditTextPreference(prefScreen.getContext()); - zeroOffset.setKey("zero_offset_" + index); - zeroOffset.setTitle("Zero Offset (in)"); - zeroOffset.setDialogTitle("Enter the zero offset (in)"); - zeroOffset.getEditText().setSingleLine(); - zeroOffset.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - zeroOffset.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(zeroOffset); - prefScreen.addPreference(zeroOffset); - - EditTextPreference twistRate = new EditTextPreference(prefScreen.getContext()); - twistRate.setKey("twist_rate_" + index); - twistRate.setTitle("Twist Rate (in)"); - twistRate.setDialogTitle("Enter the twist rate (in)"); - twistRate.getEditText().setSingleLine(); - twistRate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - twistRate.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(twistRate); - prefScreen.addPreference(twistRate); - - List directionlist = Arrays.asList("L", "R"); - - ListPreference twistRateList = new ListPreference(prefScreen.getContext()); - twistRateList.setKey("twist_rate_direction_" + index); - twistRateList.setTitle("Twist Rate Direction"); - twistRateList.setEntries(directionlist.toArray(new String[0])); - twistRateList.setEntryValues(directionlist.toArray(new String[0])); - bindPreferenceSummaryToValue(twistRateList); - prefScreen.addPreference(twistRateList); - } - - - private void addBulletDataFields(PreferenceScreen prefScreen, int index) { - EditTextPreference diameter = new EditTextPreference(prefScreen.getContext()); - diameter.setKey("diameter_" + index); - diameter.setTitle("Diameter (in)"); - diameter.setDialogTitle("Enter the diameter (inches)"); - diameter.getEditText().setSingleLine(); - diameter.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - diameter.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(diameter); - prefScreen.addPreference(diameter); - - EditTextPreference weight = new EditTextPreference(prefScreen.getContext()); - weight.setKey("weight_" + index); - weight.setTitle("Weight (gr)"); - weight.setDialogTitle("Enter the weight (gr)"); - weight.getEditText().setSingleLine(); - weight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - weight.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(weight); - prefScreen.addPreference(weight); - - EditTextPreference ballistic = new EditTextPreference(prefScreen.getContext()); - ballistic.setKey("ballistic_" + index); - ballistic.setTitle("Ballistic Coefficient (G7)"); - ballistic.setDialogTitle("Enter the Ballistic Coefficient (G7)"); - ballistic.getEditText().setSingleLine(); - ballistic.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - ballistic.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(ballistic); - prefScreen.addPreference(ballistic); - - EditTextPreference length = new EditTextPreference(prefScreen.getContext()); - length.setKey("length_" + index); - length.setTitle("Length (in)"); - length.setDialogTitle("Enter the length (in)"); - length.getEditText().setSingleLine(); - length.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); - length.setDefaultValue("0.0"); - bindPreferenceSummaryToValue(length); - prefScreen.addPreference(length); - } - - } - - @Override - protected boolean isValidFragment(String fragmentName) { - return true; - } -} +// }); +// +//// Preference bleEnable = getPreferenceScreen().findPreference("ble_enabled"); +//// Preference bleLocationMethod = getPreferenceScreen().findPreference("ble_loc_method"); +//// Preference bleOptions = getPreferenceScreen().findPreference("ble_options"); +//// Preference bleConfigURL = getPreferenceScreen().findPreference("ble_config_url"); +//// bleLocationMethod.setEnabled(prefs.getBoolean(bleEnable.getKey(), false)); +//// bleOptions.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); +//// bleConfigURL.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); +//// bleEnable.setOnPreferenceChangeListener(((preference, newValue) -> { +//// bleLocationMethod.setEnabled((boolean) newValue); +//// bleOptions.setEnabled((boolean) newValue); +//// bleConfigURL.setEnabled((boolean) newValue); +//// return true; +//// })); +// +// // TODO: introduce FLIR and ANGEL sensors +// } +// +// public void startBleScan() { +// BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); +// if (btAdapter == null || !btAdapter.isEnabled()) +// return; +// +// scannedEntries.clear(); +// scannedEntryValues.clear(); +// scannedDevices.clear(); +// +// +// Preference scanBlePref = findPreference("scan_ble_devices"); +// +// BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner(); +// +// +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +// if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { +// return; +// } +// } +// +// ScanCallback scanCallback = new ScanCallback() { +// @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) +// @Override +// public void onScanResult(int callbackType, ScanResult result) { +// BluetoothDevice device = result.getDevice(); +// String name = device.getName(); +// String address = device.getAddress(); +// +// if (name == null && !scannedEntryValues.contains(address)) { +// name = "Unnamed Device"; +// } +// if (!scannedEntryValues.contains(address)) { +// scannedEntries.add(name != null ? name + " (" + address + ")" : address); +// scannedEntryValues.add(address); +// +// updateKestrelListPreference(); +// } +// } +// }; +// +// scanner.startScan(scanCallback); +// +// new Handler(Looper.getMainLooper()).postDelayed(() -> { +// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { +// if (ActivityCompat.checkSelfPermission(getContext(), +// Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { +// return; +// } +// } +// scanner.stopScan(scanCallback); +// +// if (scanBlePref != null) scanBlePref.setEnabled(true); +// +// updateKestrelListPreference(); +// }, 8000); +// +// } +// +// private void updateKestrelListPreference() { +// ListPreference kestrelPref = (ListPreference) findPreference("kestrel_device_address"); +// +// if (kestrelPref == null) return; +// +// kestrelPref.setEntries(scannedEntries.toArray(new CharSequence[0])); +// kestrelPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); +// kestrelPref.setEnabled(!scannedEntries.isEmpty()); +// +// if (scannedEntries.isEmpty()) { +// kestrelPref.setSummary("No BLE devices found"); +// } else { +// kestrelPref.setSummary("Select a device"); +// } +// } +// } +// +// +// /* +// * Fragment for video settings +// */ +// @TargetApi(Build.VERSION_CODES.HONEYCOMB) +// public static class VideoPreferenceFragment extends PreferenceFragment { +// ArrayList frameRateList = new ArrayList<>(); +// ArrayList resList = new ArrayList<>(); +// +// @Override +// public void onCreate(Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// addPreferencesFromResource(R.xml.pref_video); +// +// PreferenceScreen videoOptsScreen = getPreferenceScreen(); +// +// // Create camera selection preference +// ArrayList cameras = new ArrayList<>(); +// for (int i = 0; i < Camera.getNumberOfCameras(); i++) { +// Camera.CameraInfo info = new Camera.CameraInfo(); +// Camera.getCameraInfo(i, info); +// cameras.add(Integer.toString(i)); +// } +// ListPreference cameraSelectList = (ListPreference) videoOptsScreen.findPreference("camera_select"); +// cameraSelectList.setEntries(cameras.toArray(new String[0])); +// cameraSelectList.setEntryValues(cameras.toArray(new String[0])); +// bindPreferenceSummaryToValue(cameraSelectList); +// videoOptsScreen.addPreference(cameraSelectList); +// +// bindPreferenceSummaryToValue(findPreference("video_codec")); +// // get possible video capture frame rates and sizes +// Camera 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); +// camera.release(); +// +// // add list of supported frame rates +// ListPreference frameRatePrefList = (ListPreference) videoOptsScreen.findPreference("video_framerate"); +// frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); +// frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); +// bindPreferenceSummaryToValue(findPreference("video_framerate")); +// +// // add list of configurable presets +// ArrayList presetNames = new ArrayList<>(); +// ArrayList presetIndexes = new ArrayList<>(); +// for (int i = 1; i <= 5; i++) { +// PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(videoOptsScreen.getContext()); +// prefScreen.setKey("video_res" + i); +// String presetName = "Video Preset #" + i; +// prefScreen.setTitle(presetName); +// presetNames.add(presetName); +// presetIndexes.add(String.valueOf(i - 1)); +// +// ListPreference sizeList = new ListPreference(prefScreen.getContext()); +// sizeList.setKey("video_size" + i); +// sizeList.setTitle("Frame Size"); +// sizeList.setEntries(resList.toArray(new String[0])); +// sizeList.setEntryValues(resList.toArray(new String[0])); +// bindPreferenceSummaryToValue(sizeList); +// prefScreen.addPreference(sizeList); +// +// EditTextPreference minBitrate = new EditTextPreference(prefScreen.getContext()); +// minBitrate.setKey("video_min_bitrate" + i); +// minBitrate.setTitle("Min Bitrate (kbits/s)"); +// minBitrate.getEditText().setSingleLine(); +// minBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// minBitrate.setDefaultValue("3000"); +// bindPreferenceSummaryToValue(minBitrate); +// prefScreen.addPreference(minBitrate); +// +// EditTextPreference maxBitrate = new EditTextPreference(prefScreen.getContext()); +// maxBitrate.setKey("video_max_bitrate" + i); +// maxBitrate.setTitle("Max Bitrate (kbits/s)"); +// maxBitrate.getEditText().setSingleLine(); +// maxBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// maxBitrate.setDefaultValue("3000"); +// bindPreferenceSummaryToValue(maxBitrate); +// prefScreen.addPreference(maxBitrate); +// +// bindPreferenceSummaryToValue(prefScreen); +// videoOptsScreen.addPreference(prefScreen); +// } +// +// // add list of selectable presets +// ListPreference selectedPresetList = (ListPreference) videoOptsScreen.findPreference("video_preset"); +// presetNames.add("Auto select"); +// presetIndexes.add("AUTO"); +// selectedPresetList.setEntries(presetNames.toArray(new String[0])); +// selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); +// +// // Setup Camera Listener +// cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> { +// Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue); +// updateCameraSettings(Integer.parseInt((String) newValue)); +// cameraSelectList.setSummary(newValue.toString()); +// return true; +// }); +// } +// +// protected void updateCameraSettings(Integer cameraId) { +// Camera 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); +// camera.release(); +// } +// } +// +// +// /* +// * Fragment for audio settings +// */ +// @TargetApi(Build.VERSION_CODES.HONEYCOMB) +// public static class AudioPreferenceFragment extends PreferenceFragment { +// @Override +// public void onCreate(Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// addPreferencesFromResource(R.xml.pref_audio); +// +// PreferenceScreen audioOptsScreen = getPreferenceScreen(); +// bindPreferenceSummaryToValue(findPreference("audio_codec")); +// +// // get possible video capture frame rates and sizes +// List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); +// List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192"); +// +// // add list of supported sample rates +// ListPreference sampleRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_samplerate"); +// sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0])); +// sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0])); +// bindPreferenceSummaryToValue(findPreference("audio_samplerate")); +// +// // add list of supported bitrates +// ListPreference bitRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_bitrate"); +// bitRatePrefList.setEntries(bitRateList.toArray(new String[0])); +// bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0])); +// bindPreferenceSummaryToValue(findPreference("audio_samplerate")); +// } +// } +// +// +// @TargetApi(Build.VERSION_CODES.HONEYCOMB) +// public static class KestrelPreferenceFragment extends PreferenceFragment { +// @Override +// public void onCreate(Bundle savedInstanceState) { +// super.onCreate(savedInstanceState); +// +// addPreferencesFromResource(R.xml.pref_kestrel); +// +// PreferenceScreen kestrelOptsScreen = getPreferenceScreen(); +// +// ArrayList presetNames = new ArrayList<>(); +// ArrayList presetIndexes = new ArrayList<>(); +// +// for (int i = 1; i <= 5; i++) { +// PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(kestrelOptsScreen.getContext()); +// prefScreen.setKey("kestrel_preset" + i); +// String presetName = "Gun Profile Preset #" + i; +// prefScreen.setTitle(presetName); +// presetNames.add(presetName); +// presetIndexes.add(String.valueOf(i - 1)); +// +// addBulletDataFields(prefScreen, i); +// addGunFields(prefScreen, i); +// addScopeDataFields(prefScreen, i); +// +// bindPreferenceSummaryToValue(prefScreen); +// kestrelOptsScreen.addPreference(prefScreen); +// } +// +// +// // add list of selectable presets +// ListPreference selectedPresetList = (ListPreference) kestrelOptsScreen.findPreference("kestrel_profile_preset"); +// presetNames.add("Auto select"); +// presetIndexes.add("AUTO"); +// selectedPresetList.setEntries(presetNames.toArray(new String[0])); +// selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); +// } +// +// private void addProfileFields(PreferenceScreen preferenceScreen, int index) { +//// +//// +//// +//// +//// +// } +// +// private void addScopeDataFields(PreferenceScreen prefScreen, int index) { +// List unitList = Arrays.asList("inches", "mil", "tmoa", "smoa", "clicks", "cm"); +// +// ListPreference eUnitList = new ListPreference(prefScreen.getContext()); +// eUnitList.setKey("e_unit_" + index); +// eUnitList.setTitle("E Units"); +// eUnitList.setEntries(unitList.toArray(new String[0])); +// eUnitList.setEntryValues(unitList.toArray(new String[0])); +// bindPreferenceSummaryToValue(eUnitList); +// prefScreen.addPreference(eUnitList); +// +// ListPreference wUnitList = new ListPreference(prefScreen.getContext()); +// wUnitList.setKey("w_unit_" + index); +// wUnitList.setTitle("W Units"); +// wUnitList.setEntries(unitList.toArray(new String[0])); +// wUnitList.setEntryValues(unitList.toArray(new String[0])); +// bindPreferenceSummaryToValue(wUnitList); +// prefScreen.addPreference(wUnitList); +// } +// +// private void addGunFields(PreferenceScreen prefScreen, int index) { +// EditTextPreference muzzleVel = new EditTextPreference(prefScreen.getContext()); +// muzzleVel.setKey("muzzle_velocity_" + index); +// muzzleVel.setTitle("Muzzle Velocity (fps)"); +// muzzleVel.setDialogTitle("Enter the muzzle velocity (fps)"); +// muzzleVel.getEditText().setSingleLine(); +// muzzleVel.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// muzzleVel.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(muzzleVel); +// prefScreen.addPreference(muzzleVel); +// +// EditTextPreference zeroRange = new EditTextPreference(prefScreen.getContext()); +// zeroRange.setKey("zero_range_" + index); +// zeroRange.setTitle("Zero Range (m)"); +// zeroRange.setDialogTitle("Enter the zero range (m)"); +// zeroRange.getEditText().setSingleLine(); +// zeroRange.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// zeroRange.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(zeroRange); +// prefScreen.addPreference(zeroRange); +// +// EditTextPreference boreHeight = new EditTextPreference(prefScreen.getContext()); +// boreHeight.setKey("bore_height_" + index); +// boreHeight.setTitle("Bore Height (in)"); +// boreHeight.setDialogTitle("Enter the bore height (in)"); +// boreHeight.getEditText().setSingleLine(); +// boreHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// boreHeight.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(boreHeight); +// prefScreen.addPreference(boreHeight); +// +// EditTextPreference zeroHeight = new EditTextPreference(prefScreen.getContext()); +// zeroHeight.setKey("zero_height_" + index); +// zeroHeight.setTitle("Zero Height (in)"); +// zeroHeight.setDialogTitle("Enter the zero height (in)"); +// zeroHeight.getEditText().setSingleLine(); +// zeroHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// zeroHeight.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(zeroHeight); +// prefScreen.addPreference(zeroHeight); +// +// EditTextPreference zeroOffset = new EditTextPreference(prefScreen.getContext()); +// zeroOffset.setKey("zero_offset_" + index); +// zeroOffset.setTitle("Zero Offset (in)"); +// zeroOffset.setDialogTitle("Enter the zero offset (in)"); +// zeroOffset.getEditText().setSingleLine(); +// zeroOffset.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// zeroOffset.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(zeroOffset); +// prefScreen.addPreference(zeroOffset); +// +// EditTextPreference twistRate = new EditTextPreference(prefScreen.getContext()); +// twistRate.setKey("twist_rate_" + index); +// twistRate.setTitle("Twist Rate (in)"); +// twistRate.setDialogTitle("Enter the twist rate (in)"); +// twistRate.getEditText().setSingleLine(); +// twistRate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// twistRate.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(twistRate); +// prefScreen.addPreference(twistRate); +// +// List directionlist = Arrays.asList("L", "R"); +// +// ListPreference twistRateList = new ListPreference(prefScreen.getContext()); +// twistRateList.setKey("twist_rate_direction_" + index); +// twistRateList.setTitle("Twist Rate Direction"); +// twistRateList.setEntries(directionlist.toArray(new String[0])); +// twistRateList.setEntryValues(directionlist.toArray(new String[0])); +// bindPreferenceSummaryToValue(twistRateList); +// prefScreen.addPreference(twistRateList); +// } +// +// +// private void addBulletDataFields(PreferenceScreen prefScreen, int index) { +// EditTextPreference diameter = new EditTextPreference(prefScreen.getContext()); +// diameter.setKey("diameter_" + index); +// diameter.setTitle("Diameter (in)"); +// diameter.setDialogTitle("Enter the diameter (inches)"); +// diameter.getEditText().setSingleLine(); +// diameter.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// diameter.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(diameter); +// prefScreen.addPreference(diameter); +// +// EditTextPreference weight = new EditTextPreference(prefScreen.getContext()); +// weight.setKey("weight_" + index); +// weight.setTitle("Weight (gr)"); +// weight.setDialogTitle("Enter the weight (gr)"); +// weight.getEditText().setSingleLine(); +// weight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// weight.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(weight); +// prefScreen.addPreference(weight); +// +// EditTextPreference ballistic = new EditTextPreference(prefScreen.getContext()); +// ballistic.setKey("ballistic_" + index); +// ballistic.setTitle("Ballistic Coefficient (G7)"); +// ballistic.setDialogTitle("Enter the Ballistic Coefficient (G7)"); +// ballistic.getEditText().setSingleLine(); +// ballistic.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// ballistic.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(ballistic); +// prefScreen.addPreference(ballistic); +// +// EditTextPreference length = new EditTextPreference(prefScreen.getContext()); +// length.setKey("length_" + index); +// length.setTitle("Length (in)"); +// length.setDialogTitle("Enter the length (in)"); +// length.getEditText().setSingleLine(); +// length.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); +// length.setDefaultValue("0.0"); +// bindPreferenceSummaryToValue(length); +// prefScreen.addPreference(length); +// } +// +// } +// +// @Override +// protected boolean isValidFragment(String fragmentName) { +// return true; +// } +//} 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 9846a0d1..3d47b583 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java +++ b/sensorhub-android-service/src/org/sensorhub/android/comm/BluetoothManager.java @@ -107,30 +107,36 @@ public void onReceive(Context context, Intent intent) /** - * Returns the first paired device whose name matches the given pattern - * @param macAddress regular expression to match device names + * Returns the first paired device whose address or name matches the given identifier. + * Tries MAC address match first, then falls back to name matching (startsWith, case-insensitive). + * @param deviceId MAC address or device name to match * @return first matching device - * @throws IOException if a paired device with a matching name cannot be found + * @throws IOException if a paired device with a matching address or name cannot be found */ - public BluetoothDevice findDevice(String macAddress) throws IOException + public BluetoothDevice findDevice(String deviceId) throws IOException { BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); if(btAdapter == null || !btAdapter.isEnabled()) throw new IOException("Bluetooth is not available or not enabled"); + // match by MAC address for (BluetoothDevice dev: btAdapter.getBondedDevices()) { - if (dev.getAddress() != null && dev.getAddress().startsWith(macAddress)) { + if (dev.getAddress() != null && dev.getAddress().equalsIgnoreCase(deviceId)) { return dev; } -// if(dev.getName() != null && dev.getName().startsWith(deviceNameRegex)){ -// return dev; -// } -// if (dev.getName().matches(deviceNameRegex)) -// return dev; } - - throw new IOException("Cannot find device " + macAddress); + + // match by device name (case-insensitive startsWith) + String lowerDeviceId = deviceId.toLowerCase(); + for (BluetoothDevice dev: btAdapter.getBondedDevices()) + { + if (dev.getName() != null && dev.getName().toLowerCase().startsWith(lowerDeviceId)) { + return dev; + } + } + + throw new IOException("Cannot find device " + deviceId); } diff --git a/sensorhub-android-service/src/org/sensorhub/android/comm/ble/BleNetwork.java b/sensorhub-android-service/src/org/sensorhub/android/comm/ble/BleNetwork.java index 4e495896..3cc7b166 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/comm/ble/BleNetwork.java +++ b/sensorhub-android-service/src/org/sensorhub/android/comm/ble/BleNetwork.java @@ -41,6 +41,11 @@ import android.bluetooth.le.ScanSettings; import android.content.Context; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + public class BleNetwork extends AbstractModule implements IBleNetwork @@ -253,12 +258,112 @@ public boolean startPairing(String address) @Override + @SuppressLint("MissingPermission") public void connectGatt(String address, GattCallback callback) { - BluetoothDevice btDevice = aBleAdapter.getRemoteDevice(address); + String resolvedAddress = resolveDeviceAddress(address); + BluetoothDevice btDevice = aBleAdapter.getRemoteDevice(resolvedAddress); GattClientImpl client = new GattClientImpl(aContext, btDevice, callback); client.connect(); - log.info("Connecting to BT device " + address + "..."); + log.info("Connecting to BT device " + resolvedAddress + " (input: " + address + ")..."); + } + + private static final long BLE_NAME_SCAN_TIMEOUT_MS = 15000; + + /** + * Resolves a device identifier to a MAC address. + * If the input is already a valid MAC address, returns it directly. + * Otherwise, searches bonded devices by name first, then falls back to + * a short BLE scan filtered by device name (for unbonded BLE devices like Kestrel). + */ + @SuppressLint("MissingPermission") + private String resolveDeviceAddress(String deviceId) + { + // If it looks like a MAC address, use it directly + if (deviceId != null && deviceId.matches("^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$")) + return deviceId; + + String lowerInput = deviceId != null ? deviceId.toLowerCase() : ""; + + // First: search bonded devices by name + if (aBleAdapter != null) { + for (BluetoothDevice dev : aBleAdapter.getBondedDevices()) { + String name = dev.getName(); + if (name != null && name.toLowerCase().startsWith(lowerInput)) { + log.info("Resolved device name '{}' to bonded MAC {}", deviceId, dev.getAddress()); + return dev.getAddress(); + } + } + } + + // Second: targeted BLE scan by name (for unbonded BLE devices) + log.info("Device '{}' not bonded, starting targeted BLE scan...", deviceId); + String scannedAddress = scanForDeviceByName(deviceId); + if (scannedAddress != null) { + log.info("BLE scan resolved '{}' to MAC {}", deviceId, scannedAddress); + return scannedAddress; + } + + log.warn("Could not resolve device identifier '{}' to a MAC address, using as-is", deviceId); + return deviceId; + } + + /** + * Performs a short BLE scan filtered by device name. + * Returns the MAC address of the first matching device, or null if not found within the timeout. + */ + @SuppressLint("MissingPermission") + private String scanForDeviceByName(String deviceName) + { + if (aBleAdapter == null || !aBleAdapter.isEnabled()) + return null; + + BluetoothLeScanner scanner = aBleAdapter.getBluetoothLeScanner(); + if (scanner == null) + return null; + + String lowerName = deviceName != null ? deviceName.toLowerCase() : ""; + AtomicReference foundAddress = new AtomicReference<>(null); + CountDownLatch latch = new CountDownLatch(1); + + ScanCallback callback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, ScanResult result) { + if (result == null || result.getDevice() == null) + return; + + BluetoothDevice device = result.getDevice(); + String name = device.getName(); + + if (name != null && name.toLowerCase().startsWith(lowerName)) { + foundAddress.set(device.getAddress()); + log.info("BLE scan found '{}' at {}", name, device.getAddress()); + latch.countDown(); + } + } + + @Override + public void onScanFailed(int errorCode) { + log.error("BLE scan failed with error code {}", errorCode); + latch.countDown(); + } + }; + + ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(0) + .build(); + + scanner.startScan(Collections.emptyList(), settings, callback); + + try { + latch.await(BLE_NAME_SCAN_TIMEOUT_MS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + scanner.stopScan(callback); + return foundAddress.get(); } public void setContext(Context context) { From dd0ae0020a7ae3666e401d0067181fd49dd9870a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 7 Apr 2026 12:56:04 -0500 Subject: [PATCH 03/26] add wardriving capabilities and controller driver added wardriving with ability to collect scanned ble and wifi networks. added and tested controller driver from old dev project. updated the copyright. --- build.gradle | 2 +- sensorhub-android-app/AndroidManifest.xml | 1 + sensorhub-android-app/build.gradle | 2 + .../res/xml/pref_sensors.xml | 34 + .../org/sensorhub/android/MainActivity.java | 88 +- .../sensorhub/android/SensorsFragment.java | 9 +- .../android/UserSettingsActivity.java | 848 ------------------ .../AndroidManifest.xml | 37 + sensorhub-android-controller/README.md | 16 + sensorhub-android-controller/build.gradle | 44 + .../sensor/controller/ControllerConfig.java | 52 ++ .../sensor/controller/ControllerDriver.java | 264 ++++++ .../sensor/controller/ControllerOutput.java | 201 +++++ .../impl/sensor/controller/Descriptor.java | 75 ++ .../org.sensorhub.api.module.IModuleProvider | 1 + .../src/test/java/empty | 0 .../src/test/resources/empty | 0 .../impl/sensor/polar/BatteryOutput.java | 3 +- .../impl/sensor/polar/HeartRateOutput.java | 3 +- .../sensorhub/impl/sensor/polar/Polar.java | 4 +- .../impl/sensor/polar/PolarConfig.java | 23 +- .../impl/sensor/polar/PolarDescriptor.java | 22 +- .../AndroidManifest.xml | 21 + sensorhub-android-wardriving/README.md | 27 + sensorhub-android-wardriving/build.gradle | 47 + .../impl/sensor/wardriving/BLEOutput.java | 116 +++ .../impl/sensor/wardriving/Descriptor.java | 76 ++ .../impl/sensor/wardriving/Wardriving.java | 351 ++++++++ .../sensor/wardriving/WardrivingConfig.java | 52 ++ .../impl/sensor/wardriving/WifiOutput.java | 132 +++ .../org.sensorhub.api.module.IModuleProvider | 1 + .../src/test/java/empty | 0 .../src/test/resources/empty | 0 33 files changed, 1661 insertions(+), 891 deletions(-) delete mode 100644 sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java create mode 100644 sensorhub-android-controller/AndroidManifest.xml create mode 100644 sensorhub-android-controller/README.md create mode 100644 sensorhub-android-controller/build.gradle create mode 100644 sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerConfig.java create mode 100644 sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerDriver.java create mode 100644 sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerOutput.java create mode 100644 sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/Descriptor.java create mode 100644 sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider create mode 100644 sensorhub-android-controller/src/test/java/empty create mode 100644 sensorhub-android-controller/src/test/resources/empty create mode 100644 sensorhub-android-wardriving/AndroidManifest.xml create mode 100644 sensorhub-android-wardriving/README.md create mode 100644 sensorhub-android-wardriving/build.gradle create mode 100644 sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java create mode 100644 sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java create mode 100644 sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java create mode 100644 sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java create mode 100644 sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java create mode 100644 sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider create mode 100644 sensorhub-android-wardriving/src/test/java/empty create mode 100644 sensorhub-android-wardriving/src/test/resources/empty diff --git a/build.gradle b/build.gradle index f632da5f..4731a74b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ ext.compileSdkVersion = 33 ext.minSdkVersion = 34 ext.targetSdkVersion = 30 ext.buildToolsVersion = "30.0.2" -version = '3.1.2' +version = '4.0.0' buildscript { repositories { diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index f90eca25..5b631672 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -32,6 +32,7 @@ + + + + + + + + + = Build.VERSION_CODES.TIRAMISU) { + if (checkSelfPermission(Manifest.permission.NEARBY_WIFI_DEVICES) == PackageManager.PERMISSION_DENIED) + permissions.add(Manifest.permission.NEARBY_WIFI_DEVICES); + } if (checkSelfPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS) == PackageManager.PERMISSION_DENIED) permissions.add(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_DENIED) diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index 1818f218..181b1f98 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -53,6 +53,9 @@ public class SensorsFragment extends PreferenceFragmentCompat { {"angel_enabled", "angel_address", "angel_options"}, {"flirone_enabled", "flir_options"}, {"ste_radpager_enabled","ste_radpager_options"}, + {"wardriving_enabled", "wardriving_options"}, + {"controller_enabled", "controller_options"}, + }; /** Keys of Preferences that use the Bluetooth device picker dialog */ @@ -100,8 +103,6 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S setupBluetoothDevicePickers(); } - // ==================== Bluetooth Device Picker ==================== - private void setupBluetoothDevicePickers() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); @@ -200,7 +201,6 @@ private void saveDeviceAddress(String prefKey, String address, String displayTex } } - // ==================== Video Preferences ==================== private void setupVideoPreferences() { // Camera selection @@ -282,7 +282,6 @@ private void updateCameraSettings(int cameraId) { } } - // ==================== Audio Preferences ==================== private void setupAudioPreferences() { List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); @@ -301,8 +300,6 @@ private void setupAudioPreferences() { } } - // ==================== Helpers ==================== - private BluetoothAdapter getBluetoothAdapter() { BluetoothManager btManager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE); return btManager != null ? btManager.getAdapter() : null; diff --git a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java deleted file mode 100644 index a8724f24..00000000 --- a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java +++ /dev/null @@ -1,848 +0,0 @@ -///***************************** BEGIN LICENSE BLOCK *************************** -// -// The contents of this file are subject to the Mozilla Public License, v. 2.0. -// If a copy of the MPL was not distributed with this file, You can obtain one -// at http://mozilla.org/MPL/2.0/. -// -// Software distributed under the License is distributed on an "AS IS" basis, -// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -// for the specific language governing rights and limitations under the License. -// -// Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. -// ******************************* END LICENSE BLOCK ***************************/ -// -//package org.sensorhub.android; -// -//import android.Manifest; -//import android.annotation.TargetApi; -//import android.app.AlertDialog; -//import android.bluetooth.BluetoothAdapter; -//import android.bluetooth.BluetoothDevice; -//import android.bluetooth.le.BluetoothLeScanner; -//import android.bluetooth.le.ScanCallback; -//import android.bluetooth.le.ScanResult; -//import android.content.DialogInterface; -//import android.content.SharedPreferences; -//import android.content.pm.PackageManager; -//import android.hardware.Camera; -//import android.net.wifi.WifiManager; -//import android.os.Build; -//import android.os.Bundle; -//import android.os.Handler; -//import android.os.Looper; -//import android.preference.EditTextPreference; -//import android.preference.ListPreference; -//import android.preference.Preference; -//import android.preference.PreferenceActivity; -//import android.preference.PreferenceFragment; -//import android.preference.PreferenceManager; -//import android.preference.PreferenceScreen; -//import android.text.InputType; -//import android.util.Log; -//import android.widget.BaseAdapter; -// -//import androidx.annotation.RequiresPermission; -//import androidx.core.app.ActivityCompat; -// -//import org.slf4j.Logger; -//import org.slf4j.LoggerFactory; -// -//import java.math.BigInteger; -//import java.net.InetAddress; -//import java.net.URL; -//import java.net.UnknownHostException; -//import java.nio.ByteOrder; -//import java.util.ArrayList; -//import java.util.Arrays; -//import java.util.HashSet; -//import java.util.List; -//import java.util.Set; -// -// -//public class UserSettingsActivity extends PreferenceActivity { -// -// private static final Logger log = LoggerFactory.getLogger(UserSettingsActivity.class); -// -// @Override -// public void onBuildHeaders(List
target) { -// loadHeadersFromResource(R.xml.pref_headers, target); -// } -// -// -// /* -// * A preference value change listener that updates the preference's summary to reflect its new value. -// */ -// private static final Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { -// @Override -// public boolean onPreferenceChange(Preference preference, Object value) { -// String stringValue = value.toString(); -// -// if (preference instanceof ListPreference listPreference) { -// int index = listPreference.findIndexOfValue(stringValue); -// preference.setSummary(index >= 0 ? listPreference.getEntries()[index] : null); -// } else if (preference.getKey().startsWith("video_res")) { -// PreferenceScreen presetSettings = (PreferenceScreen) preference; -// String frameSize = ((ListPreference) presetSettings.getPreference(0)).getValue(); -// String minBitrate = ((EditTextPreference) presetSettings.getPreference(1)).getText(); -// String maxBitrate = ((EditTextPreference) presetSettings.getPreference(2)).getText(); -// presetSettings.setSummary(frameSize + " @ " + minBitrate + "-" + maxBitrate + " kbits/s"); -// ((BaseAdapter) presetSettings.getRootAdapter()).notifyDataSetChanged(); -// } else { -// preference.setSummary(stringValue); -// } -// -// // detect errors -// if (preference.getKey().equals("sos_uri")) { -// try { -// URL url = new URL(value.toString()); -// if (!url.getProtocol().equals("http") && !url.getProtocol().equals("https")) -// throw new Exception("SOS URL must be HTTP or HTTPS"); -// } catch (Exception e) { -// AlertDialog.Builder dlgAlert = new AlertDialog.Builder(preference.getContext()); -// dlgAlert.setMessage("Invalid SOS URL"); -// dlgAlert.setTitle(e.getMessage()); -// dlgAlert.setPositiveButton("OK", null); -// dlgAlert.setCancelable(true); -// dlgAlert.create().show(); -// } -// } -// -// return true; -// } -// }; -// -// -// /* -// * Binds a preference's summary to its value. More specifically, when the -// * preference's value is changed, its summary (line of text below the -// * preference title) is updated to reflect the value. The summary is also -// * immediately updated upon calling this method. The exact display format is -// * dependent on the type of preference. -// * -// * @see #sBindPreferenceSummaryToValueListener -// */ -// private static void bindPreferenceSummaryToValue(Preference preference) { -// // Set the listener to watch for value changes. -// preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); -// -// // for preference screens, call listener when screen is closed -// if (preference instanceof PreferenceScreen) { -// preference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { -// @Override -// public boolean onPreferenceClick(Preference preference) { -// ((PreferenceScreen) preference).getDialog().setOnCancelListener(new DialogInterface.OnCancelListener() { -// @Override -// public void onCancel(DialogInterface dialog) { -// sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, ""); -// } -// }); -// return true; -// } -// }); -// } -// -// // Trigger the listener immediately with the preference's current value. -// sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, PreferenceManager.getDefaultSharedPreferences(preference.getContext()).getString(preference.getKey(), "")); -// } -// -// -// /* -// * Fragment for general preferences -// */ -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class GeneralPreferenceFragment extends PreferenceFragment { -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_general); -// bindPreferenceSummaryToValue(findPreference("device_name")); -// bindPreferenceSummaryToValue(findPreference("ip_address")); -// bindPreferenceSummaryToValue(findPreference("port")); -// bindPreferenceSummaryToValue(findPreference("endpoint_path")); -// bindPreferenceSummaryToValue(findPreference("username")); -// bindPreferenceSummaryToValue(findPreference("password")); -// -// -// WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); -// int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); -// -// // Convert little-endian to big-endianif needed -// 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 = "Unable to get IP Address"; -// } -// -// Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); -// ipAddressLabel.setSummary(ipAddressString); -// -// -// SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); -// -// Preference oAuthEnabled = getPreferenceScreen().findPreference("o_auth_enabled"); -// Preference tokenEndpoint = getPreferenceScreen().findPreference("token_endpoint"); -// Preference clientID = getPreferenceScreen().findPreference("client_id"); -// Preference clientSecret = getPreferenceScreen().findPreference("client_secret"); -// -// tokenEndpoint.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); -// clientID.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); -// clientSecret.setEnabled(prefs.getBoolean(oAuthEnabled.getKey(), false)); -// -// oAuthEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// tokenEndpoint.setEnabled((boolean) newValue); -// clientID.setEnabled((boolean) newValue); -// clientSecret.setEnabled((boolean) newValue); -// return true; -// }); -// } -// } -// -// -// /* -// * Fragment for sensor preferences -// */ -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class SensorPreferenceFragment extends PreferenceFragment { -// -// List scannedEntries = new ArrayList<>(); -// List scannedEntryValues = new ArrayList<>(); -// Set scannedDevices = new HashSet<>(); -// -// -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_sensors); -// bindPreferenceSummaryToValue(findPreference("uid_extension")); -// bindPreferenceSummaryToValue(findPreference("angel_address")); -// -// SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); -// -// Preference accelerometerEnable = getPreferenceScreen().findPreference("accel_enabled"); -// Preference accelerometerOptions = getPreferenceScreen().findPreference("accel_options"); -// accelerometerOptions.setEnabled(prefs.getBoolean(accelerometerEnable.getKey(), false)); -// accelerometerEnable.setOnPreferenceChangeListener((preference, newValue) -> { -// accelerometerOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference gyroEnabled = getPreferenceScreen().findPreference("gyro_enabled"); -// Preference gyroOptions = getPreferenceScreen().findPreference("gyro_options"); -// gyroOptions.setEnabled(prefs.getBoolean(gyroEnabled.getKey(), false)); -// gyroEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// gyroOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference magEnabled = getPreferenceScreen().findPreference("mag_enabled"); -// Preference magOptions = getPreferenceScreen().findPreference("mag_options"); -// magOptions.setEnabled(prefs.getBoolean(magEnabled.getKey(), false)); -// magEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// magOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference orientQuatEnabled = getPreferenceScreen().findPreference("orient_quat_enabled"); -// Preference orientQuatOptions = getPreferenceScreen().findPreference("orient_quat_options"); -// orientQuatOptions.setEnabled(prefs.getBoolean(orientQuatEnabled.getKey(), false)); -// orientQuatEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// orientQuatOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference orientEulerEnabled = getPreferenceScreen().findPreference("orient_euler_enabled"); -// Preference orientEulerOptions = getPreferenceScreen().findPreference("orient_euler_options"); -// orientEulerOptions.setEnabled(prefs.getBoolean(orientEulerEnabled.getKey(), false)); -// orientEulerEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// orientEulerOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference gpsEnabled = getPreferenceScreen().findPreference("gps_enabled"); -// Preference gpsOptions = getPreferenceScreen().findPreference("gps_options"); -// gpsOptions.setEnabled(prefs.getBoolean(gpsEnabled.getKey(), false)); -// gpsEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// gpsOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference netlocEnabled = getPreferenceScreen().findPreference("netloc_enabled"); -// Preference netlocOptions = getPreferenceScreen().findPreference("netloc_options"); -// netlocOptions.setEnabled(prefs.getBoolean(netlocEnabled.getKey(), false)); -// netlocEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// netlocOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference camEnabled = getPreferenceScreen().findPreference("cam_enabled"); -// Preference camOptions = getPreferenceScreen().findPreference("cam_options"); -// camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); -// camEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// camOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference videoRollEnabled = getPreferenceScreen().findPreference("video_roll_enabled"); -// Preference videoRollOptions = getPreferenceScreen().findPreference("video_roll_options"); -// videoRollOptions.setEnabled(prefs.getBoolean(videoRollEnabled.getKey(), false)); -// videoRollEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// videoRollOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference audioEnabled = getPreferenceScreen().findPreference("audio_enabled"); -// Preference audioOptions = getPreferenceScreen().findPreference("audio_options"); -// camOptions.setEnabled(prefs.getBoolean(camEnabled.getKey(), false)); -// audioEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// audioOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference trupulseEnabled = getPreferenceScreen().findPreference("trupulse_enabled"); -// Preference trupulseOptions = getPreferenceScreen().findPreference("trupulse_options"); -// Preference trupulseDatasource = getPreferenceScreen().findPreference("trupulse_datasource"); -// ListPreference trupulseListPref = (ListPreference) getPreferenceScreen().findPreference("trupulse_device_address"); -// -// trupulseOptions.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); -// trupulseDatasource.setEnabled(prefs.getBoolean(trupulseEnabled.getKey(), false)); -// trupulseEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// trupulseOptions.setEnabled((boolean) newValue); -// trupulseDatasource.setEnabled((boolean) newValue); -// trupulseListPref.setEnabled((boolean) newValue); -// return true; -// }); -// -// Preference meshtasticEnabled = getPreferenceScreen().findPreference("meshtastic_enabled"); -// Preference meshtasticOptions = getPreferenceScreen().findPreference("meshtastic_options"); -// -// ListPreference meshDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("meshtastic_device_address"); -// -// -// Preference polarEnabled = getPreferenceScreen().findPreference("polar_enabled"); -// Preference polarOptions = getPreferenceScreen().findPreference("polar_options"); -// ListPreference polarDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("polar_device_address"); -// polarEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// polarOptions.setEnabled((boolean) newValue); -// polarDeviceListPref.setEnabled((boolean) newValue); -// return true; -// }); -// -// -// Preference kestrelEnabled = getPreferenceScreen().findPreference("kestrel_enabled"); -// Preference kestrelOptions = getPreferenceScreen().findPreference("kestrel_options"); -//// bindPreferenceSummaryToValue(findPreference("kestrel_device_name")); -//// bindPreferenceSummaryToValue(findPreference("kestrel_serial")); -// -// ListPreference kestrelDeviceListPref = (ListPreference) getPreferenceScreen().findPreference("kestrel_device_address"); -// -// kestrelEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// kestrelOptions.setEnabled((boolean) newValue); -// kestrelDeviceListPref.setEnabled((boolean) newValue); -// return true; -// }); -// -// -// Preference scanPref = findPreference("scan_ble_devices"); -// -// scanPref.setOnPreferenceClickListener(preference -> { -// startBleScan(); -// return true; -// }); -// -// -// BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); -// if (btAdapter != null && btAdapter.isEnabled()) { -// -//// if (!scannedEntries.isEmpty()) { -//// kestrelDeviceListPref.setEntries(scannedEntries.toArray(new CharSequence[0])); -//// kestrelDeviceListPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); -//// } else { -//// kestrelDeviceListPref.setEnabled(false); -//// kestrelDeviceListPref.setSummary("No BLE devices found"); -//// } -// -// -// if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) { -// return; -// } -// Set bondedDevices = btAdapter.getBondedDevices(); -// -// List entries = new ArrayList<>(); -// List entryValues = new ArrayList<>(); -// -// for (BluetoothDevice device : bondedDevices) { -// String name = device.getName(); -// String mac = device.getAddress(); -// entries.add(name != null ? name + " (" + mac + ")" : mac); -// entryValues.add(mac); -// } -// -// if (!entries.isEmpty()) { -// meshDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); -// meshDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); -// -// trupulseListPref.setEntries(entries.toArray(new CharSequence[0])); -// trupulseListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); -// -// polarDeviceListPref.setEntries(entries.toArray(new CharSequence[0])); -// polarDeviceListPref.setEntryValues(entryValues.toArray(new CharSequence[0])); -// } else { -// meshDeviceListPref.setEnabled(false); -// meshDeviceListPref.setSummary("No paired Bluetooth devices found"); -// -// trupulseListPref.setEnabled(false); -// trupulseListPref.setSummary("No paired Bluetooth devices found"); -// -// polarDeviceListPref.setEnabled(false); -// polarDeviceListPref.setSummary("No paired Bluetooth devices found"); -// } -// } -// -// meshtasticOptions.setEnabled(prefs.getBoolean(meshtasticEnabled.getKey(), false)); -// meshtasticEnabled.setOnPreferenceChangeListener((preference, newValue) -> { -// meshtasticOptions.setEnabled((boolean) newValue); -// return true; -// }); -// -//// Preference bleEnable = getPreferenceScreen().findPreference("ble_enabled"); -//// Preference bleLocationMethod = getPreferenceScreen().findPreference("ble_loc_method"); -//// Preference bleOptions = getPreferenceScreen().findPreference("ble_options"); -//// Preference bleConfigURL = getPreferenceScreen().findPreference("ble_config_url"); -//// bleLocationMethod.setEnabled(prefs.getBoolean(bleEnable.getKey(), false)); -//// bleOptions.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); -//// bleConfigURL.setEnabled((prefs.getBoolean(bleEnable.getKey(), false))); -//// bleEnable.setOnPreferenceChangeListener(((preference, newValue) -> { -//// bleLocationMethod.setEnabled((boolean) newValue); -//// bleOptions.setEnabled((boolean) newValue); -//// bleConfigURL.setEnabled((boolean) newValue); -//// return true; -//// })); -// -// // TODO: introduce FLIR and ANGEL sensors -// } -// -// public void startBleScan() { -// BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter(); -// if (btAdapter == null || !btAdapter.isEnabled()) -// return; -// -// scannedEntries.clear(); -// scannedEntryValues.clear(); -// scannedDevices.clear(); -// -// -// Preference scanBlePref = findPreference("scan_ble_devices"); -// -// BluetoothLeScanner scanner = btAdapter.getBluetoothLeScanner(); -// -// -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { -// if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { -// return; -// } -// } -// -// ScanCallback scanCallback = new ScanCallback() { -// @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) -// @Override -// public void onScanResult(int callbackType, ScanResult result) { -// BluetoothDevice device = result.getDevice(); -// String name = device.getName(); -// String address = device.getAddress(); -// -// if (name == null && !scannedEntryValues.contains(address)) { -// name = "Unnamed Device"; -// } -// if (!scannedEntryValues.contains(address)) { -// scannedEntries.add(name != null ? name + " (" + address + ")" : address); -// scannedEntryValues.add(address); -// -// updateKestrelListPreference(); -// } -// } -// }; -// -// scanner.startScan(scanCallback); -// -// new Handler(Looper.getMainLooper()).postDelayed(() -> { -// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { -// if (ActivityCompat.checkSelfPermission(getContext(), -// Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { -// return; -// } -// } -// scanner.stopScan(scanCallback); -// -// if (scanBlePref != null) scanBlePref.setEnabled(true); -// -// updateKestrelListPreference(); -// }, 8000); -// -// } -// -// private void updateKestrelListPreference() { -// ListPreference kestrelPref = (ListPreference) findPreference("kestrel_device_address"); -// -// if (kestrelPref == null) return; -// -// kestrelPref.setEntries(scannedEntries.toArray(new CharSequence[0])); -// kestrelPref.setEntryValues(scannedEntryValues.toArray(new CharSequence[0])); -// kestrelPref.setEnabled(!scannedEntries.isEmpty()); -// -// if (scannedEntries.isEmpty()) { -// kestrelPref.setSummary("No BLE devices found"); -// } else { -// kestrelPref.setSummary("Select a device"); -// } -// } -// } -// -// -// /* -// * Fragment for video settings -// */ -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class VideoPreferenceFragment extends PreferenceFragment { -// ArrayList frameRateList = new ArrayList<>(); -// ArrayList resList = new ArrayList<>(); -// -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_video); -// -// PreferenceScreen videoOptsScreen = getPreferenceScreen(); -// -// // Create camera selection preference -// ArrayList cameras = new ArrayList<>(); -// for (int i = 0; i < Camera.getNumberOfCameras(); i++) { -// Camera.CameraInfo info = new Camera.CameraInfo(); -// Camera.getCameraInfo(i, info); -// cameras.add(Integer.toString(i)); -// } -// ListPreference cameraSelectList = (ListPreference) videoOptsScreen.findPreference("camera_select"); -// cameraSelectList.setEntries(cameras.toArray(new String[0])); -// cameraSelectList.setEntryValues(cameras.toArray(new String[0])); -// bindPreferenceSummaryToValue(cameraSelectList); -// videoOptsScreen.addPreference(cameraSelectList); -// -// bindPreferenceSummaryToValue(findPreference("video_codec")); -// // get possible video capture frame rates and sizes -// Camera 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); -// camera.release(); -// -// // add list of supported frame rates -// ListPreference frameRatePrefList = (ListPreference) videoOptsScreen.findPreference("video_framerate"); -// frameRatePrefList.setEntries(frameRateList.toArray(new String[0])); -// frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); -// bindPreferenceSummaryToValue(findPreference("video_framerate")); -// -// // add list of configurable presets -// ArrayList presetNames = new ArrayList<>(); -// ArrayList presetIndexes = new ArrayList<>(); -// for (int i = 1; i <= 5; i++) { -// PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(videoOptsScreen.getContext()); -// prefScreen.setKey("video_res" + i); -// String presetName = "Video Preset #" + i; -// prefScreen.setTitle(presetName); -// presetNames.add(presetName); -// presetIndexes.add(String.valueOf(i - 1)); -// -// ListPreference sizeList = new ListPreference(prefScreen.getContext()); -// sizeList.setKey("video_size" + i); -// sizeList.setTitle("Frame Size"); -// sizeList.setEntries(resList.toArray(new String[0])); -// sizeList.setEntryValues(resList.toArray(new String[0])); -// bindPreferenceSummaryToValue(sizeList); -// prefScreen.addPreference(sizeList); -// -// EditTextPreference minBitrate = new EditTextPreference(prefScreen.getContext()); -// minBitrate.setKey("video_min_bitrate" + i); -// minBitrate.setTitle("Min Bitrate (kbits/s)"); -// minBitrate.getEditText().setSingleLine(); -// minBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// minBitrate.setDefaultValue("3000"); -// bindPreferenceSummaryToValue(minBitrate); -// prefScreen.addPreference(minBitrate); -// -// EditTextPreference maxBitrate = new EditTextPreference(prefScreen.getContext()); -// maxBitrate.setKey("video_max_bitrate" + i); -// maxBitrate.setTitle("Max Bitrate (kbits/s)"); -// maxBitrate.getEditText().setSingleLine(); -// maxBitrate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// maxBitrate.setDefaultValue("3000"); -// bindPreferenceSummaryToValue(maxBitrate); -// prefScreen.addPreference(maxBitrate); -// -// bindPreferenceSummaryToValue(prefScreen); -// videoOptsScreen.addPreference(prefScreen); -// } -// -// // add list of selectable presets -// ListPreference selectedPresetList = (ListPreference) videoOptsScreen.findPreference("video_preset"); -// presetNames.add("Auto select"); -// presetIndexes.add("AUTO"); -// selectedPresetList.setEntries(presetNames.toArray(new String[0])); -// selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); -// -// // Setup Camera Listener -// cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> { -// Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue); -// updateCameraSettings(Integer.parseInt((String) newValue)); -// cameraSelectList.setSummary(newValue.toString()); -// return true; -// }); -// } -// -// protected void updateCameraSettings(Integer cameraId) { -// Camera 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); -// camera.release(); -// } -// } -// -// -// /* -// * Fragment for audio settings -// */ -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class AudioPreferenceFragment extends PreferenceFragment { -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// addPreferencesFromResource(R.xml.pref_audio); -// -// PreferenceScreen audioOptsScreen = getPreferenceScreen(); -// bindPreferenceSummaryToValue(findPreference("audio_codec")); -// -// // get possible video capture frame rates and sizes -// List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000"); -// List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192"); -// -// // add list of supported sample rates -// ListPreference sampleRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_samplerate"); -// sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0])); -// sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0])); -// bindPreferenceSummaryToValue(findPreference("audio_samplerate")); -// -// // add list of supported bitrates -// ListPreference bitRatePrefList = (ListPreference) audioOptsScreen.findPreference("audio_bitrate"); -// bitRatePrefList.setEntries(bitRateList.toArray(new String[0])); -// bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0])); -// bindPreferenceSummaryToValue(findPreference("audio_samplerate")); -// } -// } -// -// -// @TargetApi(Build.VERSION_CODES.HONEYCOMB) -// public static class KestrelPreferenceFragment extends PreferenceFragment { -// @Override -// public void onCreate(Bundle savedInstanceState) { -// super.onCreate(savedInstanceState); -// -// addPreferencesFromResource(R.xml.pref_kestrel); -// -// PreferenceScreen kestrelOptsScreen = getPreferenceScreen(); -// -// ArrayList presetNames = new ArrayList<>(); -// ArrayList presetIndexes = new ArrayList<>(); -// -// for (int i = 1; i <= 5; i++) { -// PreferenceScreen prefScreen = getPreferenceManager().createPreferenceScreen(kestrelOptsScreen.getContext()); -// prefScreen.setKey("kestrel_preset" + i); -// String presetName = "Gun Profile Preset #" + i; -// prefScreen.setTitle(presetName); -// presetNames.add(presetName); -// presetIndexes.add(String.valueOf(i - 1)); -// -// addBulletDataFields(prefScreen, i); -// addGunFields(prefScreen, i); -// addScopeDataFields(prefScreen, i); -// -// bindPreferenceSummaryToValue(prefScreen); -// kestrelOptsScreen.addPreference(prefScreen); -// } -// -// -// // add list of selectable presets -// ListPreference selectedPresetList = (ListPreference) kestrelOptsScreen.findPreference("kestrel_profile_preset"); -// presetNames.add("Auto select"); -// presetIndexes.add("AUTO"); -// selectedPresetList.setEntries(presetNames.toArray(new String[0])); -// selectedPresetList.setEntryValues(presetIndexes.toArray(new String[0])); -// } -// -// private void addProfileFields(PreferenceScreen preferenceScreen, int index) { -//// -//// -//// -//// -//// -// } -// -// private void addScopeDataFields(PreferenceScreen prefScreen, int index) { -// List unitList = Arrays.asList("inches", "mil", "tmoa", "smoa", "clicks", "cm"); -// -// ListPreference eUnitList = new ListPreference(prefScreen.getContext()); -// eUnitList.setKey("e_unit_" + index); -// eUnitList.setTitle("E Units"); -// eUnitList.setEntries(unitList.toArray(new String[0])); -// eUnitList.setEntryValues(unitList.toArray(new String[0])); -// bindPreferenceSummaryToValue(eUnitList); -// prefScreen.addPreference(eUnitList); -// -// ListPreference wUnitList = new ListPreference(prefScreen.getContext()); -// wUnitList.setKey("w_unit_" + index); -// wUnitList.setTitle("W Units"); -// wUnitList.setEntries(unitList.toArray(new String[0])); -// wUnitList.setEntryValues(unitList.toArray(new String[0])); -// bindPreferenceSummaryToValue(wUnitList); -// prefScreen.addPreference(wUnitList); -// } -// -// private void addGunFields(PreferenceScreen prefScreen, int index) { -// EditTextPreference muzzleVel = new EditTextPreference(prefScreen.getContext()); -// muzzleVel.setKey("muzzle_velocity_" + index); -// muzzleVel.setTitle("Muzzle Velocity (fps)"); -// muzzleVel.setDialogTitle("Enter the muzzle velocity (fps)"); -// muzzleVel.getEditText().setSingleLine(); -// muzzleVel.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// muzzleVel.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(muzzleVel); -// prefScreen.addPreference(muzzleVel); -// -// EditTextPreference zeroRange = new EditTextPreference(prefScreen.getContext()); -// zeroRange.setKey("zero_range_" + index); -// zeroRange.setTitle("Zero Range (m)"); -// zeroRange.setDialogTitle("Enter the zero range (m)"); -// zeroRange.getEditText().setSingleLine(); -// zeroRange.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// zeroRange.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(zeroRange); -// prefScreen.addPreference(zeroRange); -// -// EditTextPreference boreHeight = new EditTextPreference(prefScreen.getContext()); -// boreHeight.setKey("bore_height_" + index); -// boreHeight.setTitle("Bore Height (in)"); -// boreHeight.setDialogTitle("Enter the bore height (in)"); -// boreHeight.getEditText().setSingleLine(); -// boreHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// boreHeight.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(boreHeight); -// prefScreen.addPreference(boreHeight); -// -// EditTextPreference zeroHeight = new EditTextPreference(prefScreen.getContext()); -// zeroHeight.setKey("zero_height_" + index); -// zeroHeight.setTitle("Zero Height (in)"); -// zeroHeight.setDialogTitle("Enter the zero height (in)"); -// zeroHeight.getEditText().setSingleLine(); -// zeroHeight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// zeroHeight.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(zeroHeight); -// prefScreen.addPreference(zeroHeight); -// -// EditTextPreference zeroOffset = new EditTextPreference(prefScreen.getContext()); -// zeroOffset.setKey("zero_offset_" + index); -// zeroOffset.setTitle("Zero Offset (in)"); -// zeroOffset.setDialogTitle("Enter the zero offset (in)"); -// zeroOffset.getEditText().setSingleLine(); -// zeroOffset.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// zeroOffset.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(zeroOffset); -// prefScreen.addPreference(zeroOffset); -// -// EditTextPreference twistRate = new EditTextPreference(prefScreen.getContext()); -// twistRate.setKey("twist_rate_" + index); -// twistRate.setTitle("Twist Rate (in)"); -// twistRate.setDialogTitle("Enter the twist rate (in)"); -// twistRate.getEditText().setSingleLine(); -// twistRate.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// twistRate.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(twistRate); -// prefScreen.addPreference(twistRate); -// -// List directionlist = Arrays.asList("L", "R"); -// -// ListPreference twistRateList = new ListPreference(prefScreen.getContext()); -// twistRateList.setKey("twist_rate_direction_" + index); -// twistRateList.setTitle("Twist Rate Direction"); -// twistRateList.setEntries(directionlist.toArray(new String[0])); -// twistRateList.setEntryValues(directionlist.toArray(new String[0])); -// bindPreferenceSummaryToValue(twistRateList); -// prefScreen.addPreference(twistRateList); -// } -// -// -// private void addBulletDataFields(PreferenceScreen prefScreen, int index) { -// EditTextPreference diameter = new EditTextPreference(prefScreen.getContext()); -// diameter.setKey("diameter_" + index); -// diameter.setTitle("Diameter (in)"); -// diameter.setDialogTitle("Enter the diameter (inches)"); -// diameter.getEditText().setSingleLine(); -// diameter.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// diameter.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(diameter); -// prefScreen.addPreference(diameter); -// -// EditTextPreference weight = new EditTextPreference(prefScreen.getContext()); -// weight.setKey("weight_" + index); -// weight.setTitle("Weight (gr)"); -// weight.setDialogTitle("Enter the weight (gr)"); -// weight.getEditText().setSingleLine(); -// weight.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// weight.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(weight); -// prefScreen.addPreference(weight); -// -// EditTextPreference ballistic = new EditTextPreference(prefScreen.getContext()); -// ballistic.setKey("ballistic_" + index); -// ballistic.setTitle("Ballistic Coefficient (G7)"); -// ballistic.setDialogTitle("Enter the Ballistic Coefficient (G7)"); -// ballistic.getEditText().setSingleLine(); -// ballistic.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// ballistic.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(ballistic); -// prefScreen.addPreference(ballistic); -// -// EditTextPreference length = new EditTextPreference(prefScreen.getContext()); -// length.setKey("length_" + index); -// length.setTitle("Length (in)"); -// length.setDialogTitle("Enter the length (in)"); -// length.getEditText().setSingleLine(); -// length.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); -// length.setDefaultValue("0.0"); -// bindPreferenceSummaryToValue(length); -// prefScreen.addPreference(length); -// } -// -// } -// -// @Override -// protected boolean isValidFragment(String fragmentName) { -// return true; -// } -//} diff --git a/sensorhub-android-controller/AndroidManifest.xml b/sensorhub-android-controller/AndroidManifest.xml new file mode 100644 index 00000000..26edfd10 --- /dev/null +++ b/sensorhub-android-controller/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-controller/README.md b/sensorhub-android-controller/README.md new file mode 100644 index 00000000..54dfb2e4 --- /dev/null +++ b/sensorhub-android-controller/README.md @@ -0,0 +1,16 @@ +# Android Controller Driver + +OpenSensorHub driver for Android gamepad controllers. Captures real-time input from any connected gamepad via USB. + +## Captured Inputs + +- **Buttons**: A, B, X, Y, L1, R1, L3 (left stick click), R3 (right stick click), Mode, Start, Select +- **Triggers**: Left trigger, Right trigger (analog 0.0 - 1.0) +- **Joysticks**: Left stick X/Y, Right stick X/Y (analog -1.0 to 1.0) +- **D-Pad**: 8-directional (UP, DOWN, LEFT, RIGHT, and diagonals) plus NONE + +## Setup + +1. Connect a gamepad controller to the Android device (USB) +2. Enable the controller sensor in the osh-android app sensors tab +3. The driver auto-detects connected gamepads and listens for events diff --git a/sensorhub-android-controller/build.gradle b/sensorhub-android-controller/build.gradle new file mode 100644 index 00000000..1dbc2de0 --- /dev/null +++ b/sensorhub-android-controller/build.gradle @@ -0,0 +1,44 @@ +apply plugin: 'com.android.library' + +description = 'Android Controller' +ext.details = 'Driver for Android Controller Sensors' +version = '1.0.0' + +dependencies { + //api 'org.sensorhub:sensorhub-core:' + oshCoreVersion + api project(':sensorhub-core') + api project(':sensorhub-android-service') + + implementation project(path: ':sensorhub-driver-android') +} + +configurations.configureEach { + exclude group: "ch.qos.logback" +} + + +android { + namespace 'org.sensorhub.impl.sensor.controller' + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + + lintOptions { + abortOnError false + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/main/java'] + resources.srcDirs = ['src/main/resources'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } +} \ No newline at end of file diff --git a/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerConfig.java b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerConfig.java new file mode 100644 index 00000000..1193a586 --- /dev/null +++ b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerConfig.java @@ -0,0 +1,52 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.controller; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorConfig; + +import android.content.Context; +import android.provider.Settings; + + +/** + * Configuration class for the Android Controller driver. + * + * @author Kalyn Stricklin + * @since 05/26/2024 + */ +public class ControllerConfig extends SensorConfig +{ + public ControllerConfig() + { + this.moduleClass = ControllerDriver.class.getCanonicalName(); + } + + public String deviceName = "controller"; + public String uid_extension; + + public static String getUid() { + Context context = SensorHubService.getContext(); + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public String getUidWithExt() { + String baseUid = getUid(); + if (uid_extension != null && !uid_extension.isEmpty()) + return baseUid + ":" + uid_extension; + return baseUid; + } +} diff --git a/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerDriver.java b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerDriver.java new file mode 100644 index 00000000..5162cfbb --- /dev/null +++ b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerDriver.java @@ -0,0 +1,264 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.controller; + +import android.content.Context; +import android.hardware.input.InputManager; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +import net.opengis.sensorml.v20.PhysicalComponent; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.SensorMLBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + + +/** + * Android gamepad controller driver. Captures button presses, trigger axes, joystick axes and D-Pad input from any connected gamepad + * + * @author Kalyn Stricklin + * @since 05/26/2024 + */ +public class ControllerDriver extends AbstractSensorModule implements InputManager.InputDeviceListener { + private final ArrayList smlComponents; + private final SensorMLBuilder smlBuilder; + static final String UID_PREFIX = "urn:osh:sensor:controller:"; + static final Logger logger = LoggerFactory.getLogger(ControllerDriver.class.getSimpleName()); + private Context context; + ControllerOutput output; + private HandlerThread eventThread; + private Handler eventHandler; + private InputManager inputManager; + private int controllerDeviceId = -1; + + private boolean btnA, btnB, btnX, btnY, + btnL1, btnR1, btnL3, btnR3, + btnMode, btnStart, btnSelect; + private float triggerL, triggerR, + leftX, leftY, rightX, rightY; + private String dpad = "NONE"; + public ControllerDriver() { + this.smlComponents = new ArrayList(); + this.smlBuilder = new SensorMLBuilder(); + } + + @Override + public void doInit() { + logger.info("Initializing Controller Sensor"); + this.xmlID = "CONTROLLER_" + Build.SERIAL; + this.uniqueID = UID_PREFIX + config.getUidWithExt(); + + context = SensorHubService.getContext(); + inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); + + findController(); + + output = new ControllerOutput(this); + output.doInit(); + addOutput(output, false); + } + + @Override + public void doStart() throws SensorException { + eventThread = new HandlerThread("ControllerThread"); + eventThread.start(); + eventHandler = new Handler(eventThread.getLooper()); + + inputManager.registerInputDeviceListener(this, eventHandler); + + logger.info("Controller sensor started, device ID: {}", controllerDeviceId); + } + + private void findController() { + int[] deviceIds = inputManager.getInputDeviceIds(); + for (int id : deviceIds) { + InputDevice device = inputManager.getInputDevice(id); + if (device != null && isGamepad(device)) { + controllerDeviceId = id; + logger.info("Found controller: {} (id={})", device.getName(), id); + return; + } + } + logger.warn("No gamepad controller connected"); + } + + private boolean isGamepad(InputDevice device) { + return device.supportsSource(InputDevice.SOURCE_GAMEPAD) || device.supportsSource(InputDevice.SOURCE_JOYSTICK); + } + + public boolean onKeyEvent(KeyEvent event) { + if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == 0 + && (event.getSource() & InputDevice.SOURCE_JOYSTICK) == 0) + return false; + + if (event.getRepeatCount() > 0) + return true; + + boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN; + int keyCode = event.getKeyCode(); + + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_A: btnA = pressed; break; + case KeyEvent.KEYCODE_BUTTON_B: btnB = pressed; break; + case KeyEvent.KEYCODE_BUTTON_X: btnX = pressed; break; + case KeyEvent.KEYCODE_BUTTON_Y: btnY = pressed; break; + case KeyEvent.KEYCODE_BUTTON_L1: btnL1 = pressed; break; + case KeyEvent.KEYCODE_BUTTON_R1: btnR1 = pressed; break; + case KeyEvent.KEYCODE_BUTTON_THUMBL: btnL3 = pressed; break; + case KeyEvent.KEYCODE_BUTTON_THUMBR: btnR3 = pressed; break; + case KeyEvent.KEYCODE_BUTTON_MODE: btnMode = pressed; break; + case KeyEvent.KEYCODE_BUTTON_START: btnStart = pressed; break; + case KeyEvent.KEYCODE_BUTTON_SELECT: btnSelect = pressed; break; + default: return false; + } + + logger.info("Button: {} {}", keyCodeName(keyCode), pressed ? "PRESSED" : "RELEASED"); + publishState(); + return true; + } + + public boolean onMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == 0) + return false; + if (event.getAction() != MotionEvent.ACTION_MOVE) + return false; + + InputDevice device = event.getDevice(); + + leftX = getCenteredAxis(event, device, MotionEvent.AXIS_X); + leftY = getCenteredAxis(event, device, MotionEvent.AXIS_Y); + rightX = getCenteredAxis(event, device, MotionEvent.AXIS_Z); + rightY = getCenteredAxis(event, device, MotionEvent.AXIS_RZ); + + triggerL = event.getAxisValue(MotionEvent.AXIS_LTRIGGER); + triggerR = event.getAxisValue(MotionEvent.AXIS_RTRIGGER); + + float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X); + float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y); + dpad = hatToDpad(hatX, hatY); + + publishState(); + return true; + } + + private void publishState() { + output.setData( + btnA, btnB, btnX, btnY, + btnL1, btnR1, triggerL, triggerR, + btnL3, btnR3, + btnMode, btnStart, btnSelect, + dpad, + leftX, leftY, rightX, rightY + ); + } + + private static float getCenteredAxis(MotionEvent event, InputDevice device, int axis) { + InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); + if (range != null) { + float flat = range.getFlat(); + float value = event.getAxisValue(axis); + if (Math.abs(value) > flat) + return value; + } + return 0; + } + + private static String hatToDpad(float hatX, float hatY) { + boolean left = Float.compare(hatX, -1.0f) == 0; + boolean right = Float.compare(hatX, 1.0f) == 0; + boolean up = Float.compare(hatY, -1.0f) == 0; + boolean down = Float.compare(hatY, 1.0f) == 0; + + if (up && left) return "UP_LEFT"; + if (up && right) return "UP_RIGHT"; + if (down && left) return "DOWN_LEFT"; + if (down && right) return "DOWN_RIGHT"; + if (up) return "UP"; + if (down) return "DOWN"; + if (left) return "LEFT"; + if (right) return "RIGHT"; + return "NONE"; + } + + private static String keyCodeName(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_BUTTON_A: return "A"; + case KeyEvent.KEYCODE_BUTTON_B: return "B"; + case KeyEvent.KEYCODE_BUTTON_X: return "X"; + case KeyEvent.KEYCODE_BUTTON_Y: return "Y"; + case KeyEvent.KEYCODE_BUTTON_L1: return "L1"; + case KeyEvent.KEYCODE_BUTTON_R1: return "R1"; + case KeyEvent.KEYCODE_BUTTON_THUMBL: return "L3"; + case KeyEvent.KEYCODE_BUTTON_THUMBR: return "R3"; + case KeyEvent.KEYCODE_BUTTON_MODE: return "MODE"; + case KeyEvent.KEYCODE_BUTTON_START: return "START"; + case KeyEvent.KEYCODE_BUTTON_SELECT: return "SELECT"; + default: return "KEY_" + keyCode; + } + } + + @Override + public void onInputDeviceAdded(int deviceId) { + InputDevice device = inputManager.getInputDevice(deviceId); + if (device != null && isGamepad(device)) { + controllerDeviceId = deviceId; + logger.info("Controller connected: {} (id={})", device.getName(), deviceId); + } + } + + @Override + public void onInputDeviceRemoved(int deviceId) { + if (deviceId == controllerDeviceId) { + logger.info("Controller disconnected (id={})", deviceId); + controllerDeviceId = -1; + } + } + + @Override + public void onInputDeviceChanged(int deviceId) { + logger.debug("Input device changed: {}", deviceId); + } + + @Override + public void doStop() { + if (inputManager != null) { + inputManager.unregisterInputDeviceListener(this); + } + + if (eventThread != null) { + eventThread.quitSafely(); + eventThread = null; + } + + eventHandler = null; + logger.info("Controller sensor stopped"); + } + + @Override + public boolean isConnected() { + return controllerDeviceId >= 0; + } +} diff --git a/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerOutput.java b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerOutput.java new file mode 100644 index 00000000..cf7dcb5a --- /dev/null +++ b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/ControllerOutput.java @@ -0,0 +1,201 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.controller; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; + + +/** + * Single unified output for gamepad controller state: + * buttons, triggers, joystick axes, and D-Pad. + * + * @author Kalyn Stricklin + * @since 05/26/2024 + */ +public class ControllerOutput extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "controller"; + private static final String SENSOR_OUTPUT_LABEL = "Gamepad Controller"; + private static final Logger logger = LoggerFactory.getLogger(ControllerOutput.class); + + protected ControllerOutput(ControllerDriver parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + SWEHelper fac = new SWEHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("GamepadState")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("btnA", fac.createBoolean() + .label("A Button") + .definition(SWEHelper.getPropertyUri("ButtonA")) + .build()) + .addField("btnB", fac.createBoolean() + .label("B Button") + .definition(SWEHelper.getPropertyUri("ButtonB")) + .build()) + .addField("btnX", fac.createBoolean() + .label("X Button") + .definition(SWEHelper.getPropertyUri("ButtonX")) + .build()) + .addField("btnY", fac.createBoolean() + .label("Y Button") + .definition(SWEHelper.getPropertyUri("ButtonY")) + .build()) + .addField("btnL1", fac.createBoolean() + .label("Left Bumper") + .definition(SWEHelper.getPropertyUri("LeftBumper")) + .build()) + .addField("btnR1", fac.createBoolean() + .label("Right Bumper") + .definition(SWEHelper.getPropertyUri("RightBumper")) + .build()) + .addField("triggerL", fac.createQuantity() + .label("Left Trigger") + .definition(SWEHelper.getPropertyUri("LeftTrigger")) + .build()) + .addField("triggerR", fac.createQuantity() + .label("Right Trigger") + .definition(SWEHelper.getPropertyUri("RightTrigger")) + .build()) + .addField("btnL3", fac.createBoolean() + .label("Left Stick Click") + .definition(SWEHelper.getPropertyUri("LeftStickClick")) + .build()) + .addField("btnR3", fac.createBoolean() + .label("Right Stick Click") + .definition(SWEHelper.getPropertyUri("RightStickClick")) + .build()) + .addField("btnMode", fac.createBoolean() + .label("Mode Button") + .definition(SWEHelper.getPropertyUri("ModeButton")) + .build()) + .addField("btnStart", fac.createBoolean() + .label("Start Button") + .definition(SWEHelper.getPropertyUri("StartButton")) + .build()) + .addField("btnSelect", fac.createBoolean() + .label("Select Button") + .definition(SWEHelper.getPropertyUri("SelectButton")) + .build()) + .addField("dpad", fac.createCategory() + .label("D-Pad Direction") + .definition(SWEHelper.getPropertyUri("DPadDirection")) + .addAllowedValues("NONE", "UP", "UP_RIGHT", "RIGHT", "DOWN_RIGHT", + "DOWN", "DOWN_LEFT", "LEFT", "UP_LEFT") + .build()) + .addField("leftStickX", fac.createQuantity() + .label("Left Stick X") + .definition(SWEHelper.getPropertyUri("LeftStickX")) + .addAllowedInterval(-1.0, 1.0) + .build()) + .addField("leftStickY", fac.createQuantity() + .label("Left Stick Y") + .definition(SWEHelper.getPropertyUri("LeftStickY")) + .build()) + .addField("rightStickX", fac.createQuantity() + .label("Right Stick X") + .definition(SWEHelper.getPropertyUri("RightStickX")) + .addAllowedInterval(-1.0, 1.0) + .build()) + .addField("rightStickY", fac.createQuantity() + .label("Right Stick Y") + .definition(SWEHelper.getPropertyUri("RightStickY")) + .build()) + + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + public void setData(boolean a, boolean b, boolean x, boolean y, + boolean l1, boolean r1, float triggerL, float triggerR, + boolean l3, boolean r3, + boolean mode, boolean start, boolean select, + String dpad, + float leftX, float leftY, float rightX, float rightY) { + + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + + dataBlock.setBooleanValue(idx++, a); + dataBlock.setBooleanValue(idx++, b); + dataBlock.setBooleanValue(idx++, x); + dataBlock.setBooleanValue(idx++, y); + + dataBlock.setBooleanValue(idx++, l1); + dataBlock.setBooleanValue(idx++, r1); + + dataBlock.setDoubleValue(idx++, triggerL); + dataBlock.setDoubleValue(idx++, triggerR); + + dataBlock.setBooleanValue(idx++, l3); + dataBlock.setBooleanValue(idx++, r3); + + dataBlock.setBooleanValue(idx++, mode); + dataBlock.setBooleanValue(idx++, start); + dataBlock.setBooleanValue(idx++, select); + + dataBlock.setStringValue(idx++, dpad); + + dataBlock.setDoubleValue(idx++, leftX); + dataBlock.setDoubleValue(idx++, leftY); + dataBlock.setDoubleValue(idx++, rightX); + dataBlock.setDoubleValue(idx++, rightY); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 1; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/Descriptor.java b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/Descriptor.java new file mode 100644 index 00000000..e501a9f9 --- /dev/null +++ b/sensorhub-android-controller/src/main/java/org/sensorhub/impl/sensor/controller/Descriptor.java @@ -0,0 +1,75 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.controller; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; + + +/** + *

+ * Descriptor of Android Controller driver module for automatic discovery + * by the ModuleRegistry + *

+ * + * @author Kalyn Stricklin + * @since 05/26/2024 + */ +public class Descriptor implements IModuleProvider +{ + + @Override + public String getModuleName() + { + return "Android Controller Driver"; + } + + + @Override + public String getModuleDescription() + { + return "Driver supporting Android Controllers"; + } + + + @Override + public String getModuleVersion() + { + return "0.1"; + } + + + @Override + public String getProviderName() + { + return "Botts Innovative Research, Inc."; + } + + + @Override + public Class> getModuleClass() + { + return ControllerDriver.class; + } + + + @Override + public Class getModuleConfigClass() + { + return ControllerConfig.class; + } + +} diff --git a/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..3bd1f5f8 --- /dev/null +++ b/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.controller.Descriptor \ No newline at end of file diff --git a/sensorhub-android-controller/src/test/java/empty b/sensorhub-android-controller/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-controller/src/test/resources/empty b/sensorhub-android-controller/src/test/resources/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/BatteryOutput.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/BatteryOutput.java index 35126a8d..7d10f0b1 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/BatteryOutput.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/BatteryOutput.java @@ -8,7 +8,8 @@ WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. - Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. ******************************* END LICENSE BLOCK ***************************/ diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/HeartRateOutput.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/HeartRateOutput.java index 71748029..18dda79f 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/HeartRateOutput.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/HeartRateOutput.java @@ -8,7 +8,8 @@ WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. - Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. ******************************* END LICENSE BLOCK ***************************/ diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java index c9fd2821..a73b6586 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java @@ -8,7 +8,9 @@ WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. - Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.polar; diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java index 846e0f2a..42a4a193 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java @@ -1,16 +1,17 @@ /***************************** BEGIN LICENSE BLOCK *************************** -The contents of this file are subject to the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the License. - -Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. - -******************************* END LICENSE BLOCK ***************************/ + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.polar; diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java index df51544c..1ec1a479 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java @@ -1,17 +1,17 @@ /***************************** BEGIN LICENSE BLOCK *************************** -The contents of this file are subject to the Mozilla Public License, v. 2.0. -If a copy of the MPL was not distributed with this file, You can obtain one -at http://mozilla.org/MPL/2.0/. - -Software distributed under the License is distributed on an "AS IS" basis, -WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License -for the specific language governing rights and limitations under the License. - -Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. - -******************************* END LICENSE BLOCK ***************************/ + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ package org.sensorhub.impl.sensor.polar; import org.sensorhub.api.module.IModule; diff --git a/sensorhub-android-wardriving/AndroidManifest.xml b/sensorhub-android-wardriving/AndroidManifest.xml new file mode 100644 index 00000000..d2a3abe1 --- /dev/null +++ b/sensorhub-android-wardriving/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-wardriving/README.md b/sensorhub-android-wardriving/README.md new file mode 100644 index 00000000..0bb1e3a4 --- /dev/null +++ b/sensorhub-android-wardriving/README.md @@ -0,0 +1,27 @@ +# Wardriving WiFi and BLE Scan Driver + +OpenSensorHub driver that performs wardriving by scanning for nearby WiFi access points and Bluetooth Low Energy (BLE) devices, and device's GPS location at the time of scan. + +## Outputs + +### WiFi Scan +Each observation captures a single access point: +- **BSSID** - MAC address of the access point +- **SSID** - Network name (empty for hidden networks) +- **RSSI** - Signal strength in dBm +- **Frequency** - Channel frequency in MHz +- **Capabilities** - Security/encryption schemes (e.g. WPA2, WPA3) +- **Location** - GPS lat/lon/alt of the device at scan time + +### BLE Scan +Each observation captures a single BLE device: +- **Device Address** - MAC address of the BLE device +- **Device Name** - Advertised name (if available) +- **RSSI** - Signal strength in dBm +- **Location** - GPS lat/lon/alt of the device at scan time + +## Setup +1. Enable the wardriving sensor in the osh-android app sensors tab +2. Ensure WiFi and Bluetooth are enabled on the device +3. Grant location and nearby device permissions if prompted +4. The driver begins periodic WiFi scans and continuous BLE scanning automatically \ No newline at end of file diff --git a/sensorhub-android-wardriving/build.gradle b/sensorhub-android-wardriving/build.gradle new file mode 100644 index 00000000..ef5842a0 --- /dev/null +++ b/sensorhub-android-wardriving/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.library' + +description = 'Wardriving' +ext.details = 'Driver for scanning and logging wireless networks' +version = '1.0.0' + +dependencies { + //api 'org.sensorhub:sensorhub-core:' + oshCoreVersion + api project(':sensorhub-core') + api project(':sensorhub-android-service') + + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation project(path: ':sensorhub-driver-android') + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core:1.5.0' +} +configurations.configureEach { + exclude group: "ch.qos.logback" +} + +android { + namespace 'org.sensorhub.impl.sensor.wardriving' + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + + lintOptions { + abortOnError false + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/main/java'] + resources.srcDirs = ['src/main/resources'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java new file mode 100644 index 00000000..dd458d8b --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java @@ -0,0 +1,116 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for BLE device scan results. + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class BLEOutput extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "bleScan"; + private static final String SENSOR_OUTPUT_LABEL = "BLE Device Scan"; + private static final Logger logger = LoggerFactory.getLogger(BLEOutput.class); + + protected BLEOutput(Wardriving parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("BLEScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("deviceAddress", fac.createText() + .label("Device Address") + .definition(SWEHelper.getPropertyUri("NetworkAddress")) + .description("MAC address of the BLE device") + .build()) + .addField("deviceName", fac.createText() + .label("Device Name") + .definition(SWEHelper.getPropertyUri("DeviceName")) + .description("Advertised name of the BLE device") + .build()) + .addField("rssi", fac.createQuantity() + .label("Signal Strength") + .definition(SWEHelper.getPropertyUri("SignalStrength")) + .description("Received signal strength indicator") + .build()) + .addField("location", fac.newLocationVectorLLA( + SWEHelper.getPropertyUri("SensorLocation"))) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + public void setData(String deviceAddress, String deviceName, int rssi, double lat, double lon, double alt) { + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, deviceAddress != null ? deviceAddress : ""); + dataBlock.setStringValue(idx++, deviceName != null ? deviceName : ""); + dataBlock.setIntValue(idx++, rssi); + dataBlock.setDoubleValue(idx++, lat); + dataBlock.setDoubleValue(idx++, lon); + dataBlock.setDoubleValue(idx++, alt); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java new file mode 100644 index 00000000..f666c9ee --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java @@ -0,0 +1,76 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; + + +/** + *

+ * Descriptor of Android sensors driver module for automatic discovery + * by the ModuleRegistry + *

+ * + * @author Alex Robin + * @since Sep 7, 2013 + */ +public class Descriptor implements IModuleProvider +{ + + @Override + public String getModuleName() + { + return "Wardriving"; + } + + + @Override + public String getModuleDescription() + { + return "Driver for collecting wireless networks"; + } + + + @Override + public String getModuleVersion() + { + return "0.1"; + } + + + @Override + public String getProviderName() + { + return "GeoRobotix LLC"; + } + + + @Override + public Class> getModuleClass() + { + return Wardriving.class; + } + + + @Override + public Class getModuleConfigClass() + { + return WardrivingConfig.class; + } + +} 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 new file mode 100644 index 00000000..f806e839 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java @@ -0,0 +1,351 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanSettings; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; + +import androidx.core.content.ContextCompat; + +import net.opengis.sensorml.v20.PhysicalComponent; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.SensorMLBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Wardriving sensor driver that scans for WiFi access points and + * records their details along with the device's GPS location. + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Wardriving extends AbstractSensorModule { + static final String UID_PREFIX = "urn:osh:sensor:wardriving:"; + + private final ArrayList smlComponents; + private final SensorMLBuilder smlBuilder; + static final Logger logger = LoggerFactory.getLogger(Wardriving.class.getSimpleName()); + + private Context context; + WifiOutput wifiOutput; + BLEOutput bleOutput; + private HandlerThread eventThread; + private Handler eventHandler; + private BluetoothLeScanner bluetoothLeScanner; + private BluetoothManager bluetoothManager; + private WifiManager wifiManager; + private LocationManager locationManager; + private BroadcastReceiver wifiReceiver; + private LocationListener locationListener; + + private volatile double currentLat = 0.0; + private volatile double currentLon = 0.0; + private volatile double currentAlt = 0.0; + private volatile boolean scanning = false; + private Runnable scanRunnable; + private ScanCallback bleScanCallback; + + public Wardriving() { + this.smlComponents = new ArrayList(); + this.smlBuilder = new SensorMLBuilder(); + } + + @Override + public void doInit() { + logger.info("Initializing Wardriving Sensor"); + this.xmlID = "WARDRIVING_" + Build.SERIAL; + this.uniqueID = UID_PREFIX + config.getUidWithExt(); + + context = SensorHubService.getContext(); + + bleOutput = new BLEOutput(this); + bleOutput.doInit(); + addOutput(bleOutput, false); + + wifiOutput = new WifiOutput(this); + wifiOutput.doInit(); + addOutput(wifiOutput, false); + } + + @Override + public void doStart() throws SensorException { + eventThread = new HandlerThread("WardrivingThread"); + eventThread.start(); + eventHandler = new Handler(eventThread.getLooper()); + + wifiManager = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + if (wifiManager == null) + throw new SensorException("WiFi service not available"); + + if (!wifiManager.isWifiEnabled()) { + logger.warn("WiFi is disabled"); + } + + wifiReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + logger.info("WiFi scan broadcast received"); + handleScanResults(); + } + }; + + IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + context.registerReceiver(wifiReceiver, filter); + + // start GPS location updates + startLocationUpdates(); + + scanning = true; + scanRunnable = new Runnable() { + @Override + public void run() { + if (scanning) { + logger.info("Triggering WiFi scan"); + boolean started = wifiManager.startScan(); + logger.info("WiFi scan started: {}", started); + eventHandler.postDelayed(this, config.scanIntervalMs); + } + } + }; + eventHandler.post(scanRunnable); + + // start BLE scanning + startBleScan(); + + logger.info("Wardriving sensor started"); + } + + private void startBleScan() { + bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (bluetoothManager == null) { + logger.warn("BluetoothManager not available, skipping BLE scan"); + return; + } + + BluetoothAdapter adapter = bluetoothManager.getAdapter(); + if (adapter == null || !adapter.isEnabled()) { + logger.warn("Bluetooth adapter not available or disabled, skipping BLE scan"); + return; + } + + try { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { + logger.error("BLUETOOTH_SCAN permission not granted"); + return; + } + + bluetoothLeScanner = adapter.getBluetoothLeScanner(); + if (bluetoothLeScanner == null) { + logger.warn("BLE scanner not available"); + return; + } + + bleScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) { + if (!scanning) return; + + String address = result.getDevice().getAddress(); + String name = null; + try { + name = result.getDevice().getName(); + } catch (SecurityException e) { + } + int rssi = result.getRssi(); + + bleOutput.setData(address, name, rssi, currentLat, currentLon, currentAlt); + } + + @Override + public void onBatchScanResults(List results) { + for (android.bluetooth.le.ScanResult result : results) { + onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result); + } + } + + @Override + public void onScanFailed(int errorCode) { + logger.error("BLE scan failed with error code: {}", errorCode); + } + }; + + ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(0) + .build(); + + bluetoothLeScanner.startScan(Collections.emptyList(), settings, bleScanCallback); + logger.info("BLE scanning started"); + + } catch (SecurityException e) { + logger.error("Security exception starting BLE scan", e); + } + } + + private void startLocationUpdates() { + locationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + currentLat = location.getLatitude(); + currentLon = location.getLongitude(); + currentAlt = location.hasAltitude() ? location.getAltitude() : 0.0; + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) {} + @Override + public void onProviderEnabled(String provider) {} + @Override + public void onProviderDisabled(String provider) {} + }; + + try { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000, 0, locationListener, eventThread.getLooper()); + + Location last = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (last != null) { + currentLat = last.getLatitude(); + currentLon = last.getLongitude(); + currentAlt = last.hasAltitude() ? last.getAltitude() : 0.0; + } + } else { + logger.error("Location permission not granted"); + } + } catch (SecurityException e) { + logger.error("Security exception requesting location updates", e); + } + } + + private void handleScanResults() { + if (!scanning) + return; + + try { + List results = wifiManager.getScanResults(); + if (results == null || results.isEmpty()) { + logger.debug("No WiFi scan results"); + return; + } + + logger.info("Scan found {} WiFi access points at [{}, {}]", + results.size(), currentLat, currentLon); + + for (ScanResult ap : results) { + logger.info("AP: BSSID={} SSID=\"{}\" RSSI={}dBm Freq={}MHz Security={}", + ap.BSSID, + ap.SSID != null ? ap.SSID : "", + ap.level, + ap.frequency, + ap.capabilities); + + wifiOutput.setData( + ap.BSSID, + ap.SSID, + ap.level, + ap.frequency, + ap.capabilities, + currentLat, + currentLon, + currentAlt + ); + } + } catch (SecurityException e) { + logger.error("Security exception reading scan results", e); + } + } + + @Override + public void doStop() { + scanning = false; + + if (eventHandler != null && scanRunnable != null) { + eventHandler.removeCallbacks(scanRunnable); + } + + if (wifiReceiver != null) { + try { + context.unregisterReceiver(wifiReceiver); + } catch (IllegalArgumentException e) { + logger.warn("WiFi receiver already unregistered"); + } + wifiReceiver = null; + } + + if (bluetoothLeScanner != null && bleScanCallback != null) { + try { + bluetoothLeScanner.stopScan(bleScanCallback); + } catch (SecurityException e) { + logger.warn("Security exception stopping BLE scan", e); + } + bleScanCallback = null; + bluetoothLeScanner = null; + } + + if (locationManager != null && locationListener != null) { + locationManager.removeUpdates(locationListener); + locationListener = null; + } + + if (eventThread != null) { + eventThread.quitSafely(); + eventThread = null; + } + + eventHandler = null; + logger.info("Wardriving sensor stopped"); + } + + @Override + public boolean isConnected() { + return wifiManager != null && scanning; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java new file mode 100644 index 00000000..82daa6b9 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java @@ -0,0 +1,52 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorConfig; + +import android.content.Context; +import android.provider.Settings; + + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class WardrivingConfig extends SensorConfig +{ + + public WardrivingConfig() + { + this.moduleClass = Wardriving.class.getCanonicalName(); + } + public String uid_extension; + + public long scanIntervalMs = 10000; + + public static String getUid() { + Context context = SensorHubService.getContext(); + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public String getUidWithExt() + { + String baseUid = getUid(); + if (uid_extension != null && !uid_extension.isEmpty()) + return baseUid + ":" + uid_extension; + return baseUid; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java new file mode 100644 index 00000000..438a8b59 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java @@ -0,0 +1,132 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for wardriving WiFi access point scan results + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class WifiOutput extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "wifiScan"; + private static final String SENSOR_OUTPUT_LABEL = "WiFi Access Point Scan"; + private static final Logger logger = LoggerFactory.getLogger(WifiOutput.class); + + protected WifiOutput(Wardriving parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("WifiScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("bssid", fac.createText() + .label("BSSID") + .definition(SWEHelper.getPropertyUri("NetworkAddress")) + .description("MAC address of the access point") + .build()) + .addField("ssid", fac.createText() + .label("SSID") + .definition(SWEHelper.getPropertyUri("NetworkName")) + .description("Network name (may be empty for hidden networks)") + .build()) + .addField("rssi", fac.createQuantity() + .label("Signal Strength") + .definition(SWEHelper.getPropertyUri("SignalStrength")) + .description("Received signal strength indicator") + .build()) + .addField("frequency", fac.createQuantity() + .label("Channel Frequency") + .definition(SWEHelper.getPropertyUri("RadioFrequency")) + .description("Center frequency of the channel in MHz") + .build()) + .addField("capabilities", fac.createText() + .label("Security Capabilities") + .definition(SWEHelper.getPropertyUri("SecurityCapabilities")) + .description("Authentication and encryption schemes supported") + .build()) + .addField("location", fac.newLocationVectorLLA( + SWEHelper.getPropertyUri("SensorLocation"))) + + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + + public void setData(String bssid, String ssid, int rssi, int frequency, + String capabilities, double lat, double lon, double alt) { + + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, bssid); + dataBlock.setStringValue(idx++, ssid != null ? ssid : ""); + dataBlock.setIntValue(idx++, rssi); + dataBlock.setIntValue(idx++, frequency); + dataBlock.setStringValue(idx++, capabilities != null ? capabilities : ""); + dataBlock.setDoubleValue(idx++, lat); + dataBlock.setDoubleValue(idx++, lon); + dataBlock.setDoubleValue(idx++, alt); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..26092ad3 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.wardriving.Descriptor \ No newline at end of file diff --git a/sensorhub-android-wardriving/src/test/java/empty b/sensorhub-android-wardriving/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-wardriving/src/test/resources/empty b/sensorhub-android-wardriving/src/test/resources/empty new file mode 100644 index 00000000..e69de29b From 22e88d08a7d6e1ec2b3932a09aaaae459b450355 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Apr 2026 14:56:50 -0500 Subject: [PATCH 04/26] added template directory and readme --- sensorhub-android-app/build.gradle | 20 ++- .../res/xml/pref_sensors.xml | 25 ++++ .../org/sensorhub/android/MainActivity.java | 22 +++- .../sensorhub/android/SensorsFragment.java | 30 ++--- .../AndroidManifest.xml | 21 ++++ sensorhub-android-template/README.md | 91 ++++++++++++++ sensorhub-android-template/build.gradle | 47 +++++++ .../impl/sensor/template/Descriptor.java | 76 +++++++++++ .../impl/sensor/template/Output.java | 101 +++++++++++++++ .../impl/sensor/template/Sensor.java | 118 ++++++++++++++++++ .../impl/sensor/template/TemplateConfig.java | 51 ++++++++ .../org.sensorhub.api.module.IModuleProvider | 1 + .../src/test/java/empty | 0 .../src/test/resources/empty | 0 14 files changed, 585 insertions(+), 18 deletions(-) create mode 100644 sensorhub-android-template/AndroidManifest.xml create mode 100644 sensorhub-android-template/README.md create mode 100644 sensorhub-android-template/build.gradle create mode 100644 sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Descriptor.java create mode 100644 sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java create mode 100644 sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java create mode 100644 sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java create mode 100644 sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider create mode 100644 sensorhub-android-template/src/test/java/empty create mode 100644 sensorhub-android-template/src/test/resources/empty diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index b1862e07..986801df 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' description = 'OSH Android App' -ext.details = 'OSH app for Android' +ext.details = 'OSH app for Android' repositories { // maven { @@ -29,6 +29,7 @@ dependencies { implementation project(':sensorhub-android-polar') implementation project(':sensorhub-android-wardriving') implementation project(':sensorhub-android-controller') + implementation project(':sensorhub-android-template') implementation project(':sensorhub-driver-android') implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.github.tony19:logback-android:3.0.0' @@ -59,8 +60,24 @@ android { targetSdkVersion rootProject.targetSdkVersion versionCode 1 versionName rootProject.version + applicationId "com.georobotix.android" } + //https://developer.android.com/build/build-variants#groovy +// flavorDimensions += "version" // maybe dont need +// productFlavors { +// create("free") { +// dimension = "version" +// applicationIdSuffix = ".free" +// buildConfigField("boolean", "IS_PREMIUM", "false") +// } +// create("premium") { +// dimension = "version" +// applicationIdSuffix = ".premium" +// buildConfigField("boolean", "IS_PREMIUM", "true") +// } +// } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -94,5 +111,6 @@ android { excludes += ["META-INF/INDEX.LIST"] } } + } diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index b2a5c6a0..7093e1c8 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -455,4 +455,29 @@ android:entryValues="@array/sos_option_values" android:defaultValue="@array/sos_option_defaults" android:layout="@layout/preference_list_item"/> + + + + + + + diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 06527c90..9ea3a842 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -14,8 +14,6 @@ package org.sensorhub.android; -import static android.content.ContentValues.TAG; - import android.Manifest; import android.annotation.SuppressLint; import android.app.AlertDialog; @@ -89,6 +87,7 @@ import org.sensorhub.impl.sensor.meshtastic.control.TextMessageControl; import org.sensorhub.impl.sensor.polar.PolarConfig; import org.sensorhub.impl.sensor.ste.STERadPagerConfig; +import org.sensorhub.impl.sensor.template.TemplateConfig; import org.sensorhub.impl.sensor.trupulse.SimulatedDataStream; import org.sensorhub.impl.sensor.trupulse.TruPulseConfig; import org.sensorhub.impl.sensor.trupulse.TruPulseWithGeolocConfig; @@ -166,7 +165,8 @@ enum Sensors { PolarHRMonitor, Kestrel, Wardriving, - Controller + Controller, + Template } private final ServiceConnection sConn = new ServiceConnection() @@ -554,6 +554,19 @@ public boolean verify(String arg0, SSLSession arg1) { if (isSosServiceEnabled) { sensorhubConfig.add(sosConfig); } + + // Template Driver + enabled = prefs.getBoolean("template_enabled", false); + if (enabled) { + TemplateConfig templateConfig = new TemplateConfig(); + templateConfig.id = "TEMPLATE_DRIVER_"; + templateConfig.name = "Template [" + deviceName + "]"; + templateConfig.autoStart = true; + templateConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; + templateConfig.uid_extension = prefs.getString("uid_extension", ""); + sensorhubConfig.add(templateConfig); + } + } protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) @@ -889,6 +902,9 @@ boolean isPushingSensor(Sensors sensor) { } else if (Sensors.Controller.equals(sensor)) { return prefs.getBoolean("controller_enabled", false) && prefs.getStringSet("controller_options", Collections.emptySet()).contains("PUSH_REMOTE"); + } else if (Sensors.Template.equals(sensor)) { + return prefs.getBoolean("template_enabled", false) + && prefs.getStringSet("template_options", Collections.emptySet()).contains("PUSH_REMOTE"); } return false; diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index 181b1f98..766ce7f8 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -36,25 +36,26 @@ public class SensorsFragment extends PreferenceFragmentCompat { private static final String[][] SWITCH_DEPENDENTS = { - {"accel_enabled", "accel_options"}, - {"gyro_enabled", "gyro_options"}, - {"mag_enabled", "mag_options"}, + {"accel_enabled", "accel_options"}, + {"gyro_enabled", "gyro_options"}, + {"mag_enabled", "mag_options"}, {"orient_quat_enabled", "orient_quat_options"}, {"orient_euler_enabled","orient_euler_options"}, - {"gps_enabled", "gps_options"}, - {"netloc_enabled", "netloc_options"}, - {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_preset", "camera_select"}, - {"video_roll_enabled", "video_roll_options"}, - {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"}, - {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"}, - {"polar_enabled", "polar_device_address", "polar_options"}, - {"kestrel_enabled", "kestrel_device_address", "kestrel_options"}, - {"trupulse_enabled", "trupulse_datasource", "trupulse_options", "trupulse_device_address", "trupulse_simu"}, - {"angel_enabled", "angel_address", "angel_options"}, - {"flirone_enabled", "flir_options"}, + {"gps_enabled", "gps_options"}, + {"netloc_enabled", "netloc_options"}, + {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_preset", "camera_select"}, + {"video_roll_enabled", "video_roll_options"}, + {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"}, + {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"}, + {"polar_enabled", "polar_device_address", "polar_options"}, + {"kestrel_enabled", "kestrel_device_address", "kestrel_options"}, + {"trupulse_enabled", "trupulse_datasource", "trupulse_options", "trupulse_device_address", "trupulse_simu"}, + {"angel_enabled", "angel_address", "angel_options"}, + {"flirone_enabled", "flir_options"}, {"ste_radpager_enabled","ste_radpager_options"}, {"wardriving_enabled", "wardriving_options"}, {"controller_enabled", "controller_options"}, + {"template_enabled", "template_device_address", "template_options"}, }; @@ -64,6 +65,7 @@ public class SensorsFragment extends PreferenceFragmentCompat { "polar_device_address", "kestrel_device_address", "trupulse_device_address", + "template_device_address" }; private ArrayList frameRateList = new ArrayList<>(); diff --git a/sensorhub-android-template/AndroidManifest.xml b/sensorhub-android-template/AndroidManifest.xml new file mode 100644 index 00000000..d2a3abe1 --- /dev/null +++ b/sensorhub-android-template/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-template/README.md b/sensorhub-android-template/README.md new file mode 100644 index 00000000..5822066c --- /dev/null +++ b/sensorhub-android-template/README.md @@ -0,0 +1,91 @@ +# Template Driver Integration + +## 1. Add the Template Module +- Duplciate the template directory +- Rename it appropriately + +## 2. Add dependency to App Module: + - In 'sensorhub-android-app' `build.gradle` we need to include the project as a dependency: +```groovy + implementation project(':sensorhub-android-template') +``` +## 3. Add Preferences UI +- In `res/xml/pref_sensors.xml`, add: +```xml + + + +``` + +- In `SensorsFragment.java`, include the "enabled" and "options" in the SWITCH_DEPENDENTS map. +- +- **Note:** If the driver uses BLE to connect you must also add the ability to select the devices 'BLE Address' (Examples: Kestrel, Trupulse, Meshtastic,+ Polar) +- Add device selection: +```xml + +``` +- In `SensorsFragment.java`, include the "template_device_address" under the BT_DEVICE_PREF_KEYS + +## 4. Update `MainActivity` +- Import the drivers Config class +`import org.sensorhub.impl.sensor.template.TemplateConfig;` +- Add to `Sensors Enum` +``` +Template +``` + +- Enable Push check +Update `isPushingSensors(Sensors sensor)`: +``` +if (Sensors.Template.equals(sensor)) { + return prefs.getBoolean("template_enabled", false) + && prefs.getStringSet("template_options", Collections.emptySet()).contains("PUSH_REMOTE"); + } +``` +- Add to updateConfig(...) +``` + // Template Driver + enabled = prefs.getBoolean("template_enabled", false); + if (enabled) { + SensorConfig templateConfig = new SensorConfig(); + templateConfig.id = "TEMPLATE_DRIVER_"; + templateConfig.name = "Template [" + deviceName + "]"; + templateConfig.autoStart = true; + templateConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; + templateConfig.uid_extension = prefs.getString("uid_extension", ""); + sensorhubConfig.add(templateConfig); + } +``` + + +### Adding External Modules (osh-addons/osh-core/...) +This is slightly different process then local modules +1. Include the module in `settings.gradle` +```groovy +'sensors/positioning/sensorhub-driver-trupulse' +``` +**>**: Ensure the module path in settings.gradle matches the project folder structure exactly, and you include the correct submodule repository + +2. Add Dependency in `sensorhub-android-lib` +```groovy +api project(':sensorhub-driver-kestrel') +``` +3. Repeat steps 3-5 in the first set of instructions \ No newline at end of file diff --git a/sensorhub-android-template/build.gradle b/sensorhub-android-template/build.gradle new file mode 100644 index 00000000..be7d7faa --- /dev/null +++ b/sensorhub-android-template/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.library' + +description = 'Template Driver' +ext.details = 'Driver template for android' +version = '1.0.0' + +dependencies { + //api 'org.sensorhub:sensorhub-core:' + oshCoreVersion + api project(':sensorhub-core') + api project(':sensorhub-android-service') + + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation project(path: ':sensorhub-driver-android') + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core:1.5.0' +} +configurations.configureEach { + exclude group: "ch.qos.logback" +} + +android { + namespace 'org.sensorhub.impl.sensor.template' + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + + lintOptions { + abortOnError false + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/main/java'] + resources.srcDirs = ['src/main/resources'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Descriptor.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Descriptor.java new file mode 100644 index 00000000..c11a90ad --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Descriptor.java @@ -0,0 +1,76 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; + + +/** + *

+ * Descriptor of Android sensors driver module for automatic discovery + * by the ModuleRegistry + *

+ * + * @author Alex Robin + * @since Sep 7, 2013 + */ +public class Descriptor implements IModuleProvider +{ + + @Override + public String getModuleName() + { + return "Template"; + } + + + @Override + public String getModuleDescription() + { + return "Driver template"; + } + + + @Override + public String getModuleVersion() + { + return "0.1"; + } + + + @Override + public String getProviderName() + { + return "GeoRobotix LLC"; + } + + + @Override + public Class> getModuleClass() + { + return Sensor.class; + } + + + @Override + public Class getModuleConfigClass() + { + return TemplateConfig.class; + } + +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java new file mode 100644 index 00000000..3aa99130 --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java @@ -0,0 +1,101 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for template WiFi access point scan results + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Output extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "wifiScan"; + private static final String SENSOR_OUTPUT_LABEL = "WiFi Access Point Scan"; + private static final Logger logger = LoggerFactory.getLogger(Output.class); + + protected Output(Sensor parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("WifiScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("text", fac.createText() + .label("Text") + .definition(SWEHelper.getPropertyUri("Text")) + .description("Example text field") + .build()) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + + public void setData(String text) { + + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, text); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java new file mode 100644 index 00000000..c094d75a --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java @@ -0,0 +1,118 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; + +import net.opengis.sensorml.v20.PhysicalComponent; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.SensorMLBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Sensor extends AbstractSensorModule { + static final String UID_PREFIX = "urn:osh:sensor:template:driver:"; + + private final ArrayList smlComponents; + private final SensorMLBuilder smlBuilder; + static final Logger logger = LoggerFactory.getLogger(Sensor.class.getSimpleName()); + private Context context; + Output output; + private HandlerThread eventThread; + private Handler eventHandler; + Thread processingThread; + volatile boolean doProcessing = true; + + + public Sensor() { + this.smlComponents = new ArrayList(); + this.smlBuilder = new SensorMLBuilder(); + } + + @Override + public void doInit() { + logger.info("Initializing Sensor"); + this.xmlID = "TEMPLATE_DRIVER_" + Build.SERIAL; + this.uniqueID = UID_PREFIX + config.getUidWithExt(); + + context = SensorHubService.getContext(); + + output = new Output(this); + output.doInit(); + addOutput(output, false); + + } + + @Override + public void doStart() throws SensorException { + eventThread = new HandlerThread("TemplateThread"); + eventThread.start(); + eventHandler = new Handler(eventThread.getLooper()); + + startProcessing(); + } + + public void startProcessing() { + doProcessing = true; + + processingThread = new Thread(() -> { + while (doProcessing) { + output.setData( "Sample Data"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + processingThread.start(); + } + + public void stopProcessing() { + doProcessing = false; + } + + @Override + public void doStop() { + + if (eventThread != null) { + eventThread.quitSafely(); + eventThread = null; + } + + eventHandler = null; + logger.info("Sensor stopped"); + } + + @Override + public boolean isConnected() { + return processingThread != null && processingThread.isAlive(); + } +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java new file mode 100644 index 00000000..ed6d5126 --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java @@ -0,0 +1,51 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import org.sensorhub.android.SensorHubService; + +import android.content.Context; +import android.provider.Settings; + + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class TemplateConfig extends org.sensorhub.api.sensor.SensorConfig +{ + + public TemplateConfig() + { + this.moduleClass = Sensor.class.getCanonicalName(); + } + public String uid_extension; + + public long scanIntervalMs = 10000; + + public static String getUid() { + Context context = SensorHubService.getContext(); + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public String getUidWithExt() + { + String baseUid = getUid(); + if (uid_extension != null && !uid_extension.isEmpty()) + return baseUid + ":" + uid_extension; + return baseUid; + } +} diff --git a/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..924ba4ab --- /dev/null +++ b/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.template.Descriptor \ No newline at end of file diff --git a/sensorhub-android-template/src/test/java/empty b/sensorhub-android-template/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-template/src/test/resources/empty b/sensorhub-android-template/src/test/resources/empty new file mode 100644 index 00000000..e69de29b From 401b85cae27026e5577f07cc2b36e359ceee7539 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 8 Apr 2026 15:22:10 -0500 Subject: [PATCH 05/26] Add devices IP addy back --- .../sensorhub/android/SettingsFragment.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index 2aa6df71..1879f152 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -1,6 +1,9 @@ package org.sensorhub.android; +import static android.content.Context.WIFI_SERVICE; + import android.content.SharedPreferences; +import android.net.wifi.WifiManager; import android.os.Bundle; import android.widget.Toast; @@ -11,10 +14,15 @@ import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreferenceCompat; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.nio.ByteOrder; + /* * Fragment for settings preferences @@ -27,6 +35,26 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref_settings, rootKey); + + WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); + int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); + + // Convert little-endian to big-endianif needed + 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 = "Unable to get IP Address"; + } + + Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); + ipAddressLabel.setSummary(ipAddressString); setupSavedServers(); setupOAuthToggle(); } From 47d161efde4e1609c95421165ceb7d243fb58f27 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 14 Apr 2026 07:20:02 -0500 Subject: [PATCH 06/26] removed video preset in config, fixed issues with build --- .../res/xml/pref_sensors.xml | 7 +-- .../res/xml/pref_settings.xml | 29 +++-------- .../org/sensorhub/android/MainActivity.java | 51 +++++-------------- .../sensorhub/android/SensorsFragment.java | 34 +++++-------- .../sensorhub/android/SettingsFragment.java | 22 ++++++++ .../sensorhub/android/SensorHubService.java | 31 +++++++++-- 6 files changed, 83 insertions(+), 91 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 7093e1c8..a0d031f3 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -166,10 +166,9 @@ app:useSimpleSummaryProvider="true" android:layout="@layout/preference_list_item"/> - - - - + - - - @@ -82,14 +79,14 @@ android:title="Username" android:selectAllOnFocus="true" android:singleLine="true" - app:useSimpleSummaryProvider="true" + android:summary="Enter username" android:layout="@layout/preference_item" /> + - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 9ea3a842..f662d8d0 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -144,7 +144,6 @@ public class MainActivity extends AppCompatActivity implements SensorHubServiceP URL url; AndroidSensorsDriver androidSensors; boolean showVideo; - URI clientUri = null; URL clientURL = null; String deviceID; @@ -249,23 +248,17 @@ public void updateConfig(SharedPreferences prefs, String runName) if (host.isEmpty()) host = "127.0.0.1"; - if (port.isEmpty()) port = "8585"; - - String newUrl = (isTLSEnabled ? "https://" : "http://") + host + ":" + port + endpointPath; + + String url = (isTLSEnabled ? "https://" : "http://") + host + ":" + port + endpointPath; try { - clientUri = new URI(newUrl); - } catch (URISyntaxException e) { - throw new RuntimeException(e); + clientURL = new URI(url).toURL(); + } catch (URISyntaxException | MalformedURLException e) { + log.error("Error: Client URL is invalid"); } - try { - clientURL = clientUri.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } boolean disableSslCheck = prefs.getBoolean("sos_disable_ssl_check", false); if (disableSslCheck) @@ -306,6 +299,7 @@ public boolean verify(String arg0, SSLSession arg1) { String tokenEndpoint = prefs.getString("token_endpoint", "").trim(); String clientSecret = prefs.getString("client_secret", "").trim(); + String deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); String deviceName = prefs.getString("device_name", null); if (deviceName == null || deviceName.length() < 2) @@ -334,32 +328,13 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.videoConfig.codec = prefs.getString("video_codec", VideoEncoderConfig.JPEG_CODEC); sensorsConfig.videoConfig.frameRate = Integer.parseInt(prefs.getString("video_framerate", "30")); - String selectedPreset = prefs.getString("video_preset", "0"); - if ("AUTO".equals(selectedPreset)) { - sensorsConfig.videoConfig.autoPreset = true; - sensorsConfig.videoConfig.selectedPreset = 0; - } - else { - sensorsConfig.videoConfig.autoPreset = false; - sensorsConfig.videoConfig.selectedPreset = Integer.parseInt(selectedPreset); - } - - int resIdx = 1; - ArrayList presetList = new ArrayList<>(); - while (prefs.contains("video_size" + resIdx)) - { - String resString = prefs.getString("video_size" + resIdx, "Disabled"); - String[] tokens = resString.split("x"); - VideoPreset preset = new VideoPreset(); - preset.width = Integer.parseInt(tokens[0]); - preset.height = Integer.parseInt(tokens[1]); - preset.minBitrate = Integer.parseInt(prefs.getString("video_min_bitrate" + resIdx, "3000")); - preset.maxBitrate = Integer.parseInt(prefs.getString("video_max_bitrate" + resIdx, "3000")); - preset.selectedBitrate = preset.maxBitrate; - presetList.add(preset); - resIdx++; - } - sensorsConfig.videoConfig.presets = presetList.toArray(new VideoPreset[0]); + String resolutionStr = prefs.getString("video_resolution", "640x480"); + String[] resParts = resolutionStr.split("x"); + VideoPreset videoPreset = new VideoPreset(); + videoPreset.width = Integer.parseInt(resParts[0]); + videoPreset.height = Integer.parseInt(resParts[1]); + sensorsConfig.videoConfig.presets = new VideoPreset[]{videoPreset}; + sensorsConfig.videoConfig.selectedPreset = 0; sensorsConfig.outputVideoRoll = prefs.getBoolean("video_roll_enabled", false); diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index 766ce7f8..af718397 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -43,7 +43,7 @@ public class SensorsFragment extends PreferenceFragmentCompat { {"orient_euler_enabled","orient_euler_options"}, {"gps_enabled", "gps_options"}, {"netloc_enabled", "netloc_options"}, - {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_preset", "camera_select"}, + {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_resolution", "camera_select"}, {"video_roll_enabled", "video_roll_options"}, {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"}, {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"}, @@ -75,7 +75,6 @@ public class SensorsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { setPreferencesFromResource(R.xml.pref_sensors, rootKey); - // Wire up switch visibility toggling for (String[] group : SWITCH_DEPENDENTS) { String switchKey = group[0]; SwitchPreferenceCompat switchPref = findPreference(switchKey); @@ -97,11 +96,9 @@ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable S }); } - // Populate video and audio preference lists dynamically setupVideoPreferences(); setupAudioPreferences(); - // Wire up Bluetooth device picker for all BLE device preferences setupBluetoothDevicePickers(); } @@ -112,7 +109,6 @@ private void setupBluetoothDevicePickers() { Preference pref = findPreference(key); if (pref == null) continue; - // Show the currently saved address in the summary String saved = prefs.getString(key, ""); if (!saved.isEmpty()) { pref.setSummary(saved); @@ -141,7 +137,6 @@ private void showDevicePickerDialog(String prefKey) { } } - // Add manual entry option at the end names.add("Enter name or address manually..."); addresses.add(null); @@ -151,7 +146,6 @@ private void showDevicePickerDialog(String prefKey) { .setTitle("Select Device") .setItems(displayNames, (dialog, which) -> { if (addresses.get(which) == null) { - // Manual entry showManualAddressDialog(prefKey); } else { saveDeviceAddress(prefKey, addresses.get(which), displayNames[which]); @@ -166,7 +160,6 @@ private void showManualAddressDialog(String prefKey) { input.setInputType(InputType.TYPE_CLASS_TEXT); input.setHint("e.g. Ballistic or AA:BB:CC:DD:EE:FF"); - // Load current value if any SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); String current = prefs.getString(prefKey, ""); if (!current.isEmpty()) { @@ -246,19 +239,13 @@ private void setupVideoPreferences() { frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0])); } - // Preset list - ListPreference selectedPresetList = findPreference("video_preset"); - if (selectedPresetList != null) { - ArrayList presetNames = new ArrayList<>(); - ArrayList presetIndexes = new ArrayList<>(); - for (int i = 0; i < 5; i++) { - presetNames.add("Video Preset #" + (i + 1)); - presetIndexes.add(String.valueOf(i)); - } - presetNames.add("Auto select"); - presetIndexes.add("AUTO"); - selectedPresetList.setEntries(presetNames.toArray(new String[0])); - selectedPresetList.setEntryValues(presetIndexes.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)); } } @@ -279,6 +266,11 @@ private void updateCameraSettings(int cameraId) { 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); } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index 1879f152..9dd72033 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -5,8 +5,13 @@ import android.content.SharedPreferences; import android.net.wifi.WifiManager; import android.os.Bundle; +import android.text.InputFilter; +import android.text.InputType; +import android.text.method.PasswordTransformationMethod; +import android.widget.EditText; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.preference.EditTextPreference; import androidx.preference.Preference; @@ -55,6 +60,23 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); ipAddressLabel.setSummary(ipAddressString); + + + EditTextPreference passwordPref = findPreference("password"); + if (passwordPref != null) { + passwordPref.setSummaryProvider(pref -> "••••••••"); + passwordPref.setOnBindEditTextListener(editText -> + editText.setTransformationMethod(PasswordTransformationMethod.getInstance()) + ); + } + + EditTextPreference secretPref = findPreference("client_secret"); + if (secretPref != null) { + secretPref.setSummaryProvider(pref -> "••••••••"); + secretPref.setOnBindEditTextListener(editText -> + editText.setTransformationMethod(PasswordTransformationMethod.getInstance()) + ); + } setupSavedServers(); setupOAuthToggle(); } diff --git a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java index 5aab62bb..725bdf8a 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java +++ b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java @@ -1,5 +1,6 @@ package org.sensorhub.android; +import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -18,17 +19,24 @@ import android.os.IBinder; import android.os.PowerManager; import android.os.Process; +import android.os.SystemClock; + +import com.ctc.wstx.stax.WstxInputFactory; +import com.ctc.wstx.stax.WstxOutputFactory; import org.sensorhub.api.common.SensorHubException; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.impl.SensorHub; import org.sensorhub.impl.SensorHubConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.vast.xml.XMLImplFinder; import javax.xml.parsers.DocumentBuilderFactory; public class SensorHubService extends Service { + private static final Logger log = LoggerFactory.getLogger(SensorHubService.class); final IBinder binder = new LocalBinder(); private HandlerThread msgThread; private Handler msgHandler; @@ -69,8 +77,8 @@ public void onCreate() { //Dexter.loadFromAssets(this.getApplicationContext(), "stax-api-1.0-2.dex"); // set default StAX implementation - XMLImplFinder.setStaxInputFactory(com.ctc.wstx.stax.WstxInputFactory.class.newInstance()); - XMLImplFinder.setStaxOutputFactory(com.ctc.wstx.stax.WstxOutputFactory.class.newInstance()); + XMLImplFinder.setStaxInputFactory(WstxInputFactory.class.newInstance()); + XMLImplFinder.setStaxOutputFactory(WstxOutputFactory.class.newInstance()); // set default DOM implementation DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -179,7 +187,7 @@ public void run() { try { sensorhub.start(); } catch (SensorHubException e) { - e.printStackTrace(); + log.error("Error starting SensorHub: "+ e.getMessage()); // Release locks if startup fails releaseWakeLocks(); } @@ -203,7 +211,7 @@ private void acquireWakeLocks() { .getSystemService(Context.WIFI_SERVICE); if (wifiManager != null && wifiLock == null) { wifiLock = wifiManager.createWifiLock( - WifiManager.WIFI_MODE_FULL_HIGH_PERF, + WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "SensorHub::WiFiLock" ); wifiLock.acquire(); @@ -276,6 +284,21 @@ public void onDestroy() super.onDestroy(); } + @Override + public void onTaskRemoved(Intent rootIntent) { + log.info("Task removed, scheduling restart"); + Intent restartIntent = new Intent(getApplicationContext(), SensorHubService.class); + PendingIntent pendingIntent = PendingIntent.getService( + getApplicationContext(), 1, restartIntent, + PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE + ); + AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE); + if (alarmManager != null) { + alarmManager.set(AlarmManager.ELAPSED_REALTIME, + SystemClock.elapsedRealtime() + 1000, pendingIntent); + } + super.onTaskRemoved(rootIntent); + } @Override public IBinder onBind(Intent intent) From 92437c8c539cc915d28df1af33088582cfc2e70a Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 14 Apr 2026 08:02:38 -0500 Subject: [PATCH 07/26] Update DashboardFragment.java --- .../org/sensorhub/android/DashboardFragment.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index 23fd3ff1..4a1f501c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -29,6 +29,8 @@ import com.google.android.material.textfield.TextInputLayout; import android.graphics.drawable.GradientDrawable; +import android.widget.Toast; + import androidx.core.content.ContextCompat; import org.sensorhub.api.event.Event; @@ -135,6 +137,7 @@ private void updateFabIcon() { } private void stopHub() { + Toast.makeText(requireContext(), "Stopping SensorHub", Toast.LENGTH_SHORT).show(); stopRefreshingStatus(); provider.stopSensorHub(); updateFabIcon(); @@ -144,7 +147,6 @@ private void stopHub() { requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } - // ==================== Run Name Popup ==================== protected synchronized void showRunNamePopup() { MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireContext()); @@ -185,6 +187,7 @@ public void onClick(DialogInterface dialog, int whichButton) { showVideoConfigErrorPopup(); newStatusMessage("Video Config Error: Check Settings"); } else { + Toast.makeText(requireContext(), "Starting SensorHub...", Toast.LENGTH_SHORT).show(); newStatusMessage("Starting SensorHub..."); provider.getSostClients().clear(); provider.getConSysClients().clear(); @@ -192,12 +195,14 @@ public void onClick(DialogInterface dialog, int whichButton) { SensorHubService service = provider.getBoundService(); + //todo: fix this while (service.getSensorHub() == null) { System.out.println("Waiting for BoundService Hub to start..."); } while (service.getSensorHub().getEventBus() == null) { System.out.println("Waiting for BoundService Hub EventBus to start..."); } + // todo: EventBus shEvtBus = (EventBus) service.getSensorHub().getEventBus(); shEvtBus.newSubscription() @@ -220,8 +225,6 @@ protected void showVideoConfigErrorPopup() { .show(); } - // ==================== Status Display ==================== - protected void startRefreshingStatus() { if (displayCallback != null) return; @@ -292,7 +295,6 @@ protected synchronized void displayStatus() { } } - // Stream statuses mainInfoText.append("

"); for (SOSTClient client : provider.getSostClients()) { mainInfoText.append("SOS-T Client

"); @@ -355,7 +357,6 @@ else if (dt > stream.getValue().measPeriodMs) mainInfoText.append("No Sensors Set to Push Remotely"); } - // video info — update status card AndroidSensorsDriver sensors = provider.getAndroidSensors(); SensorHubService service = provider.getBoundService(); if (sensors != null && service != null && service.hasVideo()) { @@ -380,8 +381,6 @@ protected synchronized void newStatusMessage(String msg) { displayHandler.post(() -> mainInfoArea.setText(mainInfoText.toString())); } - // ==================== Video ==================== - private void updateVideoStatusCard() { SensorHubService service = provider.getBoundService(); boolean hasVideo = service != null && service.hasVideo(); @@ -392,7 +391,6 @@ private void updateVideoStatusCard() { videoInfoArea.setText(videoInfoText.toString()); } - // Update the status dot color (green = streaming) if (videoStatusDot != null && videoStatusDot.getBackground() instanceof GradientDrawable) { GradientDrawable dot = (GradientDrawable) videoStatusDot.getBackground(); int color = ContextCompat.getColor(requireContext(), From 147e7471183e4964acabc88f82762f573f4b79c1 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 16 Apr 2026 07:57:40 -0500 Subject: [PATCH 08/26] updated --- .../src/org/sensorhub/android/MainActivity.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index f662d8d0..25863753 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -523,13 +523,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(controllerConfig); } - if (isApiServiceEnabled) { - sensorhubConfig.add(conSysApiService); - } - if (isSosServiceEnabled) { - sensorhubConfig.add(sosConfig); - } - // Template Driver enabled = prefs.getBoolean("template_enabled", false); if (enabled) { @@ -542,6 +535,13 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(templateConfig); } + + if (isApiServiceEnabled) { + sensorhubConfig.add(conSysApiService); + } + if (isSosServiceEnabled) { + sensorhubConfig.add(sosConfig); + } } protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) From d69bcc95e19e596b03b772c8f6dbdfb9a2d27c1d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 15 Apr 2026 07:54:18 -0500 Subject: [PATCH 09/26] added discovery service, need to update with paths to rule.txt # Conflicts: # sensorhub-android-app/src/org/sensorhub/android/MainActivity.java --- .gitmodules | 3 + sensorhub-android-app/build.gradle | 1 + .../res/layout/activity_app_status.xml | 50 +++++++++++++++++ .../res/values/strings_app_status.xml | 1 + .../res/xml/pref_settings.xml | 7 +++ .../sensorhub/android/AppStatusActivity.java | 4 ++ .../org/sensorhub/android/MainActivity.java | 55 ++++++++++++++++--- settings.gradle | 3 + submodules/botts-addons | 1 + 9 files changed, 118 insertions(+), 7 deletions(-) create mode 160000 submodules/botts-addons diff --git a/.gitmodules b/.gitmodules index 189cbdf3..437f68a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = submodules/osh-core url = git@github.com:kalynstricklin/osh-core.git branch = update-moduleutils +[submodule "submodules/botts-addons"] + path = submodules/botts-addons + url = git@github.com:Botts-Innovative-Research/botts-addons.git diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 986801df..f4899e8f 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -24,6 +24,7 @@ dependencies { implementation project(path: ':sensorhub-datastore-h2') implementation project(path: ':sensorhub-service-consys') + implementation project(path: ':sensorhub-service-discovery') implementation project(':sensorhub-android-ste') implementation project(':sensorhub-android-meshtastic') implementation project(':sensorhub-android-polar') diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml index 0b735b5b..0f9ef588 100644 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -137,6 +137,56 @@ + + + + + + + + + + + + + + + + + SOS Service Status ConSys Service Status + Discovery Service Status HTTP Server Status Android Sensor Status Android Sensor Storage Status diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index cd0433cb..d3d2db98 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -119,6 +119,13 @@ android:defaultValue="true" android:layout="@layout/preference_switch_item" /> + + diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java index b214732a..a5a9a675 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java @@ -28,6 +28,7 @@ protected void onCreate(Bundle savedInstanceState) { 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"); @@ -35,12 +36,14 @@ protected void onCreate(Bundle savedInstanceState) { // Set status text 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); @@ -48,6 +51,7 @@ protected void onCreate(Bundle savedInstanceState) { // Color the status indicator dots 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); diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 25863753..927bfb0c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -14,6 +14,8 @@ package org.sensorhub.android; +import static org.sensorhub.android.SensorHubService.context; + import android.Manifest; import android.annotation.SuppressLint; import android.app.AlertDialog; @@ -47,6 +49,8 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; +import com.botts.impl.service.discovery.DiscoveryService; +import com.botts.impl.service.discovery.DiscoveryServiceConfig; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomnavigation.BottomNavigationView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -104,7 +108,11 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -243,6 +251,7 @@ public void updateConfig(SharedPreferences prefs, String runName) Boolean isApiServiceEnabled = prefs.getBoolean("api_service", true); Boolean isSosServiceEnabled = prefs.getBoolean("sos_service", true); + Boolean isDiscoveryServiceEnabled = prefs.getBoolean("discovery_service", true); Boolean isClientEnabled = prefs.getBoolean("enable_client", true); Boolean isTLSEnabled = prefs.getBoolean("enable_tls", false); @@ -372,6 +381,30 @@ public boolean verify(String arg0, SSLSession arg1) { 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; + + InputStream inputStream = context.getResources().openRawResource(R.raw.rules); + File outFile = new File(context.getFilesDir(), "rules.txt"); + try (OutputStream outputStream = new FileOutputStream(outFile)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + outputStream.flush(); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + discoveryServiceConfig.rulesFilePath = outFile.getAbsolutePath(); + + // OAuth ConSysOAuthConfig conSysOAuthConfig = new ConSysOAuthConfig(); conSysOAuthConfig.oAuthEnabled = isOAuthEnabled; conSysOAuthConfig.tokenEndpoint = tokenEndpoint; @@ -523,6 +556,18 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(controllerConfig); } + //---------- SERVICES --------------------- + if (isApiServiceEnabled) { + sensorhubConfig.add(conSysApiService); + } + if (isSosServiceEnabled) { + sensorhubConfig.add(sosConfig); + } + if (isDiscoveryServiceEnabled) { + sensorhubConfig.add(discoveryServiceConfig); + } + + // Template Driver enabled = prefs.getBoolean("template_enabled", false); if (enabled) { @@ -535,13 +580,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(templateConfig); } - - if (isApiServiceEnabled) { - sensorhubConfig.add(conSysApiService); - } - if (isSosServiceEnabled) { - sensorhubConfig.add(sosConfig); - } } protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) @@ -691,6 +729,9 @@ else if(id == R.id.action_status) { 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; diff --git a/settings.gradle b/settings.gradle index 8a7c6248..d9ac7b45 100644 --- a/settings.gradle +++ b/settings.gradle @@ -32,6 +32,9 @@ def repos = [ 'sensors/health/sensorhub-driver-angelsensor', 'processing/sensorhub-process-vecmath', 'processing/sensorhub-process-geoloc' + ], + 'botts-addons' : [ + 'services/sensorhub-service-discovery' ] ] diff --git a/submodules/botts-addons b/submodules/botts-addons new file mode 160000 index 00000000..7381f8c0 --- /dev/null +++ b/submodules/botts-addons @@ -0,0 +1 @@ +Subproject commit 7381f8c06d7cdbaa03aa9a66f4bc6451cb1d712b From 8ad0df3458e69623d10416f8d65f6859bbb8d4e4 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 16 Apr 2026 08:41:55 -0500 Subject: [PATCH 10/26] update the discovery service rules to download from a link --- .../res/xml/pref_settings.xml | 7 +++ .../org/sensorhub/android/MainActivity.java | 57 +++++++++++-------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index d3d2db98..d7b52ddd 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -126,6 +126,13 @@ android:defaultValue="true" android:layout="@layout/preference_switch_item" /> + diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 927bfb0c..779e316c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -113,6 +113,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -126,6 +127,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.concurrent.FutureTask; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -388,19 +390,29 @@ public boolean verify(String arg0, SSLSession arg1) { discoveryServiceConfig.name= "Discovery Service"; discoveryServiceConfig.autoStart = true; - InputStream inputStream = context.getResources().openRawResource(R.raw.rules); File outFile = new File(context.getFilesDir(), "rules.txt"); - try (OutputStream outputStream = new FileOutputStream(outFile)) { - byte[] buffer = new byte[1024]; - int length; - while ((length = inputStream.read(buffer)) > 0) { - outputStream.write(buffer, 0, length); + 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(); } - outputStream.flush(); - } catch (FileNotFoundException e) { - throw new RuntimeException(e); - } catch (IOException e) { - throw new RuntimeException(e); + 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(); @@ -556,18 +568,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(controllerConfig); } - //---------- SERVICES --------------------- - if (isApiServiceEnabled) { - sensorhubConfig.add(conSysApiService); - } - if (isSosServiceEnabled) { - sensorhubConfig.add(sosConfig); - } - if (isDiscoveryServiceEnabled) { - sensorhubConfig.add(discoveryServiceConfig); - } - - // Template Driver enabled = prefs.getBoolean("template_enabled", false); if (enabled) { @@ -580,6 +580,17 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(templateConfig); } + //---------- SERVICES --------------------- + if (isApiServiceEnabled) { + sensorhubConfig.add(conSysApiService); + } + if (isSosServiceEnabled) { + sensorhubConfig.add(sosConfig); + } + if (isDiscoveryServiceEnabled) { + sensorhubConfig.add(discoveryServiceConfig); + } + } protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) From e50f127dd181ff9c024bbb9f1cbfda84f7f3c5a6 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 16 Apr 2026 08:45:04 -0500 Subject: [PATCH 11/26] Update MainActivity.java --- .../src/org/sensorhub/android/MainActivity.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 779e316c..2eed33f9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -108,7 +108,6 @@ import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; From 99ff66be9c4b016206475ef630538ff345165062 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 16 Apr 2026 09:41:52 -0500 Subject: [PATCH 12/26] Added username, password and endpoint to the saved settings, fixed summaries and visibility of passwords --- .../res/xml/pref_settings.xml | 9 +- .../sensorhub/android/SettingsFragment.java | 94 ++++++++++++------- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index d7b52ddd..29d7cca4 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -79,14 +79,15 @@ android:title="Username" android:selectAllOnFocus="true" android:singleLine="true" - android:summary="Enter username" + app:useSimpleSummaryProvider="true" + android:summary="Enter your username or leave blank" android:layout="@layout/preference_item" /> diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index 9dd72033..423c1202 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -5,13 +5,9 @@ import android.content.SharedPreferences; import android.net.wifi.WifiManager; import android.os.Bundle; -import android.text.InputFilter; -import android.text.InputType; import android.text.method.PasswordTransformationMethod; -import android.widget.EditText; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.preference.EditTextPreference; import androidx.preference.Preference; @@ -19,6 +15,9 @@ import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreferenceCompat; +import org.json.JSONException; +import org.json.JSONObject; + import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; @@ -64,18 +63,12 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { EditTextPreference passwordPref = findPreference("password"); if (passwordPref != null) { - passwordPref.setSummaryProvider(pref -> "••••••••"); - passwordPref.setOnBindEditTextListener(editText -> - editText.setTransformationMethod(PasswordTransformationMethod.getInstance()) - ); + passwordPref.setSummaryProvider(pref -> "•••••••"); } EditTextPreference secretPref = findPreference("client_secret"); if (secretPref != null) { secretPref.setSummaryProvider(pref -> "••••••••"); - secretPref.setOnBindEditTextListener(editText -> - editText.setTransformationMethod(PasswordTransformationMethod.getInstance()) - ); } setupSavedServers(); setupOAuthToggle(); @@ -123,10 +116,15 @@ private void putSavedServers(Set servers) { } private String getDisplayName(String entry) { - // entry format: "ip|port|name" - String[] parts = entry.split("\\|", 3); - if (parts.length >= 3) return parts[2] + " (" + parts[0] + ":" + parts[1] + ")"; - return entry; + try { + JSONObject obj = new JSONObject(entry); + String name = obj.optString("name"); + String ip = obj.optString("ip"); + String port = obj.optString("port"); + return name + " (" + ip + ":" + port + ")"; + } catch (JSONException e) { + return entry; + } } private void updateSavedServersSummary(Preference pref) { @@ -143,6 +141,9 @@ private void saveCurrentServer() { String name = prefs.getString("server_name", "").trim(); String ip = prefs.getString("ip_address", "").trim(); String port = prefs.getString("port", "").trim(); + String username = prefs.getString("username", "").trim(); + String password = prefs.getString("password", "").trim(); + String endpoint = prefs.getString("endpoint_path", "").trim(); if (ip.isEmpty() || port.isEmpty()) { Toast.makeText(requireContext(), "Server address and port are required", Toast.LENGTH_SHORT).show(); @@ -153,19 +154,34 @@ private void saveCurrentServer() { name = ip + ":" + port; } - String entry = ip + "|" + port + "|" + name; + JSONObject obj = new JSONObject(); + try { + obj.put("ip", ip); + obj.put("port", port); + obj.put("name", name); + obj.put("username", username); + obj.put("password", password); + obj.put("endpoint", endpoint); + } catch (JSONException e) { + e.printStackTrace(); + return; + } Set servers = new HashSet<>(getSavedServers()); - // Check for duplicate ip:port - String finalIp = ip; - String finalPort = port; + // remove duplicates (ip + port + endpoint) servers.removeIf(s -> { - String[] parts = s.split("\\|", 3); - return parts.length >= 2 && parts[0].equals(finalIp) && parts[1].equals(finalPort); + try { + JSONObject existing = new JSONObject(s); + return existing.optString("ip").equals(ip) && + existing.optString("port").equals(port) && + existing.optString("endpoint").equals(endpoint); + } catch (JSONException e) { + return false; + } }); - servers.add(entry); + servers.add(obj.toString()); putSavedServers(servers); Preference selectPref = findPreference("saved_servers"); @@ -189,18 +205,28 @@ private void showSelectServerDialog() { new AlertDialog.Builder(requireContext()) .setTitle("Select Server") .setItems(displayNames, (dialog, which) -> { - String[] parts = servers.get(which).split("\\|", 3); - if (parts.length < 3) return; - - EditTextPreference ipPref = findPreference("ip_address"); - EditTextPreference portPref = findPreference("port"); - EditTextPreference namePref = findPreference("server_name"); - - if (ipPref != null) ipPref.setText(parts[0]); - if (portPref != null) portPref.setText(parts[1]); - if (namePref != null) namePref.setText(parts[2]); - - Toast.makeText(requireContext(), "Loaded: " + parts[2], Toast.LENGTH_SHORT).show(); + try { + JSONObject obj = new JSONObject(servers.get(which)); + + EditTextPreference ipPref = findPreference("ip_address"); + EditTextPreference portPref = findPreference("port"); + EditTextPreference namePref = findPreference("server_name"); + EditTextPreference usernamePref = findPreference("username"); + EditTextPreference passwordPref = findPreference("password"); + EditTextPreference endpointPref = findPreference("endpoint_path"); + + if (ipPref != null) ipPref.setText(obj.optString("ip")); + if (portPref != null) portPref.setText(obj.optString("port")); + if (namePref != null) namePref.setText(obj.optString("name")); + if (usernamePref != null) usernamePref.setText(obj.optString("username")); + if (passwordPref != null) passwordPref.setText(obj.optString("password")); + if (endpointPref != null) endpointPref.setText(obj.optString("endpoint")); + + Toast.makeText(requireContext(), "Loaded: " + obj.optString("name"), Toast.LENGTH_SHORT).show(); + + } catch (JSONException e) { + Toast.makeText(requireContext(), "Failed to load server", Toast.LENGTH_SHORT).show(); + } }) .setNegativeButton("Cancel", null) .show(); From 4b24af8985ebd30e8e920d9ae16482471d7024df Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 16 Apr 2026 16:09:40 -0500 Subject: [PATCH 13/26] Add secure prefs, collapse datastream statuses --- .../res/drawable/ic_expand_less.xml | 9 + .../res/drawable/ic_expand_more.xml | 9 + .../res/layout/fragment_dashboard.xml | 202 +++++++++++------- .../res/xml/pref_settings.xml | 2 +- .../sensorhub/android/DashboardFragment.java | 98 +++++++-- .../org/sensorhub/android/MainActivity.java | 45 ++-- .../org/sensorhub/android/SecurePrefs.java | 112 ++++++++++ .../sensorhub/android/SettingsFragment.java | 63 ++++-- .../sensorhub/android/SensorHubService.java | 50 ++--- 9 files changed, 439 insertions(+), 151 deletions(-) create mode 100644 sensorhub-android-app/res/drawable/ic_expand_less.xml create mode 100644 sensorhub-android-app/res/drawable/ic_expand_more.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java diff --git a/sensorhub-android-app/res/drawable/ic_expand_less.xml b/sensorhub-android-app/res/drawable/ic_expand_less.xml new file mode 100644 index 00000000..1e92d2b9 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_expand_less.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_expand_more.xml b/sensorhub-android-app/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..cf9708d0 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_expand_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml index b0399792..1381044f 100644 --- a/sensorhub-android-app/res/layout/fragment_dashboard.xml +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -16,95 +16,149 @@ android:layout_gravity="center" android:visibility="gone" /> - - - - - + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - + android:layout_marginStart="@dimen/info_card_margin" + android:layout_marginEnd="@dimen/info_card_margin" + android:layout_marginTop="@dimen/info_card_margin" + android:visibility="gone" + app:cardCornerRadius="@dimen/card_corner_radius" + app:cardElevation="@dimen/card_elevation" + app:cardBackgroundColor="@color/md_theme_background" + app:strokeColor="@color/status_started" + app:strokeWidth="1dp"> - - + + + + - - + + + + + + + + + app:backgroundTint="@color/md_theme_primaryContainer" + app:cornerRadius="18dp" /> - + + + + + + + + + + + + + + + + + + + + + - + - + diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index 29d7cca4..bf539055 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -62,7 +62,7 @@ diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index 4a1f501c..f9e56fff 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -13,6 +13,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.ImageButton; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.TextView; @@ -28,12 +29,16 @@ import com.google.android.material.textfield.TextInputEditText; import com.google.android.material.textfield.TextInputLayout; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; import android.graphics.drawable.GradientDrawable; import android.widget.Toast; import androidx.core.content.ContextCompat; import org.sensorhub.api.event.Event; +import org.sensorhub.api.module.IModule; import org.sensorhub.api.module.ModuleEvent; import org.sensorhub.impl.client.sost.SOSTClient; import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; @@ -62,6 +67,8 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe private MaterialButton btnToggleVideo; private View videoStatusDot; private FloatingActionButton fab; + private ImageButton btnToggleStatus; + private View mainInfoScroll; private Handler displayHandler; private Runnable displayCallback; private StringBuffer mainInfoText = new StringBuffer(); @@ -69,6 +76,7 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe private Flow.Subscription subscription; private SensorHubServiceProvider provider; private boolean videoPreviewVisible = false; + private boolean statusExpanded = true; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -99,10 +107,14 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat btnToggleVideo.setOnClickListener(v -> toggleVideoPreview()); + btnToggleStatus = view.findViewById(R.id.btn_toggle_status); + btnToggleStatus.setOnClickListener(v -> toggleStatusExpanded()); + mainInfoScroll = view.findViewById(R.id.main_info_scroll); + fab = view.findViewById(R.id.fab_toggle); fab.setOnClickListener(v -> { if (!provider.isOshStarted()) { - if (provider.getBoundService() != null && provider.getBoundService().getSensorHub() == null) + if (provider.getBoundService() != null) showRunNamePopup(); } else { stopHub(); @@ -142,11 +154,21 @@ private void stopHub() { provider.stopSensorHub(); updateFabIcon(); hideVideoPreview(); + clearTextureView(); videoStatusCard.setVisibility(View.GONE); newStatusMessage("SensorHub Stopped"); requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } + private void clearTextureView() { + if (textureView == null || textureView.getSurfaceTexture() == null) return; + Canvas canvas = textureView.lockCanvas(); + if (canvas != null) { + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + textureView.unlockCanvasAndPost(canvas); + } + } + protected synchronized void showRunNamePopup() { MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireContext()); @@ -193,21 +215,7 @@ public void onClick(DialogInterface dialog, int whichButton) { provider.getConSysClients().clear(); provider.startSensorHub(); - SensorHubService service = provider.getBoundService(); - - //todo: fix this - while (service.getSensorHub() == null) { - System.out.println("Waiting for BoundService Hub to start..."); - } - while (service.getSensorHub().getEventBus() == null) { - System.out.println("Waiting for BoundService Hub EventBus to start..."); - } - // todo: - - EventBus shEvtBus = (EventBus) service.getSensorHub().getEventBus(); - shEvtBus.newSubscription() - .withTopicID(ModuleRegistry.EVENT_GROUP_ID) - .subscribe(DashboardFragment.this); + waitForHubReady(); } } }); @@ -216,6 +224,54 @@ public void onClick(DialogInterface dialog, int whichButton) { alert.show(); } + private static final int HUB_POLL_INTERVAL_MS = 200; + private static final int HUB_POLL_MAX_ATTEMPTS = 150; + private int hubPollAttempts = 0; + + private void waitForHubReady() { + hubPollAttempts = 0; + displayHandler.post(this::pollHubReady); + } + + private void pollHubReady() { + if (!isAdded()) return; + + SensorHubService service = provider.getBoundService(); + hubPollAttempts++; + + if (service != null && service.getSensorHub() != null && service.getSensorHub().getEventBus() != null) { + EventBus shEvtBus = (EventBus) service.getSensorHub().getEventBus(); + shEvtBus.newSubscription() + .withTopicID(ModuleRegistry.EVENT_GROUP_ID) + .subscribe(DashboardFragment.this); + + ModuleRegistry registry = (ModuleRegistry) service.getSensorHub().getModuleRegistry(); + for (IModule module : registry.getLoadedModules()) { + if (module instanceof SOSTClient) { + provider.getSostClients().add((SOSTClient) module); + } else if (module instanceof ConSysApiClientModule) { + provider.getConSysClients().add((ConSysApiClientModule) module); + } else if (module instanceof AndroidSensorsDriver) { + provider.setAndroidSensors((AndroidSensorsDriver) module); + } + } + + if (!provider.isOshStarted()) { + provider.setOshStarted(true); + updateFabIcon(); + startRefreshingStatus(); + updateVideoStatusCard(); + if (videoPreviewVisible) + showVideo(); + } + } else if (hubPollAttempts < HUB_POLL_MAX_ATTEMPTS) { + displayHandler.postDelayed(this::pollHubReady, HUB_POLL_INTERVAL_MS); + } else { + newStatusMessage("SensorHub failed to start"); + updateFabIcon(); + } + } + protected void showVideoConfigErrorPopup() { String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; new MaterialAlertDialogBuilder(requireContext()) @@ -372,6 +428,8 @@ else if (dt > stream.getValue().measPeriodMs) // ignore display errors } updateVideoStatusCard(); + if (videoPreviewVisible) + showVideo(); } } @@ -399,6 +457,12 @@ private void updateVideoStatusCard() { } } + private void toggleStatusExpanded() { + statusExpanded = !statusExpanded; + mainInfoScroll.setVisibility(statusExpanded ? View.VISIBLE : View.GONE); + btnToggleStatus.setImageResource(statusExpanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); + } + private void toggleVideoPreview() { videoPreviewVisible = !videoPreviewVisible; if (videoPreviewVisible) { @@ -420,7 +484,7 @@ private void hideVideoPreview() { protected void showVideo() { SensorHubService service = provider.getBoundService(); - if (service != null && service.getVideoTexture() != null) { + if (service != null && service.getVideoTexture() != null && !service.getVideoTexture().isReleased()) { if (textureView.getSurfaceTexture() != service.getVideoTexture()) textureView.setSurfaceTexture(service.getVideoTexture()); } diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 2eed33f9..46a57290 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -244,10 +244,10 @@ public void updateConfig(SharedPreferences prefs, String runName) deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); sensorhubConfig = new InMemoryConfigDb(new ModuleClassFinder()); - String host = prefs.getString("ip_address", "").trim(); - String port = prefs.getString("port", "").trim(); + String host = prefs.getString("ip_address", "127.0.0.1").trim(); + String portStr = prefs.getString("port", "8080").trim(); String user = prefs.getString("username", null); - String password = prefs.getString("password", null); + String password = SecurePrefs.get(this, "password", null); String endpointPath = prefs.getString("endpoint_path", null); Boolean isApiServiceEnabled = prefs.getBoolean("api_service", true); @@ -256,17 +256,34 @@ public void updateConfig(SharedPreferences prefs, String runName) Boolean isClientEnabled = prefs.getBoolean("enable_client", true); Boolean isTLSEnabled = prefs.getBoolean("enable_tls", false); - if (host.isEmpty()) + if (host == null || host.isEmpty()) host = "127.0.0.1"; - if (port.isEmpty()) - port = "8585"; - - String url = (isTLSEnabled ? "https://" : "http://") + host + ":" + port + endpointPath; + host = host.replace("http://", "").replace("https://", ""); + int port; try { - clientURL = new URI(url).toURL(); + port = Integer.parseInt(portStr); + if (port < 1 || port > 65535) { + port = 8080; + } + } catch (NumberFormatException e) { + port = 8080; + } + + if (endpointPath.isEmpty()) { + endpointPath = ""; + } else if (!endpointPath.startsWith("/")) { + endpointPath = "/" + endpointPath; + } + + String urlStr = (isTLSEnabled ? "https://" : "http://") + + host + ":" + port + endpointPath; + + try { + clientURL = new URI(urlStr).toURL(); } catch (URISyntaxException | MalformedURLException e) { - log.error("Error: Client URL is invalid"); + log.error("Invalid URL: " + urlStr, e); + clientURL = null; } @@ -305,9 +322,9 @@ public boolean verify(String arg0, SSLSession arg1) { // OAuth Boolean isOAuthEnabled = prefs.getBoolean("o_auth_enabled", false); - String clientId = prefs.getString("client_id", "").trim(); - String tokenEndpoint = prefs.getString("token_endpoint", "").trim(); - String clientSecret = prefs.getString("client_secret", "").trim(); + String clientId = SecurePrefs.get(this, "client_id", "").trim(); + String tokenEndpoint = SecurePrefs.get(this, "token_endpoint", "").trim(); + String clientSecret = SecurePrefs.get(this, "client_secret", "").trim(); String deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); @@ -875,8 +892,6 @@ protected void showAboutPopup() { alert.show(); } - // ======================================== - boolean isPushingSensor(Sensors sensor) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); diff --git a/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java new file mode 100644 index 00000000..042ea734 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java @@ -0,0 +1,112 @@ +package org.sensorhub.android; + +import android.content.Context; +import android.content.SharedPreferences; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyProperties; +import android.util.Base64; + +import androidx.preference.PreferenceManager; + +import java.nio.charset.StandardCharsets; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; + +public class SecurePrefs { + private static final String KEY_ALIAS = "osh_android_secure_key"; + private static final String SECURE_PREFS_NAME = "osh_secure_prefs"; + + private static final Set SENSITIVE_KEYS = new HashSet<>(Arrays.asList( + "password", "client_secret", "token_endpoint", "client_id" + )); + + private static SecretKey getKey() throws Exception { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + + if (!keyStore.containsAlias(KEY_ALIAS)) { + KeyGenerator keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); + keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_ALIAS, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_GCM) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .build() + ); + keyGenerator.generateKey(); + } + return (SecretKey) keyStore.getKey(KEY_ALIAS, null); + } + + private static String encrypt(String plainText) { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.ENCRYPT_MODE, getKey()); + + byte[] iv = cipher.getIV(); + byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + + return Base64.encodeToString(iv, Base64.NO_WRAP) + ":" + + Base64.encodeToString(encrypted, Base64.NO_WRAP); + } catch (Exception e) { + return null; + } + } + + private static String decrypt(String encryptedText) { + try { + String[] parts = encryptedText.split(":"); + if (parts.length != 2) return null; + + byte[] iv = Base64.decode(parts[0], Base64.NO_WRAP); + byte[] data = Base64.decode(parts[1], Base64.NO_WRAP); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, getKey(), new GCMParameterSpec(128, iv)); + + byte[] decryptedBytes = cipher.doFinal(data); + return new String(decryptedBytes, StandardCharsets.UTF_8); + } catch (Exception e) { + return null; + } + } + + private static SharedPreferences getSecureStore(Context context) { + return context.getSharedPreferences(SECURE_PREFS_NAME, Context.MODE_PRIVATE); + } + + public static void put(Context context, String key, String value) { + if (value == null || value.isEmpty()) { + getSecureStore(context).edit().remove(key).apply(); + return; + } + String encrypted = encrypt(value); + if (encrypted != null) { + getSecureStore(context).edit().putString(key, encrypted).apply(); + } + } + + public static String get(Context context, String key, String defaultValue) { + String encrypted = getSecureStore(context).getString(key, null); + if (encrypted == null) return defaultValue; + + String decrypted = decrypt(encrypted); + return decrypted != null ? decrypted : defaultValue; + } + + public static void remove(Context context, String key) { + getSecureStore(context).edit().remove(key).apply(); + } + + public static boolean isSensitiveKey(String key) { + return SENSITIVE_KEYS.contains(key); + } + +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index 423c1202..afd530db 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -43,7 +43,6 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE); int ipAddress = wifiManager.getConnectionInfo().getIpAddress(); - // Convert little-endian to big-endianif needed if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) { ipAddress = Integer.reverseBytes(ipAddress); } @@ -61,19 +60,38 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { ipAddressLabel.setSummary(ipAddressString); - EditTextPreference passwordPref = findPreference("password"); - if (passwordPref != null) { - passwordPref.setSummaryProvider(pref -> "•••••••"); - } + setupSecurePreference("password", "•••••••"); + setupSecurePreference("client_secret", "••••••••"); + setupSecurePreference("client_id", null); + setupSecurePreference("token_endpoint", null); - EditTextPreference secretPref = findPreference("client_secret"); - if (secretPref != null) { - secretPref.setSummaryProvider(pref -> "••••••••"); - } setupSavedServers(); setupOAuthToggle(); } + private void setupSecurePreference(String key, String maskedSummary) { + EditTextPreference pref = findPreference(key); + if (pref == null) return; + + pref.setPersistent(false); + + String value = SecurePrefs.get(requireContext(), key, ""); + pref.setText(value); + + if (maskedSummary != null) { + pref.setSummaryProvider(p -> { + String v = SecurePrefs.get(requireContext(), key, ""); + return (v != null && !v.isEmpty()) ? maskedSummary : "Not set"; + }); + } + + pref.setOnPreferenceChangeListener((p, newValue) -> { + SecurePrefs.put(requireContext(), key, (String) newValue); + pref.setText((String) newValue); + return false; + }); + } + // ==================== Saved Servers ==================== private void setupSavedServers() { @@ -142,7 +160,7 @@ private void saveCurrentServer() { String ip = prefs.getString("ip_address", "").trim(); String port = prefs.getString("port", "").trim(); String username = prefs.getString("username", "").trim(); - String password = prefs.getString("password", "").trim(); + String password = SecurePrefs.get(requireContext(), "password", "").trim(); String endpoint = prefs.getString("endpoint_path", "").trim(); if (ip.isEmpty() || port.isEmpty()) { @@ -154,22 +172,25 @@ private void saveCurrentServer() { name = ip + ":" + port; } + String serverKey = ip + ":" + port + endpoint; JSONObject obj = new JSONObject(); try { obj.put("ip", ip); obj.put("port", port); obj.put("name", name); obj.put("username", username); - obj.put("password", password); obj.put("endpoint", endpoint); } catch (JSONException e) { e.printStackTrace(); return; } + if (!password.isEmpty()) { + SecurePrefs.put(requireContext(), "server_pwd_" + serverKey, password); + } + Set servers = new HashSet<>(getSavedServers()); - // remove duplicates (ip + port + endpoint) servers.removeIf(s -> { try { JSONObject existing = new JSONObject(s); @@ -219,9 +240,15 @@ private void showSelectServerDialog() { if (portPref != null) portPref.setText(obj.optString("port")); if (namePref != null) namePref.setText(obj.optString("name")); if (usernamePref != null) usernamePref.setText(obj.optString("username")); - if (passwordPref != null) passwordPref.setText(obj.optString("password")); if (endpointPref != null) endpointPref.setText(obj.optString("endpoint")); + String serverKey = obj.optString("ip") + ":" + obj.optString("port") + obj.optString("endpoint"); + String savedPwd = SecurePrefs.get(requireContext(), "server_pwd_" + serverKey, ""); + if (passwordPref != null) { + passwordPref.setText(savedPwd); + SecurePrefs.put(requireContext(), "password", savedPwd); + } + Toast.makeText(requireContext(), "Loaded: " + obj.optString("name"), Toast.LENGTH_SHORT).show(); } catch (JSONException e) { @@ -254,7 +281,15 @@ private void showRemoveServerDialog() { .setPositiveButton("Remove", (dialog, which) -> { Set remaining = new HashSet<>(); for (int i = 0; i < servers.size(); i++) { - if (!checked[i]) remaining.add(servers.get(i)); + if (!checked[i]) { + remaining.add(servers.get(i)); + } else { + try { + JSONObject obj = new JSONObject(servers.get(i)); + String serverKey = obj.optString("ip") + ":" + obj.optString("port") + obj.optString("endpoint"); + SecurePrefs.remove(requireContext(), "server_pwd_" + serverKey); + } catch (JSONException ignored) {} + } } putSavedServers(remaining); diff --git a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java index 725bdf8a..3def7b87 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java +++ b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java @@ -66,10 +66,8 @@ public void onCreate() { try { - // keep handle to Android context so it can be retrieved by OSH components SensorHubService.context = getApplicationContext(); - // create video surface texture here so it's not destroyed when pausing the app SensorHubService.videoTex = new SurfaceTexture(1); SensorHubService.videoTex.detachFromGLContext(); @@ -90,7 +88,6 @@ public void onCreate() { msgThread.start(); msgHandler = new Handler(msgThread.getLooper()); - // Start as foreground service with notification startForegroundService(); } catch (Exception e) @@ -177,18 +174,30 @@ public synchronized void startSensorHub(final IModuleConfigRepository config, fi this.hasVideo = hasVideo; - // Acquire wake locks BEFORE starting the hub + if (hasVideo) { + if (videoTex != null) { + videoTex.release(); + } + videoTex = new SurfaceTexture(1); + videoTex.detachFromGLContext(); + } + acquireWakeLocks(); msgHandler.post(new Runnable() { public void run() { - // create and start sensorhub instance sensorhub = new SensorHubAndroid(new SensorHubConfig(), config); try { sensorhub.start(); } catch (SensorHubException e) { - log.error("Error starting SensorHub: "+ e.getMessage()); - // Release locks if startup fails + log.error("Error starting SensorHub: " + e.getMessage()); + try { + sensorhub.stop(); + } catch (Exception ex) { + log.error("Error stopping failed SensorHub", ex); + } + sensorhub = null; + SensorHubService.this.hasVideo = false; releaseWakeLocks(); } } @@ -234,32 +243,13 @@ private void releaseWakeLocks() { public synchronized void stopSensorHub() { - if (sensorhub == null) - return; + if (sensorhub != null) { + sensorhub.stop(); + sensorhub = null; + } this.hasVideo = false; - final SensorHubAndroid hubToStop = sensorhub; - sensorhub = null; - - final java.util.concurrent.CountDownLatch stopLatch = new java.util.concurrent.CountDownLatch(1); - - msgHandler.post(new Runnable() { - public void run() { - try { - hubToStop.stop(); - } finally { - stopLatch.countDown(); - } - } - }); - - try { - stopLatch.await(15, java.util.concurrent.TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - releaseWakeLocks(); } From fdb371a4123294c452b7ab67f1255055e33583c1 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 21 Apr 2026 09:26:56 -0500 Subject: [PATCH 14/26] Updated the server settings - added securePrefs for passwords and tokens - removed all server settings from SettingsFragment + pref_settings and moved to server profiles - added server profiles to allow for multiple servers to push data to simultaneously and updated the mainactivity to loop through enabled server profiles to push data to. - added pencil edit icon + edit dialog to edit server settings after added - added long press on server profile to remove server --- sensorhub-android-app/AndroidManifest.xml | 5 + .../res/drawable-anydpi/ic_edit.xml | 11 + .../res/drawable-hdpi/ic_edit.png | Bin 0 -> 470 bytes .../res/drawable-mdpi/ic_edit.png | Bin 0 -> 318 bytes .../res/drawable-xhdpi/ic_edit.png | Bin 0 -> 580 bytes .../res/drawable-xxhdpi/ic_edit.png | Bin 0 -> 878 bytes sensorhub-android-app/res/drawable/ic_add.xml | 5 + .../res/drawable/ic_edit.xml | 11 + .../res/layout/activity_app_status.xml | 2 - .../res/layout/activity_server_profiles.xml | 62 ++++ .../res/layout/dialog_edit_server_profile.xml | 195 ++++++++++++ .../res/layout/fragment_dashboard.xml | 76 +---- .../res/layout/item_server_profile.xml | 73 +++++ .../res/layout/item_server_status.xml | 89 ++++++ .../res/xml/pref_settings.xml | 137 +------- .../sensorhub/android/AppStatusActivity.java | 3 - .../sensorhub/android/DashboardFragment.java | 290 +++++++++++------ .../org/sensorhub/android/MainActivity.java | 117 +++---- .../org/sensorhub/android/SecurePrefs.java | 13 +- .../org/sensorhub/android/ServerAdapter.java | 80 +++++ .../org/sensorhub/android/ServerProfile.java | 101 ++++++ .../android/ServerProfileRepository.java | 126 ++++++++ .../android/ServerProfilesActivity.java | 182 +++++++++++ .../sensorhub/android/SettingsFragment.java | 294 ++---------------- 24 files changed, 1229 insertions(+), 643 deletions(-) create mode 100644 sensorhub-android-app/res/drawable-anydpi/ic_edit.xml create mode 100644 sensorhub-android-app/res/drawable-hdpi/ic_edit.png create mode 100644 sensorhub-android-app/res/drawable-mdpi/ic_edit.png create mode 100644 sensorhub-android-app/res/drawable-xhdpi/ic_edit.png create mode 100644 sensorhub-android-app/res/drawable-xxhdpi/ic_edit.png create mode 100644 sensorhub-android-app/res/drawable/ic_add.xml create mode 100644 sensorhub-android-app/res/drawable/ic_edit.xml create mode 100644 sensorhub-android-app/res/layout/activity_server_profiles.xml create mode 100644 sensorhub-android-app/res/layout/dialog_edit_server_profile.xml create mode 100644 sensorhub-android-app/res/layout/item_server_profile.xml create mode 100644 sensorhub-android-app/res/layout/item_server_status.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 5b631672..0f4432b6 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -58,6 +58,11 @@ android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="false" /> + diff --git a/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml b/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml new file mode 100644 index 00000000..6238d358 --- /dev/null +++ b/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml @@ -0,0 +1,11 @@ + + + diff --git a/sensorhub-android-app/res/drawable-hdpi/ic_edit.png b/sensorhub-android-app/res/drawable-hdpi/ic_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..12583f0438d0781cf0a95e761ee51e827e07f6d0 GIT binary patch literal 470 zcmV;{0V)28P)1S z@Sli~f`TOVcU6V%nPD=SHrn_HNRnhwEQkZtS`L+{if@LyoM{;petfmyJzOF4Q-sYxpM(A=cH0>jB#ryb8Ny1K8HIjjbyvvz z31N%S`=P&B-4!xFLRbO%6!ZtN$IiGwrk!FLVau2qfnK({D`Z|n*aY-Y=GExov;USB&bUJ6B@?s_Esr8MR!;>ev}JxDLYcZStd#2J`)29#dym#sp1EP{4echNE)xb8xgK2t=gtG+V}Hq)$ literal 0 HcmV?d00001 diff --git a/sensorhub-android-app/res/drawable-mdpi/ic_edit.png b/sensorhub-android-app/res/drawable-mdpi/ic_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..33be3b878795d651314bf508c042490817b54732 GIT binary patch literal 318 zcmV-E0m1%>P)NPDd@~#^_)AwLV0aK zeRxdp9e09d;3v4HS=YT!l5rnHivR!s literal 0 HcmV?d00001 diff --git a/sensorhub-android-app/res/drawable-xhdpi/ic_edit.png b/sensorhub-android-app/res/drawable-xhdpi/ic_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..a9ea931334661d234f0228ed8fa50b33f6eec038 GIT binary patch literal 580 zcmV-K0=xZ*P)oy>l;Uf`DN_$oyOqio`hi) zdwe$(H?-;TDU4Rd4!;wIn|T%n|B8ElCp%VDelE}sOHWkPG zZWgTA^Y6kC{#bD?74Q6R7ObeeGJm-^KNM#ixZmfuYsU)nYTKNXuIfvm?KaB#Ecl0Z zO%LoHYk}t*c6`w&reuq#j4$6ZFbn0I3KcRVLq;H%CI>) zoAJ37Z4+PlCBk4;U|?f=o)IQffmPd#wkacws(|^JJ^vTNEJy5~Z)s_1X#_uN9M%P& SALpb10000Ns=T)f{YMgJwHcr-b)w z3O2BfhGwiIyk}F;+5VJ!^ak(Q1XMcbe}^Ml!Fz{??B^VLQ~FN%`6T+yx`Ou(4{v&v z>IbrfbANR02jz9Iw(;KK;Z5ICjGzvo=Dl)FO)|Fo{WY3^H+@Ml)0cG4sc<^@=bx18 zHAy(DSRWqV^d^-v0S)zaGFIc@*WW5Nr~|AE57}cSo$~Pr~29y72I(&l|=> z>R5G9>Au=Wt)g|T3lEjfsf9E4%;}&KPHhcQbu>f5o4%nK!EiidRqGrXt0p_=@@VsD zs2@)Cw@xKst8;d-E%zmE z-c^iXIGzK!7LL4j2kXMao4#lm6Io;BdCs|xb>X3MAlJh2oH?H7oQGH!9x7kzp%Dzn zGgh_Ek>ED5EMlc*}tgLWkji*@G1d16n<_QPuaEX&eg(^*Qx$j4l5pDl&n_>D;Yl@|16e~c z?3HxRV_a`=D8{{N?*sN}P>g%@0@q2BBuSDaNs_^E3_Dls-47Bc + + + + diff --git a/sensorhub-android-app/res/drawable/ic_edit.xml b/sensorhub-android-app/res/drawable/ic_edit.xml new file mode 100644 index 00000000..6238d358 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_edit.xml @@ -0,0 +1,11 @@ + + + diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml index 0f9ef588..69a569ce 100644 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -8,7 +8,6 @@ android:fitsSystemWindows="true" tools:context=".AppStatusActivity"> - - + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml b/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml new file mode 100644 index 00000000..a2384931 --- /dev/null +++ b/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml index 1381044f..b916bc37 100644 --- a/sensorhub-android-app/res/layout/fragment_dashboard.xml +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -87,76 +87,22 @@ - + android:layout_height="0dp" + android:layout_weight="1" + android:fillViewport="true" + android:clipToPadding="false" + android:paddingBottom="80dp"> - - - - - - - - - - - - - - - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="@dimen/info_card_margin" /> - + diff --git a/sensorhub-android-app/res/layout/item_server_profile.xml b/sensorhub-android-app/res/layout/item_server_profile.xml new file mode 100644 index 00000000..79f91884 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_server_profile.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_server_status.xml b/sensorhub-android-app/res/layout/item_server_status.xml new file mode 100644 index 00000000..60bf5e54 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_server_status.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index bf539055..841a7dbe 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -24,155 +24,36 @@ app:useSimpleSummaryProvider="true" android:layout="@layout/preference_item" /> - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java index a5a9a675..4e02d47c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java @@ -19,7 +19,6 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_status); - // Set up toolbar with back navigation MaterialToolbar toolbar = findViewById(R.id.status_toolbar); setSupportActionBar(toolbar); toolbar.setNavigationOnClickListener(v -> onBackPressed()); @@ -33,7 +32,6 @@ protected void onCreate(Bundle savedInstanceState) { String sensorStatus = intent.getStringExtra("androidSensorStatus"); String sensorStorageStatus = intent.getStringExtra("sensorStorageStatus"); - // Set status text TextView sosStatusView = findViewById(R.id.sos_service_state); TextView conSysStatusView = findViewById(R.id.consys_service_state); TextView discoveryStatusView = findViewById(R.id.discovery_service_state); @@ -48,7 +46,6 @@ protected void onCreate(Bundle savedInstanceState) { sensorStatusView.setText(sensorStatus); storageStatusView.setText(sensorStorageStatus); - // Color the status indicator dots setStatusDotColor(findViewById(R.id.sos_status_dot), sosStatus); setStatusDotColor(findViewById(R.id.consys_status_dot), consSysStatus); setStatusDotColor(findViewById(R.id.discovery_status_dot), discoveryStatus); diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index f9e56fff..064a23c9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -52,31 +52,35 @@ import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.Flow; +import android.widget.LinearLayout; + public class DashboardFragment extends Fragment implements TextureView.SurfaceTextureListener, Flow.Subscriber { - private TextView mainInfoArea; private TextView videoInfoArea; private TextureView textureView; private MaterialCardView videoStatusCard; private MaterialButton btnToggleVideo; private View videoStatusDot; private FloatingActionButton fab; - private ImageButton btnToggleStatus; - private View mainInfoScroll; + private LinearLayout serverStatusContainer; private Handler displayHandler; private Runnable displayCallback; - private StringBuffer mainInfoText = new StringBuffer(); private StringBuffer videoInfoText = new StringBuffer(); private Flow.Subscription subscription; private SensorHubServiceProvider provider; private boolean videoPreviewVisible = false; - private boolean statusExpanded = true; + + private final Map serverCardViews = new HashMap<>(); + private final Set expandedServers = new HashSet<>(); @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -95,7 +99,6 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mainInfoArea = view.findViewById(R.id.main_info); videoInfoArea = view.findViewById(R.id.video_info); textureView = view.findViewById(R.id.video); @@ -107,9 +110,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat btnToggleVideo.setOnClickListener(v -> toggleVideoPreview()); - btnToggleStatus = view.findViewById(R.id.btn_toggle_status); - btnToggleStatus.setOnClickListener(v -> toggleStatusExpanded()); - mainInfoScroll = view.findViewById(R.id.main_info_scroll); + serverStatusContainer = view.findViewById(R.id.server_status_container); fab = view.findViewById(R.id.fab_toggle); fab.setOnClickListener(v -> { @@ -259,6 +260,8 @@ private void pollHubReady() { if (!provider.isOshStarted()) { provider.setOshStarted(true); updateFabIcon(); + serverStatusContainer.removeAllViews(); + serverCardViews.clear(); startRefreshingStatus(); updateVideoStatusCard(); if (videoPreviewVisible) @@ -287,7 +290,6 @@ protected void startRefreshingStatus() { displayCallback = new Runnable() { public void run() { displayStatus(); - mainInfoArea.setText(Html.fromHtml(mainInfoText.toString())); videoInfoArea.setText(Html.fromHtml(videoInfoText.toString())); displayHandler.postDelayed(this, 1000); } @@ -303,114 +305,132 @@ protected void stopRefreshingStatus() { } protected synchronized void displayStatus() { - mainInfoText.setLength(0); + Set activeClientIds = new HashSet<>(); - // SOST Client errors/status for (SOSTClient client : provider.getSostClients()) { + String clientId = client.getLocalID(); + activeClientIds.add(clientId); + String serverName = extractServerName(client.getName(), "SOS-T"); + String clientMode = "SOS-T"; + Map dataStreams = client.getDataStreams(); - boolean showError = (client.getCurrentError() != null); - boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); - - if (showError || showMsg) { - mainInfoText.append("

" + client.getName() + ":
"); - if (showMsg) - mainInfoText.append(client.getStatusMessage() + "
"); - if (showError) { - Throwable errorObj = client.getCurrentError(); - String errorMsg = errorObj.getMessage().trim(); - if (!errorMsg.endsWith(".")) - errorMsg += ". "; - if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) - errorMsg += errorObj.getCause().getMessage(); - mainInfoText.append("" + errorMsg + ""); - } - mainInfoText.append("

"); + StringBuffer detailHtml = new StringBuffer(); + boolean hasError = false; + + if (client.getCurrentError() != null) { + hasError = true; + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage() != null ? errorObj.getMessage().trim() : "Unknown error"; + if (!errorMsg.endsWith(".")) errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + detailHtml.append("" + errorMsg + "
"); } - } - - // ConSys Client errors/status - for (ConSysApiClientModule client : provider.getConSysClients()) { - Map dataStreams = client.getDataStreams(); - boolean showError = (client.getCurrentError() != null); - boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); - - if (showError || showMsg) { - mainInfoText.append("

" + client.getName() + ":
"); - if (showMsg) - mainInfoText.append(client.getStatusMessage() + "
"); - if (showError) { - Throwable errorObj = client.getCurrentError(); - String errorMsg = errorObj.getMessage().trim(); - if (!errorMsg.endsWith(".")) - errorMsg += ". "; - if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) - errorMsg += errorObj.getCause().getMessage(); - mainInfoText.append("" + errorMsg + ""); - } - mainInfoText.append("

"); + if (dataStreams.isEmpty() && client.getStatusMessage() != null) { + detailHtml.append(client.getStatusMessage() + "
"); } - } - mainInfoText.append("

"); - for (SOSTClient client : provider.getSostClients()) { - mainInfoText.append("SOS-T Client

"); - Map dataStreams = client.getDataStreams(); long now = System.currentTimeMillis(); + boolean allOk = !hasError && !dataStreams.isEmpty(); for (Entry stream : dataStreams.entrySet()) { - mainInfoText.append("" + stream.getKey() + " : "); + detailHtml.append("" + stream.getKey() + " : "); long lastEventTime = stream.getValue().lastEventTime; long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) - mainInfoText.append("NO OBS"); - else if (dt > stream.getValue().measPeriodMs) - mainInfoText.append("NOK (" + dt + "ms ago)"); - else - mainInfoText.append("OK (" + dt + "ms ago)"); + 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) { - mainInfoText.append(" ("); - mainInfoText.append(stream.getValue().errorCount); - mainInfoText.append(")"); + detailHtml.append(" (" + stream.getValue().errorCount + ")"); + allOk = false; } - mainInfoText.append("
"); + detailHtml.append("
"); } + + updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); } for (ConSysApiClientModule client : provider.getConSysClients()) { - mainInfoText.append("ConSysApi Client

"); + String clientId = client.getLocalID(); + activeClientIds.add(clientId); + String serverName = extractServerName(client.getName(), "Connected Systems"); + String clientMode = "Connected Systems"; + Map dataStreams = client.getDataStreams(); + StringBuffer detailHtml = new StringBuffer(); + boolean hasError = false; + + if (client.getCurrentError() != null) { + hasError = true; + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage() != null ? errorObj.getMessage().trim() : "Unknown error"; + if (!errorMsg.endsWith(".")) errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + detailHtml.append("" + errorMsg + "
"); + } + if (dataStreams.isEmpty() && client.getStatusMessage() != null) { + detailHtml.append(client.getStatusMessage() + "
"); + } + long now = System.currentTimeMillis(); + boolean allOk = !hasError && !dataStreams.isEmpty(); for (Entry stream : dataStreams.entrySet()) { - mainInfoText.append("" + stream.getKey() + " : "); + detailHtml.append("" + stream.getKey() + " : "); long lastEventTime = stream.getValue().lastEventTime; long dt = now - lastEventTime; - if (lastEventTime == Long.MIN_VALUE) - mainInfoText.append("NO OBS"); - else if (dt > stream.getValue().measPeriodMs) - mainInfoText.append("NOK (" + dt + "ms ago)"); - else - mainInfoText.append("OK (" + dt + "ms ago)"); + 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) { - mainInfoText.append(" ("); - mainInfoText.append(stream.getValue().errorCount); - mainInfoText.append(")"); + detailHtml.append(" (" + stream.getValue().errorCount + ")"); + allOk = false; } - mainInfoText.append("
"); + detailHtml.append("
"); } + + updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); } - mainInfoText.append("

"); - if (mainInfoText.length() > 5) - mainInfoText.setLength(mainInfoText.length() - 5); - mainInfoText.append("

"); + Set staleIds = new HashSet<>(serverCardViews.keySet()); + staleIds.removeAll(activeClientIds); + for (String id : staleIds) { + View card = serverCardViews.remove(id); + if (card != null) serverStatusContainer.removeView(card); + expandedServers.remove(id); + } SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); MainActivity activity = (MainActivity) requireActivity(); boolean serveOrStore = activity.shouldServe(prefs) || activity.shouldStore(prefs); - if (provider.getSostClients().isEmpty() && serveOrStore) { - mainInfoText.append("No Sensors Set to Push Remotely"); - } - if (provider.getConSysClients().isEmpty() && serveOrStore) { - mainInfoText.append("No Sensors Set to Push Remotely"); + boolean noClients = provider.getSostClients().isEmpty() && provider.getConSysClients().isEmpty(); + + View emptyView = serverStatusContainer.findViewWithTag("empty_status"); + if (noClients && serveOrStore) { + if (emptyView == null) { + TextView tv = new TextView(requireContext()); + tv.setTag("empty_status"); + tv.setText("No Sensors Set to Push Remotely"); + tv.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurfaceVariant)); + tv.setTextSize(14); + tv.setGravity(android.view.Gravity.CENTER); + int pad = (int) (16 * getResources().getDisplayMetrics().density); + tv.setPadding(pad, pad, pad, pad); + serverStatusContainer.addView(tv); + } + } else if (emptyView != null) { + serverStatusContainer.removeView(emptyView); } AndroidSensorsDriver sensors = provider.getAndroidSensors(); @@ -434,9 +454,86 @@ else if (dt > stream.getValue().measPeriodMs) } protected synchronized void newStatusMessage(String msg) { - mainInfoText.setLength(0); - mainInfoText.append(msg); - displayHandler.post(() -> mainInfoArea.setText(mainInfoText.toString())); + displayHandler.post(() -> { + serverStatusContainer.removeAllViews(); + serverCardViews.clear(); + TextView tv = new TextView(requireContext()); + tv.setText(msg); + tv.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurface)); + tv.setTextSize(14); + int pad = (int) (16 * getResources().getDisplayMetrics().density); + tv.setPadding(pad, pad, pad, pad); + serverStatusContainer.addView(tv); + }); + } + + private String extractServerName(String clientName, String fallback) { + if (clientName != null && clientName.contains(" -> ")) { + return clientName.substring(clientName.lastIndexOf(" -> ") + 4); + } + return fallback; + } + + private void updateServerCard(String clientId, String serverName, String clientMode, + boolean allOk, boolean hasError, String detailHtml) { + View card = serverCardViews.get(clientId); + + if (card == null) { + card = LayoutInflater.from(requireContext()) + .inflate(R.layout.item_server_status, serverStatusContainer, false); + serverCardViews.put(clientId, card); + serverStatusContainer.addView(card); + + 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); + 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)); + } + + 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)); + } + + 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); + + ImageButton toggle = card.findViewById(R.id.btn_toggle_server_details); + toggle.setImageResource(expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); } private void updateVideoStatusCard() { @@ -457,18 +554,12 @@ private void updateVideoStatusCard() { } } - private void toggleStatusExpanded() { - statusExpanded = !statusExpanded; - mainInfoScroll.setVisibility(statusExpanded ? View.VISIBLE : View.GONE); - btnToggleStatus.setImageResource(statusExpanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); - } - private void toggleVideoPreview() { videoPreviewVisible = !videoPreviewVisible; if (videoPreviewVisible) { textureView.setVisibility(View.VISIBLE); btnToggleVideo.setText("Hide"); - mainInfoArea.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme())); + serverStatusContainer.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme())); showVideo(); } else { hideVideoPreview(); @@ -479,7 +570,7 @@ private void hideVideoPreview() { videoPreviewVisible = false; textureView.setVisibility(View.GONE); if (btnToggleVideo != null) btnToggleVideo.setText("Show"); - mainInfoArea.setBackgroundColor(0x00000000); + serverStatusContainer.setBackgroundColor(0x00000000); } protected void showVideo() { @@ -506,7 +597,6 @@ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {} - // ==================== Event Subscriber ==================== @Override public void onSubscribe(Flow.Subscription subscription) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 46a57290..4bd24535 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -153,7 +153,6 @@ public class MainActivity extends AppCompatActivity implements SensorHubServiceP URL url; AndroidSensorsDriver androidSensors; boolean showVideo; - URL clientURL = null; String deviceID; String runName; @@ -244,50 +243,20 @@ public void updateConfig(SharedPreferences prefs, String runName) deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); sensorhubConfig = new InMemoryConfigDb(new ModuleClassFinder()); - String host = prefs.getString("ip_address", "127.0.0.1").trim(); - String portStr = prefs.getString("port", "8080").trim(); - String user = prefs.getString("username", null); - String password = SecurePrefs.get(this, "password", null); - String endpointPath = prefs.getString("endpoint_path", null); - Boolean isApiServiceEnabled = prefs.getBoolean("api_service", true); Boolean isSosServiceEnabled = prefs.getBoolean("sos_service", true); Boolean isDiscoveryServiceEnabled = prefs.getBoolean("discovery_service", true); - Boolean isClientEnabled = prefs.getBoolean("enable_client", true); - Boolean isTLSEnabled = prefs.getBoolean("enable_tls", false); - if (host == null || host.isEmpty()) - host = "127.0.0.1"; - host = host.replace("http://", "").replace("https://", ""); + ServerProfileRepository serverRepo = new ServerProfileRepository(this); + List enabledServers = serverRepo.getEnabled(); - int port; - try { - port = Integer.parseInt(portStr); - if (port < 1 || port > 65535) { - port = 8080; + boolean disableSslCheck = false; + for (ServerProfile sp : enabledServers) { + if (sp.disableSslCheck) { + disableSslCheck = true; + break; } - } catch (NumberFormatException e) { - port = 8080; } - - if (endpointPath.isEmpty()) { - endpointPath = ""; - } else if (!endpointPath.startsWith("/")) { - endpointPath = "/" + endpointPath; - } - - String urlStr = (isTLSEnabled ? "https://" : "http://") + - host + ":" + port + endpointPath; - - try { - clientURL = new URI(urlStr).toURL(); - } catch (URISyntaxException | MalformedURLException e) { - log.error("Invalid URL: " + urlStr, e); - clientURL = null; - } - - - boolean disableSslCheck = prefs.getBoolean("sos_disable_ssl_check", false); if (disableSslCheck) { TrustManager[] trustAllCerts = new TrustManager[]{ @@ -320,12 +289,6 @@ public boolean verify(String arg0, SSLSession arg1) { } } - // OAuth - Boolean isOAuthEnabled = prefs.getBoolean("o_auth_enabled", false); - String clientId = SecurePrefs.get(this, "client_id", "").trim(); - String tokenEndpoint = SecurePrefs.get(this, "token_endpoint", "").trim(); - String clientSecret = SecurePrefs.get(this, "client_secret", "").trim(); - String deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); String deviceName = prefs.getString("device_name", null); @@ -432,20 +395,28 @@ public boolean verify(String arg0, SSLSession arg1) { } discoveryServiceConfig.rulesFilePath = outFile.getAbsolutePath(); - // OAuth - ConSysOAuthConfig conSysOAuthConfig = new ConSysOAuthConfig(); - conSysOAuthConfig.oAuthEnabled = isOAuthEnabled; - conSysOAuthConfig.tokenEndpoint = tokenEndpoint; - conSysOAuthConfig.clientID = clientId; - conSysOAuthConfig.clientSecret = clientSecret; - sensorhubConfig.add(sensorsConfig); if (isPushingSensor(Sensors.Android)) { - if (isClientEnabled) { - addCSApiConfig(sensorsConfig, user, password, conSysOAuthConfig); - } else { - addSosTConfig(sensorsConfig, user, password); + for (ServerProfile sp : enabledServers) { + URL profileUrl = sp.buildClientUrl(); + if (profileUrl == null) { + log.error("Skipping server profile '{}': invalid URL", sp.name); + continue; + } + + String pwd = serverRepo.getPassword(sp.id); + + if (sp.useConSysClient) { + ConSysOAuthConfig oAuthConfig = new ConSysOAuthConfig(); + oAuthConfig.oAuthEnabled = sp.oAuthEnabled; + oAuthConfig.tokenEndpoint = serverRepo.getOAuthTokenEndpoint(sp.id); + oAuthConfig.clientID = serverRepo.getOAuthClientId(sp.id); + oAuthConfig.clientSecret = serverRepo.getOAuthClientSecret(sp.id); + addCSApiConfig(sensorsConfig, sp, profileUrl, sp.username, pwd, oAuthConfig); + } else { + addSosTConfig(sensorsConfig, sp, profileUrl, sp.username, pwd); + } } } @@ -609,18 +580,16 @@ public boolean verify(String arg0, SSLSession arg1) { } - protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) + protected void addSosTConfig(SensorConfig sensorConf, ServerProfile profile, URL serverUrl, String user, String pwd) { - if (clientURL == null) - return; SOSTClientConfig sosConfig = new SOSTClientConfig(); - sosConfig.id = sensorConf.id + "_SOST"; - sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", ""); + sosConfig.id = sensorConf.id + "_SOST_" + profile.id; + sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; sosConfig.autoStart = true; - sosConfig.sos.remoteHost = clientURL.getHost(); - sosConfig.sos.remotePort = clientURL.getPort() < 0 ? clientURL.getDefaultPort() : clientURL.getPort(); - sosConfig.sos.resourcePath = clientURL.getPath(); - sosConfig.sos.enableTLS = clientURL.getProtocol().equals("https"); + sosConfig.sos.remoteHost = serverUrl.getHost(); + sosConfig.sos.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); + sosConfig.sos.resourcePath = serverUrl.getPath(); + sosConfig.sos.enableTLS = serverUrl.getProtocol().equals("https"); sosConfig.sos.user = user; sosConfig.sos.password = pwd; sosConfig.connection.connectTimeout = 10000; @@ -631,19 +600,16 @@ protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) sensorhubConfig.add(sosConfig); } - protected void addCSApiConfig(SensorConfig sensorConf, String apiUser, String apiPwd, ConSysOAuthConfig oAuthConfig) + protected void addCSApiConfig(SensorConfig sensorConf, ServerProfile profile, URL serverUrl, String apiUser, String apiPwd, ConSysOAuthConfig oAuthConfig) { - if (clientURL == null) - return; - ConSysApiClientConfig consysConfig = new ConSysApiClientConfig(); - consysConfig.id = sensorConf.id + "_CONSYS"; - consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", ""); + consysConfig.id = sensorConf.id + "_CONSYS_" + profile.id; + consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; consysConfig.autoStart = true; - consysConfig.conSys.remoteHost = clientURL.getHost(); - consysConfig.conSys.remotePort = clientURL.getPort() < 0 ? clientURL.getDefaultPort() : clientURL.getPort(); - consysConfig.conSys.resourcePath = clientURL.getPath(); - consysConfig.conSys.enableTLS = clientURL.getProtocol().equals("https"); + consysConfig.conSys.remoteHost = serverUrl.getHost(); + consysConfig.conSys.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); + consysConfig.conSys.resourcePath = serverUrl.getPath(); + consysConfig.conSys.enableTLS = serverUrl.getProtocol().equals("https"); consysConfig.conSys.user = apiUser; consysConfig.conSys.password = apiPwd; consysConfig.connection.connectTimeout = 10000; @@ -797,8 +763,6 @@ protected void onDestroy() super.onDestroy(); } - // ==================== Controller Event Forwarding ==================== - private ControllerDriver getControllerDriver() { if (boundService == null || boundService.sensorhub == null) return null; @@ -825,7 +789,6 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) { return super.dispatchGenericMotionEvent(event); } - // ==================== Dialogs ==================== protected void showMeshtasticDialog() { LayoutInflater inflater = getLayoutInflater(); diff --git a/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java index 042ea734..2b5e98d7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java @@ -106,7 +106,18 @@ public static void remove(Context context, String key) { } public static boolean isSensitiveKey(String key) { - return SENSITIVE_KEYS.contains(key); + return SENSITIVE_KEYS.contains(key) || key.startsWith("profile_"); + } + + public static void removeByPrefix(Context context, String prefix) { + SharedPreferences secureStore = getSecureStore(context); + SharedPreferences.Editor editor = secureStore.edit(); + for (String key : secureStore.getAll().keySet()) { + if (key.startsWith(prefix)) { + editor.remove(key); + } + } + editor.apply(); } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java b/sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java new file mode 100644 index 00000000..fd070e7b --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java @@ -0,0 +1,80 @@ +package org.sensorhub.android; + +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 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/ServerProfile.java b/sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java new file mode 100644 index 00000000..a0fab6f3 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java @@ -0,0 +1,101 @@ +package org.sensorhub.android; + +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 name; + 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.name = "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("name", name); + 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.name = obj.optString("name", ""); + 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 host + ":" + port + (endpointPath != null ? endpointPath : ""); + } + + public String getClientModeLabel() { + return useConSysClient ? "Connected Systems" : "SOS-T"; + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java b/sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java new file mode 100644 index 00000000..a031a373 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java @@ -0,0 +1,126 @@ +package org.sensorhub.android; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.preference.PreferenceManager; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.List; + +public class ServerProfileRepository { + private static final String KEY_PROFILES_JSON = "server_profiles_json"; + private final Context context; + private final SharedPreferences prefs; + + public ServerProfileRepository(Context context) { + this.context = context.getApplicationContext(); + this.prefs = PreferenceManager.getDefaultSharedPreferences(this.context); + } + + public List getAll() { + List profiles = new ArrayList<>(); + String json = prefs.getString(KEY_PROFILES_JSON, null); + if (json == null) return profiles; + + try { + JSONArray arr = new JSONArray(json); + for (int i = 0; i < arr.length(); i++) { + profiles.add(ServerProfile.fromJson(arr.getJSONObject(i))); + } + } catch (JSONException e) { + // corrupted data, return empty + } + return profiles; + } + + public List getEnabled() { + List enabled = new ArrayList<>(); + for (ServerProfile p : getAll()) { + if (p.enabled) enabled.add(p); + } + return enabled; + } + + public ServerProfile getById(String id) { + for (ServerProfile p : getAll()) { + if (p.id.equals(id)) return p; + } + return null; + } + + public void save(ServerProfile profile) { + List all = getAll(); + boolean found = false; + for (int i = 0; i < all.size(); i++) { + if (all.get(i).id.equals(profile.id)) { + all.set(i, profile); + found = true; + break; + } + } + if (!found) all.add(profile); + persist(all); + } + + public void delete(String id) { + List all = getAll(); + all.removeIf(p -> p.id.equals(id)); + persist(all); + SecurePrefs.removeByPrefix(context, "profile_" + id + "_"); + } + + public void setEnabled(String id, boolean enabled) { + ServerProfile p = getById(id); + if (p != null) { + p.enabled = enabled; + save(p); + } + } + + public String getPassword(String profileId) { + return SecurePrefs.get(context, "profile_" + profileId + "_password", null); + } + + public void setPassword(String profileId, String password) { + SecurePrefs.put(context, "profile_" + profileId + "_password", password); + } + + public String getOAuthTokenEndpoint(String profileId) { + return SecurePrefs.get(context, "profile_" + profileId + "_oauth_token_endpoint", ""); + } + + public void setOAuthTokenEndpoint(String profileId, String value) { + SecurePrefs.put(context, "profile_" + profileId + "_oauth_token_endpoint", value); + } + + public String getOAuthClientId(String profileId) { + return SecurePrefs.get(context, "profile_" + profileId + "_oauth_client_id", ""); + } + + public void setOAuthClientId(String profileId, String value) { + SecurePrefs.put(context, "profile_" + profileId + "_oauth_client_id", value); + } + + public String getOAuthClientSecret(String profileId) { + return SecurePrefs.get(context, "profile_" + profileId + "_oauth_client_secret", ""); + } + + public void setOAuthClientSecret(String profileId, String value) { + SecurePrefs.put(context, "profile_" + profileId + "_oauth_client_secret", value); + } + + private void persist(List profiles) { + JSONArray arr = new JSONArray(); + for (ServerProfile p : profiles) { + try { + arr.put(p.toJson()); + } catch (JSONException ignored) { + } + } + prefs.edit().putString(KEY_PROFILES_JSON, arr.toString()).apply(); + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java new file mode 100644 index 00000000..b8208ec7 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java @@ -0,0 +1,182 @@ +package org.sensorhub.android; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.materialswitch.MaterialSwitch; + +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; + + @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 = new ServerProfileRepository(this); + + recyclerView = findViewById(R.id.server_list); + emptyText = findViewById(R.id.empty_text); + FloatingActionButton fab = findViewById(R.id.fab_add_server); + + adapter = new ServerAdapter(servers, this); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setAdapter(adapter); + + fab.setOnClickListener(v -> showServerDialog(null)); + + refreshList(); + } + + @Override + public void onEditClicked(ServerProfile profile) { + showServerDialog(profile); + } + + @Override + public void onEnabledToggled(ServerProfile profile, boolean enabled) { + repo.setEnabled(profile.id, enabled); + } + + @Override + public void onDeleteRequested(ServerProfile profile) { + new MaterialAlertDialogBuilder(this) + .setTitle("Delete Server") + .setMessage("Remove \"" + profile.name + "\"?") + .setPositiveButton("Delete", (d, w) -> { + repo.delete(profile.id); + refreshList(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void showServerDialog(ServerProfile existing) { + boolean isEdit = existing != null; + + View dialogView = LayoutInflater.from(this) + .inflate(R.layout.dialog_edit_server_profile, null); + + EditText nameInput = dialogView.findViewById(R.id.edit_name); + EditText hostInput = dialogView.findViewById(R.id.edit_host); + EditText portInput = dialogView.findViewById(R.id.edit_port); + EditText endpointInput = dialogView.findViewById(R.id.edit_endpoint); + EditText usernameInput = dialogView.findViewById(R.id.edit_username); + EditText passwordInput = dialogView.findViewById(R.id.edit_password); + + EditText tokenInput = dialogView.findViewById(R.id.edit_token_endpoint); + EditText clientIdInput = dialogView.findViewById(R.id.edit_client_id); + EditText clientSecretInput = dialogView.findViewById(R.id.edit_client_secret); + + MaterialSwitch tlsSwitch = dialogView.findViewById(R.id.switch_tls); + MaterialSwitch sslSwitch = dialogView.findViewById(R.id.switch_disable_ssl); + MaterialSwitch oauthSwitch = dialogView.findViewById(R.id.switch_oauth); + MaterialSwitch clientModeSwitch = dialogView.findViewById(R.id.switch_client_mode); + + View oauthFields = dialogView.findViewById(R.id.oauth_fields); + + Runnable updateOAuthVisibility = () -> { + boolean show = clientModeSwitch.isChecked() && oauthSwitch.isChecked(); + oauthFields.setVisibility(show ? View.VISIBLE : View.GONE); + }; + + clientModeSwitch.setOnCheckedChangeListener((btn, checked) -> { + oauthSwitch.setVisibility(checked ? View.VISIBLE : View.GONE); + updateOAuthVisibility.run(); + }); + oauthSwitch.setOnCheckedChangeListener((btn, checked) -> + updateOAuthVisibility.run()); + + if (isEdit) { + nameInput.setText(existing.name); + hostInput.setText(existing.host); + portInput.setText(String.valueOf(existing.port)); + endpointInput.setText(existing.endpointPath); + usernameInput.setText(existing.username); + tlsSwitch.setChecked(existing.enableTls); + sslSwitch.setChecked(existing.disableSslCheck); + clientModeSwitch.setChecked(existing.useConSysClient); + oauthSwitch.setChecked(existing.oAuthEnabled); + + passwordInput.setText(repo.getPassword(existing.id)); + tokenInput.setText(repo.getOAuthTokenEndpoint(existing.id)); + clientIdInput.setText(repo.getOAuthClientId(existing.id)); + clientSecretInput.setText(repo.getOAuthClientSecret(existing.id)); + } + + oauthSwitch.setVisibility(clientModeSwitch.isChecked() ? View.VISIBLE : View.GONE); + updateOAuthVisibility.run(); + + new MaterialAlertDialogBuilder(this) + .setTitle(isEdit ? "Edit Server" : "Add Server") + .setView(dialogView) + .setPositiveButton("Save", (dialog, which) -> { + String name = nameInput.getText().toString().trim(); + String host = hostInput.getText().toString().trim(); + String portStr = portInput.getText().toString().trim(); + + if (name.isEmpty() || host.isEmpty() || portStr.isEmpty()) { + Toast.makeText(this, "Name, host, and port are required", + Toast.LENGTH_SHORT).show(); + return; + } + + ServerProfile profile = isEdit ? existing : new ServerProfile(); + profile.name = name; + profile.host = host; + profile.port = Integer.parseInt(portStr); + profile.endpointPath = endpointInput.getText().toString().trim(); + profile.username = usernameInput.getText().toString().trim(); + profile.enableTls = tlsSwitch.isChecked(); + profile.disableSslCheck = sslSwitch.isChecked(); + profile.useConSysClient = clientModeSwitch.isChecked(); + profile.oAuthEnabled = oauthSwitch.isChecked(); + + repo.save(profile); + + repo.setPassword(profile.id, + passwordInput.getText().toString().trim()); + repo.setOAuthClientId(profile.id, + clientIdInput.getText().toString().trim()); + repo.setOAuthClientSecret(profile.id, + clientSecretInput.getText().toString().trim()); + repo.setOAuthTokenEndpoint(profile.id, + tokenInput.getText().toString().trim()); + + refreshList(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void refreshList() { + servers.clear(); + servers.addAll(repo.getAll()); + adapter.notifyDataSetChanged(); + emptyText.setVisibility(servers.isEmpty() ? View.VISIBLE : View.GONE); + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index afd530db..1ae79ba7 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -2,29 +2,17 @@ import static android.content.Context.WIFI_SERVICE; -import android.content.SharedPreferences; +import android.content.Intent; import android.net.wifi.WifiManager; import android.os.Bundle; -import android.text.method.PasswordTransformationMethod; -import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; -import androidx.preference.EditTextPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreferenceCompat; -import org.json.JSONException; -import org.json.JSONObject; - import java.math.BigInteger; import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import java.nio.ByteOrder; @@ -33,8 +21,6 @@ */ public class SettingsFragment extends PreferenceFragmentCompat { - private static final String PREF_SAVED_SERVERS = "saved_servers_set"; - @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref_settings, rootKey); @@ -59,277 +45,51 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); ipAddressLabel.setSummary(ipAddressString); + manageServerProfiles(); + setupDiscoveryToggle(); - setupSecurePreference("password", "•••••••"); - setupSecurePreference("client_secret", "••••••••"); - setupSecurePreference("client_id", null); - setupSecurePreference("token_endpoint", null); - - setupSavedServers(); - setupOAuthToggle(); } - private void setupSecurePreference(String key, String maskedSummary) { - EditTextPreference pref = findPreference(key); - if (pref == null) return; - - pref.setPersistent(false); - - String value = SecurePrefs.get(requireContext(), key, ""); - pref.setText(value); - - if (maskedSummary != null) { - pref.setSummaryProvider(p -> { - String v = SecurePrefs.get(requireContext(), key, ""); - return (v != null && !v.isEmpty()) ? maskedSummary : "Not set"; - }); - } - - pref.setOnPreferenceChangeListener((p, newValue) -> { - SecurePrefs.put(requireContext(), key, (String) newValue); - pref.setText((String) newValue); - return false; - }); + @Override + public void onResume() { + super.onResume(); + Preference serverPref = findPreference("manage_servers"); + if (serverPref != null) updateServerProfilesSummary(serverPref); } - // ==================== Saved Servers ==================== - - private void setupSavedServers() { - Preference selectPref = findPreference("saved_servers"); - Preference savePref = findPreference("save_current_server"); - Preference removePref = findPreference("remove_saved_server"); - - if (selectPref != null) { - updateSavedServersSummary(selectPref); - selectPref.setOnPreferenceClickListener(p -> { - showSelectServerDialog(); - return true; - }); - } - - if (savePref != null) { - savePref.setOnPreferenceClickListener(p -> { - saveCurrentServer(); - return true; - }); - } + private void manageServerProfiles() { + Preference serverPref = findPreference("manage_servers"); - if (removePref != null) { - removePref.setOnPreferenceClickListener(p -> { - showRemoveServerDialog(); + if (serverPref != null) { + updateServerProfilesSummary(serverPref); + serverPref.setOnPreferenceClickListener(p -> { + startActivity(new Intent(requireContext(), ServerProfilesActivity.class)); return true; }); } } - private List getSavedServers() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - Set serverSet = prefs.getStringSet(PREF_SAVED_SERVERS, new HashSet<>()); - return new ArrayList<>(serverSet); - } - - private void putSavedServers(Set servers) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - prefs.edit().putStringSet(PREF_SAVED_SERVERS, new HashSet<>(servers)).apply(); - } - - private String getDisplayName(String entry) { - try { - JSONObject obj = new JSONObject(entry); - String name = obj.optString("name"); - String ip = obj.optString("ip"); - String port = obj.optString("port"); - return name + " (" + ip + ":" + port + ")"; - } catch (JSONException e) { - return entry; - } - } - private void updateSavedServersSummary(Preference pref) { - List servers = getSavedServers(); - if (servers.isEmpty()) { - pref.setSummary("No saved servers"); + private void updateServerProfilesSummary(Preference pref) { + ServerProfileRepository repo = new ServerProfileRepository(requireContext()); + int total = repo.getAll().size(); + int enabled = repo.getEnabled().size(); + if (total == 0) { + pref.setSummary("No server profiles configured"); } else { - pref.setSummary(servers.size() + " saved server(s)"); + pref.setSummary(enabled + " of " + total + " server(s) enabled"); } } - private void saveCurrentServer() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); - String name = prefs.getString("server_name", "").trim(); - String ip = prefs.getString("ip_address", "").trim(); - String port = prefs.getString("port", "").trim(); - String username = prefs.getString("username", "").trim(); - String password = SecurePrefs.get(requireContext(), "password", "").trim(); - String endpoint = prefs.getString("endpoint_path", "").trim(); + private void setupDiscoveryToggle() { + SwitchPreferenceCompat enableDiscovery = findPreference("discovery_service"); - if (ip.isEmpty() || port.isEmpty()) { - Toast.makeText(requireContext(), "Server address and port are required", Toast.LENGTH_SHORT).show(); - return; - } - - if (name.isEmpty()) { - name = ip + ":" + port; - } - - String serverKey = ip + ":" + port + endpoint; - JSONObject obj = new JSONObject(); - try { - obj.put("ip", ip); - obj.put("port", port); - obj.put("name", name); - obj.put("username", username); - obj.put("endpoint", endpoint); - } catch (JSONException e) { - e.printStackTrace(); - return; - } - - if (!password.isEmpty()) { - SecurePrefs.put(requireContext(), "server_pwd_" + serverKey, password); - } - - Set servers = new HashSet<>(getSavedServers()); - - servers.removeIf(s -> { - try { - JSONObject existing = new JSONObject(s); - return existing.optString("ip").equals(ip) && - existing.optString("port").equals(port) && - existing.optString("endpoint").equals(endpoint); - } catch (JSONException e) { - return false; - } - }); - - servers.add(obj.toString()); - putSavedServers(servers); - - Preference selectPref = findPreference("saved_servers"); - if (selectPref != null) updateSavedServersSummary(selectPref); - - Toast.makeText(requireContext(), "Server saved: " + name, Toast.LENGTH_SHORT).show(); - } - - private void showSelectServerDialog() { - List servers = getSavedServers(); - if (servers.isEmpty()) { - Toast.makeText(requireContext(), "No saved servers", Toast.LENGTH_SHORT).show(); - return; - } - - String[] displayNames = new String[servers.size()]; - for (int i = 0; i < servers.size(); i++) { - displayNames[i] = getDisplayName(servers.get(i)); - } - - new AlertDialog.Builder(requireContext()) - .setTitle("Select Server") - .setItems(displayNames, (dialog, which) -> { - try { - JSONObject obj = new JSONObject(servers.get(which)); - - EditTextPreference ipPref = findPreference("ip_address"); - EditTextPreference portPref = findPreference("port"); - EditTextPreference namePref = findPreference("server_name"); - EditTextPreference usernamePref = findPreference("username"); - EditTextPreference passwordPref = findPreference("password"); - EditTextPreference endpointPref = findPreference("endpoint_path"); - - if (ipPref != null) ipPref.setText(obj.optString("ip")); - if (portPref != null) portPref.setText(obj.optString("port")); - if (namePref != null) namePref.setText(obj.optString("name")); - if (usernamePref != null) usernamePref.setText(obj.optString("username")); - if (endpointPref != null) endpointPref.setText(obj.optString("endpoint")); - - String serverKey = obj.optString("ip") + ":" + obj.optString("port") + obj.optString("endpoint"); - String savedPwd = SecurePrefs.get(requireContext(), "server_pwd_" + serverKey, ""); - if (passwordPref != null) { - passwordPref.setText(savedPwd); - SecurePrefs.put(requireContext(), "password", savedPwd); - } - - Toast.makeText(requireContext(), "Loaded: " + obj.optString("name"), Toast.LENGTH_SHORT).show(); - - } catch (JSONException e) { - Toast.makeText(requireContext(), "Failed to load server", Toast.LENGTH_SHORT).show(); - } - }) - .setNegativeButton("Cancel", null) - .show(); - } - - private void showRemoveServerDialog() { - List servers = getSavedServers(); - if (servers.isEmpty()) { - Toast.makeText(requireContext(), "No saved servers to remove", Toast.LENGTH_SHORT).show(); - return; - } - - String[] displayNames = new String[servers.size()]; - boolean[] checked = new boolean[servers.size()]; - for (int i = 0; i < servers.size(); i++) { - displayNames[i] = getDisplayName(servers.get(i)); - checked[i] = false; - } - - new AlertDialog.Builder(requireContext()) - .setTitle("Remove Saved Servers") - .setMultiChoiceItems(displayNames, checked, (dialog, which, isChecked) -> - checked[which] = isChecked - ) - .setPositiveButton("Remove", (dialog, which) -> { - Set remaining = new HashSet<>(); - for (int i = 0; i < servers.size(); i++) { - if (!checked[i]) { - remaining.add(servers.get(i)); - } else { - try { - JSONObject obj = new JSONObject(servers.get(i)); - String serverKey = obj.optString("ip") + ":" + obj.optString("port") + obj.optString("endpoint"); - SecurePrefs.remove(requireContext(), "server_pwd_" + serverKey); - } catch (JSONException ignored) {} - } - } - putSavedServers(remaining); - - Preference selectPref = findPreference("saved_servers"); - if (selectPref != null) updateSavedServersSummary(selectPref); - - int removed = servers.size() - remaining.size(); - Toast.makeText(requireContext(), removed + " server(s) removed", Toast.LENGTH_SHORT).show(); - }) - .setNegativeButton("Cancel", null) - .show(); - } - - // ==================== Client Mode & OAuth ==================== - - private void setupOAuthToggle() { - SwitchPreferenceCompat clientMode = findPreference("enable_client"); - SwitchPreferenceCompat oauth = findPreference("o_auth_enabled"); - - Preference token = findPreference("token_endpoint"); - Preference clientId = findPreference("client_id"); - Preference secret = findPreference("client_secret"); - - if (clientMode != null) { - boolean isConSys = clientMode.isChecked(); - setVisibility(isConSys, oauth); - setVisibility(isConSys && oauth != null && oauth.isChecked(), token, clientId, secret); - - clientMode.setOnPreferenceChangeListener((pref, value) -> { - boolean enabled = (Boolean) value; - setVisibility(enabled, oauth); - setVisibility(enabled && oauth != null && oauth.isChecked(), token, clientId, secret); - return true; - }); - } + Preference rules = findPreference("rules_link"); - if (oauth != null) { - oauth.setOnPreferenceChangeListener((pref, value) -> { + if (enableDiscovery != null) { + enableDiscovery.setOnPreferenceChangeListener((pref, value) -> { boolean isEnabled = (Boolean) value; - setVisibility(isEnabled, token, clientId, secret); + setVisibility(isEnabled, rules); return true; }); } From 7b87e1503ab370f1d704167378cc4b9b133fc362 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 21 Apr 2026 10:59:10 -0500 Subject: [PATCH 15/26] fixed where rules was not hidden when discovery service was disabled --- sensorhub-android-app/res/xml/pref_settings.xml | 1 - .../src/org/sensorhub/android/SettingsFragment.java | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index 841a7dbe..bf520460 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -34,7 +34,6 @@
- { boolean isEnabled = (Boolean) value; setVisibility(isEnabled, rules); From d526f8fd5cba3183a8c9a028c22454cc35fe4a7e Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 22 Apr 2026 08:56:16 -0500 Subject: [PATCH 16/26] replaced printstacktrace with logs, and added validation to the server dialog + prevented it from closing on validation error --- build.gradle | 4 +- .../sensorhub/android/DashboardFragment.java | 7 ++ .../org/sensorhub/android/MainActivity.java | 8 +- .../sensorhub/android/SOSServiceWithIPC.java | 10 +- .../sensorhub/android/SensorsFragment.java | 12 ++- .../android/ServerProfilesActivity.java | 98 ++++++++++++------- .../sensor/swe/ProxySensor/ProxySensor.java | 2 +- .../sensorhub/android/SensorHubService.java | 11 +-- 8 files changed, 94 insertions(+), 58 deletions(-) diff --git a/build.gradle b/build.gradle index 4731a74b..16bdfe66 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ ext.oshCoreVersion = '2.0.0-beta' ext.compileSdkVersion = 33 -ext.minSdkVersion = 34 +ext.minSdkVersion = 33 ext.targetSdkVersion = 30 -ext.buildToolsVersion = "30.0.2" +ext.buildToolsVersion = "34.0.0" version = '4.0.0' buildscript { diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index 064a23c9..72b5d27b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -140,6 +140,13 @@ public void onPause() { super.onPause(); } + @Override + public void onDestroyView() { + stopRefreshingStatus(); + displayHandler.removeCallbacksAndMessages(null); + super.onDestroyView(); + } + private void updateFabIcon() { if (fab == null) return; if (provider.isOshStarted()) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 4bd24535..0d7f1ac8 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -14,10 +14,7 @@ package org.sensorhub.android; -import static org.sensorhub.android.SensorHubService.context; - import android.Manifest; -import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -126,6 +123,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.FutureTask; import javax.net.ssl.HostnameVerifier; @@ -369,7 +367,7 @@ public boolean verify(String arg0, SSLSession arg1) { discoveryServiceConfig.name= "Discovery Service"; discoveryServiceConfig.autoStart = true; - File outFile = new File(context.getFilesDir(), "rules.txt"); + 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); @@ -621,7 +619,6 @@ protected void addCSApiConfig(SensorConfig sensorConf, ServerProfile profile, UR } - @SuppressLint("HandlerLeak") @Override protected void onCreate(Bundle savedInstanceState) { @@ -843,6 +840,7 @@ protected void showAboutPopup() { PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); version = pInfo.versionName; } catch (PackageManager.NameNotFoundException e) { + log.warn("Could not retrieve package version", e); } String message = "A software platform for building smart sensor networks and the Internet of Things\n\n"; diff --git a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java index b8086c95..2cd5d5e3 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SOSServiceWithIPC.java @@ -14,12 +14,16 @@ import org.vast.xml.DOMHelperException; import org.w3c.dom.Element; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class SOSServiceWithIPC extends SOSService { + private static final Logger log = LoggerFactory.getLogger(SOSServiceWithIPC.class); public static final String SQAN_TEST = "SA"; private static final String SQAN_EXTRA = "channel"; public static final String ACTION_SOS = "org.sofwerx.ogc.ACTION_SOS"; @@ -91,15 +95,15 @@ private void handleIPCRequest(String body) } catch (DOMHelperException e) { - e.printStackTrace(); + log.error("Error parsing IPC request DOM", e); } catch (IOException e) { - e.printStackTrace(); + log.error("IO error handling IPC request", e); } catch (OWSException e) { - e.printStackTrace(); + log.error("OWS error handling IPC request", e); } // OGCException e /** diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index af718397..bc6a932e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -220,17 +220,19 @@ private void setupVideoPreferences() { } // Frame rates and resolutions from camera + Camera camera = null; try { - Camera camera = Camera.open(0); + 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); - camera.release(); } catch (Exception e) { frameRateList.add("30"); resList.add("640x480"); + } finally { + if (camera != null) camera.release(); } ListPreference frameRatePrefList = findPreference("video_framerate"); @@ -250,16 +252,16 @@ private void setupVideoPreferences() { } private void updateCameraSettings(int cameraId) { + Camera camera = null; try { frameRateList.clear(); resList.clear(); - Camera camera = Camera.open(cameraId); + 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); - camera.release(); ListPreference frameRatePrefList = findPreference("video_framerate"); if (frameRatePrefList != null) { @@ -273,6 +275,8 @@ private void updateCameraSettings(int cameraId) { } } catch (Exception e) { Log.e("SensorsFragment", "Error updating camera settings", e); + } finally { + if (camera != null) camera.release(); } } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java index b8208ec7..afc84137 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java @@ -7,6 +7,7 @@ import android.widget.TextView; import android.widget.Toast; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -131,46 +132,69 @@ private void showServerDialog(ServerProfile existing) { oauthSwitch.setVisibility(clientModeSwitch.isChecked() ? View.VISIBLE : View.GONE); updateOAuthVisibility.run(); - new MaterialAlertDialogBuilder(this) + AlertDialog dialog = new MaterialAlertDialogBuilder(this) .setTitle(isEdit ? "Edit Server" : "Add Server") .setView(dialogView) - .setPositiveButton("Save", (dialog, which) -> { - String name = nameInput.getText().toString().trim(); - String host = hostInput.getText().toString().trim(); - String portStr = portInput.getText().toString().trim(); - - if (name.isEmpty() || host.isEmpty() || portStr.isEmpty()) { - Toast.makeText(this, "Name, host, and port are required", - Toast.LENGTH_SHORT).show(); - return; - } - - ServerProfile profile = isEdit ? existing : new ServerProfile(); - profile.name = name; - profile.host = host; - profile.port = Integer.parseInt(portStr); - profile.endpointPath = endpointInput.getText().toString().trim(); - profile.username = usernameInput.getText().toString().trim(); - profile.enableTls = tlsSwitch.isChecked(); - profile.disableSslCheck = sslSwitch.isChecked(); - profile.useConSysClient = clientModeSwitch.isChecked(); - profile.oAuthEnabled = oauthSwitch.isChecked(); - - repo.save(profile); - - repo.setPassword(profile.id, - passwordInput.getText().toString().trim()); - repo.setOAuthClientId(profile.id, - clientIdInput.getText().toString().trim()); - repo.setOAuthClientSecret(profile.id, - clientSecretInput.getText().toString().trim()); - repo.setOAuthTokenEndpoint(profile.id, - tokenInput.getText().toString().trim()); - - refreshList(); - }) + .setPositiveButton("Save", null) .setNegativeButton("Cancel", null) - .show(); + .create(); + + dialog.show(); + + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { + 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, "Name, host, and port are required", Toast.LENGTH_SHORT).show(); + return; + } + + if (host.contains(" ") || host.contains("://")) { + Toast.makeText(this, "Host should not include a protocol (e.g. http://)", Toast.LENGTH_SHORT).show(); + return; + } + + int port; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + Toast.makeText(this, "Port should be a number", Toast.LENGTH_SHORT).show(); + return; + } + if (port < 1 || port > 65535) { + Toast.makeText(this, "Port should be between 1 and 65535", Toast.LENGTH_SHORT).show(); + return; + } + + if (!endpoint.isEmpty() && !endpoint.startsWith("/")) { + endpoint = "/" + endpoint; + } + + + ServerProfile profile = isEdit ? existing : new ServerProfile(); + profile.name = name; + profile.host = host; + profile.port = port; + profile.endpointPath = endpoint; + profile.username = usernameInput.getText().toString().trim(); + profile.enableTls = tlsSwitch.isChecked(); + profile.disableSslCheck = sslSwitch.isChecked(); + profile.useConSysClient = clientModeSwitch.isChecked(); + profile.oAuthEnabled = oauthSwitch.isChecked(); + + repo.save(profile); + + repo.setPassword(profile.id, passwordInput.getText().toString().trim()); + repo.setOAuthClientId(profile.id, clientIdInput.getText().toString().trim()); + repo.setOAuthClientSecret(profile.id, clientSecretInput.getText().toString().trim()); + repo.setOAuthTokenEndpoint(profile.id, tokenInput.getText().toString().trim()); + + refreshList(); + dialog.dismiss(); + }); } private void refreshList() { 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 7e259d4a..1d839606 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 @@ -64,7 +64,7 @@ public void onReceive(Context context, Intent intent) { try { stopSOSStreams(); } catch (SensorHubException e) { - e.printStackTrace(); + Log.e(TAG, "Error stopping SOS streams", e); } } } diff --git a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java index 3def7b87..9ec8354f 100644 --- a/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java +++ b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java @@ -42,8 +42,8 @@ public class SensorHubService extends Service private Handler msgHandler; SensorHubAndroid sensorhub; boolean hasVideo; - static Context context; - static SurfaceTexture videoTex; + private static Context appContext; + private static SurfaceTexture videoTex; private PowerManager.WakeLock wakeLock; private WifiManager.WifiLock wifiLock; @@ -66,7 +66,7 @@ public void onCreate() { try { - SensorHubService.context = getApplicationContext(); + SensorHubService.appContext = getApplicationContext(); SensorHubService.videoTex = new SurfaceTexture(1); SensorHubService.videoTex.detachFromGLContext(); @@ -92,7 +92,7 @@ public void onCreate() { } catch (Exception e) { - e.printStackTrace(); + log.error("Error: " + e.getMessage()); } } @@ -270,7 +270,6 @@ public void onDestroy() SensorHubService.videoTex.release(); SensorHubService.videoTex = null; } - SensorHubService.context = null; super.onDestroy(); } @@ -317,6 +316,6 @@ public static SurfaceTexture getVideoTexture() public static Context getContext() { - return context; + return appContext; } } \ No newline at end of file From db9eb0d0a79a3b61eb3c29eece30eb78281eb24e Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 22 Apr 2026 10:22:46 -0500 Subject: [PATCH 17/26] changed switching fragments to show/hide instead of .replace to prevent the fragment from destroying and removing the state of the fragment on switches. --- .../org/sensorhub/android/MainActivity.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 0d7f1ac8..7ca7d270 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -155,6 +155,7 @@ public class MainActivity extends AppCompatActivity implements SensorHubServiceP String deviceID; String runName; + private Fragment activeFragment; private BroadcastReceiver broadcastReceiver; enum Sensors { @@ -635,24 +636,34 @@ protected void onCreate(Bundle savedInstanceState) Fragment homeFragment = new DashboardFragment(); Fragment sensorsFragment = new SensorsFragment(); Fragment settingsFragment = new SettingsFragment(); + + getSupportFragmentManager().beginTransaction() + .add(R.id.flFragment, homeFragment, "dashboard") + .add(R.id.flFragment, sensorsFragment, "sensors") + .add(R.id.flFragment, settingsFragment, "settings") + .hide(sensorsFragment) + .hide(settingsFragment) + .commit(); + + activeFragment = homeFragment; + BottomNavigationView bottomNav = findViewById(R.id.bottom_nav); bottomNav.setOnNavigationItemSelectedListener(item -> { switch (item.getItemId()) { case R.id.dashboard: - setCurrentFragment(homeFragment); + switchFragment(homeFragment); break; case R.id.sensors: - setCurrentFragment(sensorsFragment); + switchFragment(sensorsFragment); break; case R.id.settings: - setCurrentFragment(settingsFragment); + switchFragment(settingsFragment); break; } return true; }); - setCurrentFragment(homeFragment); bottomNav.setSelectedItemId(R.id.dashboard); hasBluetoothPermissions(); @@ -667,11 +678,14 @@ protected void onCreate(Bundle savedInstanceState) requestBatteryOptimizationExemption(); } - private void setCurrentFragment(Fragment fragment) { + private void switchFragment(Fragment fragment) { + if (fragment == activeFragment) return; getSupportFragmentManager() .beginTransaction() - .replace(R.id.flFragment, fragment) + .hide(activeFragment) + .show(fragment) .commit(); + activeFragment = fragment; } @Override From 04df83f3e534fe7dcf00b202945d5af7e57a0ad4 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Thu, 23 Apr 2026 09:44:03 -0500 Subject: [PATCH 18/26] updated to latest addons --- submodules/botts-addons | 2 +- submodules/osh-addons | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/submodules/botts-addons b/submodules/botts-addons index 7381f8c0..4a337b81 160000 --- a/submodules/botts-addons +++ b/submodules/botts-addons @@ -1 +1 @@ -Subproject commit 7381f8c06d7cdbaa03aa9a66f4bc6451cb1d712b +Subproject commit 4a337b81c290240211a0d5a2ed41b9343e6c08dd diff --git a/submodules/osh-addons b/submodules/osh-addons index dfcd8e3f..0bd7c48c 160000 --- a/submodules/osh-addons +++ b/submodules/osh-addons @@ -1 +1 @@ -Subproject commit dfcd8e3fcf63acfa421ca292b0315a64bca60735 +Subproject commit 0bd7c48c3a471dce05c7fbab5416e180cdc88e2d From c5c12f71e1cdb295ddaaad03df7f0298dbba3c92 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 29 Apr 2026 10:31:09 -0500 Subject: [PATCH 19/26] added the services back to the settings tab --- sensorhub-android-app/res/xml/pref_settings.xml | 15 +++++++++++++++ .../src/org/sensorhub/android/MainActivity.java | 4 ++-- submodules/botts-addons | 2 +- submodules/osh-addons | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index bf520460..caf3b3bd 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -36,6 +36,21 @@ + + + + + enabledServers = serverRepo.getEnabled(); diff --git a/submodules/botts-addons b/submodules/botts-addons index 4a337b81..7271af1a 160000 --- a/submodules/botts-addons +++ b/submodules/botts-addons @@ -1 +1 @@ -Subproject commit 4a337b81c290240211a0d5a2ed41b9343e6c08dd +Subproject commit 7271af1a739256a4170e5281f62efea0a57b41dc diff --git a/submodules/osh-addons b/submodules/osh-addons index 0bd7c48c..029d3235 160000 --- a/submodules/osh-addons +++ b/submodules/osh-addons @@ -1 +1 @@ -Subproject commit 0bd7c48c3a471dce05c7fbab5416e180cdc88e2d +Subproject commit 029d3235e45bf3fe91b2ef619ab53fec7f7fc03e From f0cd298f8f5d7bc18488403359cc844934dd25e5 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 5 May 2026 11:24:58 +0800 Subject: [PATCH 20/26] added traditional chinese translations --- .../res/values-zh-rTW/strings.xml | 217 ++++++++++++++++++ .../strings_activity_user_settings.xml | 6 + .../res/values-zh-rTW/strings_app_status.xml | 22 ++ sensorhub-android-app/res/values/strings.xml | 56 ++++- .../res/xml/pref_sensors.xml | 134 +++++------ .../res/xml/pref_settings.xml | 14 +- 6 files changed, 373 insertions(+), 76 deletions(-) create mode 100644 sensorhub-android-app/res/values-zh-rTW/strings.xml create mode 100644 sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml create mode 100644 sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml diff --git a/sensorhub-android-app/res/values-zh-rTW/strings.xml b/sensorhub-android-app/res/values-zh-rTW/strings.xml new file mode 100644 index 00000000..132aadc9 --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings.xml @@ -0,0 +1,217 @@ + + + OpenSensorHub + 設定參數並點擊播放按鈕以啟動 SmartHub + 設定 + 啟動 SmartHub + 停止 SmartHub + 應用程式狀態 + 關於 + 啟動代理 + 停止代理 + Meshtastic 訊息 + SOS 設定(必填) + SOS-T 設定(選填) + Android 感測器 + TruPulse 測距感測器 + Meshtastic 感測器 + Angel 感測器 + Flirone 感測器 + Polar 心率監測器 + 控制器 + 紅隼感測器 + 雷達偵測感測器 + STE輻射尋呼機 感測器 + 範本 感測器 + 加速度計資料 + 陀螺儀資料 + 磁力計資料 + 方向資料(四元數) + 方向資料(歐拉角) + GPS 定位資料 + 網路定位資料 + 影片資料 + 影片滾動資料 + 音訊資料 + + 裝置名稱 + 裝置 IP 位址 + 伺服器設定檔 + 管理伺服器 + 服務 + 啟用 SOS 服務 + 啟用連線系統服務 + 啟用探索服務 + 執行名稱 + + 啟用加速度計資料串流 + 啟用陀螺儀資料串流 + 啟用磁力計資料串流 + 啟用方向資料串流 + 啟用 GPS 定位資料串流 + 啟用網路定位資料串流 + 啟用影片資料串流 + 在影片幀標頭中包含影片滾��資料 + 啟用音訊資料串流 + 啟用 Meshtastic 資料串流(感測器須在啟動時透過藍牙連接) + 啟用雷達偵測感測器串流 + 啟用 Polar 心���感測器串流(感測器須透過藍牙 LE 連接) + 啟用紅隼氣象儀串流 + 啟用 USB 控制器串流(控制器須在啟動時透過 USB 連接) + 啟用 TruPulse 測距儀資料串流(感測器須在啟動時透過藍牙連接) + 使用模擬 TruPulse 資料取代實際感測器資料 + 啟用 Angel 感測器健康資料串流(感測器須在啟動時透過藍牙 LE 連接) + 啟用 FLIR One 熱像儀資料串流(透過 USB 連接時) + 啟用 STE RadPager 資料串流��感測器須在啟動時透過藍牙 LE 連接) + 啟用範本驅動程式串流 + 資料推送選項 + 點擊以選擇或輸入裝置位址 + 感測器 UID 延伸 + 新增、編輯或刪除伺服器 + + + 四元數 + 歐拉角 + + + + GPS + 網路 + + + + 可擷取 + 本機儲存 + 遠端推送 + + + Trupulse 裝置名稱 + + 串流實體裝置 + 模擬虛擬裝置 + + 溪流 + + + + 選擇報告項目 + 道路封閉 + 淹水 + 醫療 + 救助 + + + + 最近信標 + 三邊測量 + + + + GPS + 雷射測距儀 + 網路 + + + 現場報告 + 名稱: + 描述: + 拍攝 + 重設 + 提交報告 + 報告名稱 + + 半徑: + 緯度: + 經度: + 英尺 + + + 選擇... + 公共 + 全部 + + + + 選擇... + 開放 + 關閉 + + + 動作: + 參考編號: + 類型: + + + 選擇... + 渠道排水 + 地表 + + + + 選擇... + 儀器 + 目視 + 模型 + + + 特徵類型: + 深度: + 觀測模式: + + 描述醫療狀況... + 輸入測量值(血壓、體溫等)... + 緊急狀況(是/否) + + + 選擇... + 環境 + 健康 + 安全 + 服務 + + + 救助類型: + 人數: + 緊急程度: + 描述所需救助... + 輸入您的姓名或編號 + + + 選擇... + 人員 + 車輛 + 裝置 + + + + 選擇... + GPS + 藍牙信標 + WiFi + 行動網路 + UWB + + + + 追蹤資源: + 追蹤方法: + 輸入資源編號 + 輸入資源標籤 + + + 英寸 + 密耳 + tmoa + smoa + + 公分 + + + 感測器 + 首頁 + 設定 + 輸入訊息! + 輸入訊息! + 輸入目標節點 ID(整數) + 輸入目標節點 ID(整數) + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml b/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml new file mode 100644 index 00000000..28e4904f --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml @@ -0,0 +1,6 @@ + + + 設定 + 一般 + 感測器 + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml new file mode 100644 index 00000000..29d5bc40 --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml @@ -0,0 +1,22 @@ + + + + 應用程式狀態 + + + 初始化中 + 已初始化 + 啟動中 + 已啟動 + 停止中 + 已停止 + 未知 + + + SOS 服務狀態 + ConSys 服務狀態 + 探索服務狀態 + HTTP 伺服器狀態 + Android 感測器狀態 + Android 感測器儲存狀態 + diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 82fb066a..61159a02 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -13,10 +13,62 @@ SOS Settings (Required) SOS-T Settings (Optional) Android Sensor - TruPulse Range Finder Sensor - Meshtastic Sensor Angel Sensor + Meshtastic Sensor + Wardriving Sensor + Polar Heart Monitor Sensor + Kestrel Sensor + Controller Sensor + TruPulse Range Finder Sensor Flirone Sensor + STE Radiation Pager Sensor + Template Sensor + Accelerometer Data + Gyroscope Data + Magnetometer Data + Orientation Data (Quaternions) + Orientation Data (Euler Angles) + GPS Location Data + Network Location Data + Video Data + Video Roll Data + Audio Data + + Device Name + Device IP Address + Server Profiles + Manage Servers + Services + Enable SOS Service + Enable Connected Systems Service + Enable Discovery Service + + Run Name + + Enable streaming of accelerometer data + Enable streaming of gyroscope data + Enable streaming of magnetometer data + Enable streaming of orientation data + Enable streaming of GPS location data + Enable streaming of network location data + Enable streaming of video data + Include video roll data in video frame header + Enable streaming of audio data + Enable streaming of Meshtastic data (sensor must be connected via Bluetooth on startup) + Enable streaming of Wardriving Sensor + Enable streaming of Polar Heart Sensor (sensor must be connected via Bluetooth LE) + Enable streaming of Kestrel Weather Meter + Enable streaming of USB Controller (controller must be connected to device via USB on startup) + Enable streaming of TruPulse range finder data (sensor must be connected via Bluetooth on startup) + Use simulated TruPulse data instead of the actual sensor data + Enable streaming of Angel Sensor health data (sensor must be connected via Bluetooth LE on startup) + Enable streaming of FLIR One thermal camera data when connected on USB port + Enable streaming of STE RadPager data (sensor must be connected via Bluetooth LE on startup) + Enable streaming of template driver + Options for pushing data + Tap to select or enter device address + Sensors UID Extension + Add, edit, or remove servers JPEG diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index a0d031f3..14fa1e44 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -9,21 +9,21 @@ android:maxLines="1" android:selectAllOnFocus="true" android:singleLine="true" - android:title="Sensors UID Extension" + android:title="@string/pref_uid_extension" android:layout="@layout/preference_item" /> @@ -396,8 +396,8 @@ @@ -28,8 +28,8 @@ @@ -40,21 +40,21 @@ From b556d706db393a63363fb041175e78fd6fef6e33 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 5 May 2026 14:48:37 +0800 Subject: [PATCH 21/26] fixed to chinese traditional --- build.gradle | 2 +- sensorhub-android-app/res/values-zh-rTW/strings.xml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 16bdfe66..0e81521b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ ext.compileSdkVersion = 33 ext.minSdkVersion = 33 ext.targetSdkVersion = 30 ext.buildToolsVersion = "34.0.0" -version = '4.0.0' +version = '4.0.1' buildscript { repositories { diff --git a/sensorhub-android-app/res/values-zh-rTW/strings.xml b/sensorhub-android-app/res/values-zh-rTW/strings.xml index 132aadc9..ee4f1bdc 100644 --- a/sensorhub-android-app/res/values-zh-rTW/strings.xml +++ b/sensorhub-android-app/res/values-zh-rTW/strings.xml @@ -51,18 +51,18 @@ 啟用 GPS 定位資料串流 啟用網路定位資料串流 啟用影片資料串流 - 在影片幀標頭中包含影片滾��資料 + 在影片幀標頭中包含影片滾動資料 啟用音訊資料串流 啟用 Meshtastic 資料串流(感測器須在啟動時透過藍牙連接) 啟用雷達偵測感測器串流 - 啟用 Polar 心���感測器串流(感測器須透過藍牙 LE 連接) + 啟用 Polar 心率感測器串流(感測器須透過藍牙 LE 連接) 啟用紅隼氣象儀串流 啟用 USB 控制器串流(控制器須在啟動時透過 USB 連接) 啟用 TruPulse 測距儀資料串流(感測器須在啟動時透過藍牙連接) 使用模擬 TruPulse 資料取代實際感測器資料 啟用 Angel 感測器健康資料串流(感測器須在啟動時透過藍牙 LE 連接) 啟用 FLIR One 熱像儀資料串流(透過 USB 連接時) - 啟用 STE RadPager 資料串流��感測器須在啟動時透過藍牙 LE 連接) + 啟用 STE RadPager 資料串流(感測器須在啟動時透過藍牙 LE 連接) 啟用範本驅動程式串流 資料推送選項 點擊以選擇或輸入裝置位址 @@ -90,7 +90,7 @@ 串流實體裝置 模擬虛擬裝置 - 溪流 + STREAM From 9505989837d4e69d884640d9737ee50a31bdbae4 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 5 May 2026 10:47:23 +0800 Subject: [PATCH 22/26] initial garmin additions --- .../res/xml/pref_sensors.xml | 23 ++++++++++++++++ .../org/sensorhub/android/MainActivity.java | 27 ++++++++++++++++++- .../sensorhub/android/SensorsFragment.java | 4 ++- sensorhub-android-lib/build.gradle | 1 + settings.gradle | 1 + 5 files changed, 54 insertions(+), 2 deletions(-) diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 14fa1e44..8ee32843 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -307,6 +307,29 @@ android:defaultValue="@array/sos_option_defaults" android:layout="@layout/preference_list_item" /> + + + + + frameRateList = new ArrayList<>(); diff --git a/sensorhub-android-lib/build.gradle b/sensorhub-android-lib/build.gradle index 00a387f2..ff0a9fe9 100644 --- a/sensorhub-android-lib/build.gradle +++ b/sensorhub-android-lib/build.gradle @@ -9,6 +9,7 @@ dependencies { api project(':sensorhub-driver-angelsensor') api project(':sensorhub-driver-android') api project(':sensorhub-driver-kestrel') + api project(':sensorhub-driver-garmin') // api project(':sensorhub-driver-flirone') // api project(':sensorhub-client-consys-okhttp') // api project(':sensorhub-android-flirone') diff --git a/settings.gradle b/settings.gradle index d9ac7b45..49363b4f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,6 +30,7 @@ def repos = [ // 'sensors/others/sensorhub-driver-meshtastic', 'sensors/weather/sensorhub-driver-kestrel', 'sensors/health/sensorhub-driver-angelsensor', + 'sensors/health/sensorhub-driver-garmin', 'processing/sensorhub-process-vecmath', 'processing/sensorhub-process-geoloc' ], From 81e4a009875dd469a65ddd227aac6209889c7c28 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Fri, 29 May 2026 12:38:30 -0500 Subject: [PATCH 23/26] update server profiles and polar hr monitor --- README.md | 1 + build.gradle | 2 +- sensorhub-android-app/AndroidManifest.xml | 5 + sensorhub-android-app/build.gradle | 15 - .../res/color/toggle_bg_selector.xml | 5 + .../res/color/toggle_text_selector.xml | 5 + .../layout/activity_edit_server_profile.xml | 390 ++++++++++ .../res/layout/activity_main.xml | 1 + .../res/layout/activity_server_profiles.xml | 36 +- .../res/layout/dialog_edit_server_profile.xml | 195 ----- .../res/values-zh-rTW/strings.xml | 10 +- sensorhub-android-app/res/values/strings.xml | 95 +-- .../values/strings_activity_user_settings.xml | 5 - sensorhub-android-app/res/values/styles.xml | 9 + .../res/xml/pref_sensors.xml | 735 ++++++------------ .../res/xml/pref_settings.xml | 5 +- .../android/EditServerProfileActivity.java | 192 +++++ .../org/sensorhub/android/MainActivity.java | 109 ++- .../sensorhub/android/SensorsFragment.java | 46 +- .../android/ServerProfilesActivity.java | 150 +--- sensorhub-android-lib/build.gradle | 2 +- sensorhub-android-polar/build.gradle | 2 +- .../sensor/polar/AccelerometerOutput.java | 99 +++ .../impl/sensor/polar/ECGOutput.java | 94 +++ .../impl/sensor/polar/PPIOutput.java | 113 +++ .../sensorhub/impl/sensor/polar/Polar.java | 339 ++++---- .../impl/sensor/polar/PolarConfig.java | 10 +- .../impl/sensor/polar/PolarDescriptor.java | 4 +- .../AndroidOrientationEulerOutput.java | 2 +- settings.gradle | 1 - 30 files changed, 1509 insertions(+), 1168 deletions(-) create mode 100644 sensorhub-android-app/res/color/toggle_bg_selector.xml create mode 100644 sensorhub-android-app/res/color/toggle_text_selector.xml create mode 100644 sensorhub-android-app/res/layout/activity_edit_server_profile.xml delete mode 100644 sensorhub-android-app/res/layout/dialog_edit_server_profile.xml delete mode 100644 sensorhub-android-app/res/values/strings_activity_user_settings.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java create mode 100644 sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/AccelerometerOutput.java create mode 100644 sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/ECGOutput.java create mode 100644 sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PPIOutput.java diff --git a/README.md b/README.md index 81058756..8b03cc43 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ An Android application that collects sensor data from phones and tablets and str - Meshtastic mesh radio (Bluetooth) - STE radiation pager - BLE beacons +- Garmin Wearable Sensor ## Requirements diff --git a/build.gradle b/build.gradle index 16bdfe66..341211dc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ ext.oshCoreVersion = '2.0.0-beta' -ext.compileSdkVersion = 33 +ext.compileSdkVersion = 34 ext.minSdkVersion = 33 ext.targetSdkVersion = 30 ext.buildToolsVersion = "34.0.0" diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index 0f4432b6..d5716724 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -63,6 +63,11 @@ android:configChanges="orientation|screenSize" android:screenOrientation="portrait" android:exported="false" /> + diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index f4899e8f..cc935964 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -64,21 +64,6 @@ android { applicationId "com.georobotix.android" } - //https://developer.android.com/build/build-variants#groovy -// flavorDimensions += "version" // maybe dont need -// productFlavors { -// create("free") { -// dimension = "version" -// applicationIdSuffix = ".free" -// buildConfigField("boolean", "IS_PREMIUM", "false") -// } -// create("premium") { -// dimension = "version" -// applicationIdSuffix = ".premium" -// buildConfigField("boolean", "IS_PREMIUM", "true") -// } -// } - compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 diff --git a/sensorhub-android-app/res/color/toggle_bg_selector.xml b/sensorhub-android-app/res/color/toggle_bg_selector.xml new file mode 100644 index 00000000..0d3ac19f --- /dev/null +++ b/sensorhub-android-app/res/color/toggle_bg_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/color/toggle_text_selector.xml b/sensorhub-android-app/res/color/toggle_text_selector.xml new file mode 100644 index 00000000..185b538c --- /dev/null +++ b/sensorhub-android-app/res/color/toggle_text_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/activity_edit_server_profile.xml b/sensorhub-android-app/res/layout/activity_edit_server_profile.xml new file mode 100644 index 00000000..c2b6618c --- /dev/null +++ b/sensorhub-android-app/res/layout/activity_edit_server_profile.xml @@ -0,0 +1,390 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/activity_main.xml b/sensorhub-android-app/res/layout/activity_main.xml index 4193be95..c511d319 100644 --- a/sensorhub-android-app/res/layout/activity_main.xml +++ b/sensorhub-android-app/res/layout/activity_main.xml @@ -40,6 +40,7 @@ android:src="@drawable/logo" /> + app:navigationIcon="@drawable/ic_arrow_back"> + + + + @@ -32,7 +49,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:text="No server profiles configured.\nTap + to add one." + android:text="No server profiles configured.\nTap Add to create one." android:textColor="@color/md_theme_onSurfaceVariant" android:textSize="14sp" android:visibility="gone" /> @@ -42,21 +59,8 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:padding="@dimen/content_padding" - android:paddingBottom="80dp" /> + android:padding="@dimen/content_padding" /> - - diff --git a/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml b/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml deleted file mode 100644 index a2384931..00000000 --- a/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sensorhub-android-app/res/values-zh-rTW/strings.xml b/sensorhub-android-app/res/values-zh-rTW/strings.xml index 132aadc9..59d961f9 100644 --- a/sensorhub-android-app/res/values-zh-rTW/strings.xml +++ b/sensorhub-android-app/res/values-zh-rTW/strings.xml @@ -10,8 +10,6 @@ 啟動代理 停止代理 Meshtastic 訊息 - SOS 設定(必填) - SOS-T 設定(選填) Android 感測器 TruPulse 測距感測器 Meshtastic 感測器 @@ -64,10 +62,16 @@ 啟用 FLIR One 熱像儀資料串流(透過 USB 連接時) 啟用 STE RadPager 資料串流��感測器須在啟動時透過藍牙 LE 連接) 啟用範本驅動程式串流 + Stream biometric data from the Garmin Wearable Sensor + + + + + 資料推送選項 點擊以選擇或輸入裝置位址 感測器 UID 延伸 - 新增、編輯或刪除伺服器 + 選用 — 新增伺服器以遠端推送資料。未設定伺服器時,資料僅於本機提供。 四元數 diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 61159a02..2610b173 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -9,66 +9,69 @@ About Start Proxy Stop Proxy - Meshtastic Msg - SOS Settings (Required) - SOS-T Settings (Optional) + Meshtastic Text Message Android Sensor - Angel Sensor - Meshtastic Sensor - Wardriving Sensor - Polar Heart Monitor Sensor - Kestrel Sensor - Controller Sensor - TruPulse Range Finder Sensor - Flirone Sensor - STE Radiation Pager Sensor - Template Sensor - Accelerometer Data - Gyroscope Data - Magnetometer Data - Orientation Data (Quaternions) - Orientation Data (Euler Angles) - GPS Location Data - Network Location Data + Angel Health Monitor + Meshtastic Radio + Wardriving + Polar Heart Monitor + Kestrel Weather Meter + USB Controller + TruPulse Range Finder + Flirone + STE Radiation Pager + Template + Accelerometer + Gyroscope + Magnetometer + Orientation (Quaternions) + Orientation (Euler Angles) + GPS Location + Network Location Video Data - Video Roll Data - Audio Data + Video Roll + Audio Device Name Device IP Address Server Profiles Manage Servers Services - Enable SOS Service - Enable Connected Systems Service - Enable Discovery Service + SOS Service + Connected Systems Service + Discovery Service Run Name - Enable streaming of accelerometer data - Enable streaming of gyroscope data - Enable streaming of magnetometer data - Enable streaming of orientation data - Enable streaming of GPS location data - Enable streaming of network location data - Enable streaming of video data - Include video roll data in video frame header - Enable streaming of audio data - Enable streaming of Meshtastic data (sensor must be connected via Bluetooth on startup) - Enable streaming of Wardriving Sensor - Enable streaming of Polar Heart Sensor (sensor must be connected via Bluetooth LE) - Enable streaming of Kestrel Weather Meter - Enable streaming of USB Controller (controller must be connected to device via USB on startup) - Enable streaming of TruPulse range finder data (sensor must be connected via Bluetooth on startup) - Use simulated TruPulse data instead of the actual sensor data - Enable streaming of Angel Sensor health data (sensor must be connected via Bluetooth LE on startup) - Enable streaming of FLIR One thermal camera data when connected on USB port - Enable streaming of STE RadPager data (sensor must be connected via Bluetooth LE on startup) - Enable streaming of template driver + Stream real-time accelerometer motion data + Stream real-time gyroscope rotation data + Stream magnetometer heading and magnetic field data + Stream device orientation and attitude data + Stream GPS-based location and movement data + Stream network-based location data + Stream live video from the device camera + Attach device roll metadata to video frame headers + Stream live audio from the device microphone + Connect to a Meshtastic device over Bluetooth during startup + Stream nearby wireless network scan data + Connect to a Polar heart rate sensor over Bluetooth LE + Stream environmental data from a Kestrel weather meter + Connect to a supported USB controller during startup + Stream distance and angle data from a TruPulse rangefinder + Use simulated TruPulse measurements for testing + Stream biometric data from the Angel Sensor + Stream thermal imagery from a connected FLIR One camera + Connect to an STE RadPager over Bluetooth LE during startup + Enable the template sensor driver for development and testing + Stream biometric data from the Garmin Wearable Sensor Options for pushing data + OGC standard REST API serving this device’s sensor data. + Legacy API serving sensor data as XML over HTTP + Service providing discovery based on definable rulesets Tap to select or enter device address + Sensors UID Extension - Add, edit, or remove servers + Optional — add servers to push data remotely. Without servers, data is served locally. JPEG diff --git a/sensorhub-android-app/res/values/strings_activity_user_settings.xml b/sensorhub-android-app/res/values/strings_activity_user_settings.xml deleted file mode 100644 index d17476b6..00000000 --- a/sensorhub-android-app/res/values/strings_activity_user_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - Settings - General - Sensors - diff --git a/sensorhub-android-app/res/values/styles.xml b/sensorhub-android-app/res/values/styles.xml index 80a26efd..f7d52d6c 100644 --- a/sensorhub-android-app/res/values/styles.xml +++ b/sensorhub-android-app/res/values/styles.xml @@ -241,4 +241,13 @@ @drawable/bg_status_chip + + \ 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 8ee32843..e01f08b7 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -10,494 +10,255 @@ android:selectAllOnFocus="true" android:singleLine="true" android:title="@string/pref_uid_extension" + android:summary="An ID attached to the end of each systems UID" 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 fa496da8..ea08c6ea 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -25,7 +25,7 @@ android:layout="@layout/preference_item" /> - + @@ -48,12 +49,14 @@ android:id="@+id/csapi_service_switch" android:key="csapi_service" android:title="@string/enable_csapi_service" + android:summary="@string/summary_csapi" android:defaultValue="true" android:layout="@layout/preference_switch_item" /> diff --git a/sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java b/sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java new file mode 100644 index 00000000..2856b676 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java @@ -0,0 +1,192 @@ +package org.sensorhub.android; + +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; + +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 = new ServerProfileRepository(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, "Profile not found", Toast.LENGTH_SHORT).show(); + finish(); + return; + } + + setupToolbar(); + bindViews(); + setupListeners(); + if (isEdit) populateFields(); + updateSslVisibility(); + + 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 ? "Edit Server" : "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 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, "Name, host, and port are required", Toast.LENGTH_SHORT).show(); + return; + } + + if (host.contains(" ") || host.contains("://")) { + Toast.makeText(this, "Host should not include a protocol (e.g. http://)", Toast.LENGTH_SHORT).show(); + return; + } + + int port; + try { + port = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + Toast.makeText(this, "Port should be a number", Toast.LENGTH_SHORT).show(); + return; + } + if (port < 1 || port > 65535) { + Toast.makeText(this, "Port should be between 1 and 65535", 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/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 3335fb0e..2302fb16 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -41,12 +41,14 @@ import android.view.View; import android.view.WindowManager; import android.widget.EditText; +import android.widget.TextView; import android.os.PowerManager; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -import com.botts.impl.driver.garmin.GarminConfig; +//import com.botts.impl.sensor.garmin.GarminConfig; +import com.botts.impl.sensor.garmin.GarminConfig; import com.botts.impl.service.discovery.DiscoveryService; import com.botts.impl.service.discovery.DiscoveryServiceConfig; import com.google.android.material.appbar.MaterialToolbar; @@ -111,20 +113,15 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.security.cert.X509Certificate; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.FutureTask; import javax.net.ssl.HostnameVerifier; @@ -157,6 +154,7 @@ public class MainActivity extends AppCompatActivity implements SensorHubServiceP String runName; private Fragment activeFragment; + private TextView toolbarTitle; private BroadcastReceiver broadcastReceiver; enum Sensors { @@ -398,7 +396,7 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(sensorsConfig); - if (isPushingSensor(Sensors.Android)) { + if (isPushingAnySensor()) { for (ServerProfile sp : enabledServers) { URL profileUrl = sp.buildClientUrl(); if (profileUrl == null) { @@ -507,7 +505,7 @@ public boolean verify(String arg0, SSLSession arg1) { polarConfig.name = "Polar Heart [" + deviceName + "]"; polarConfig.autoStart = true; polarConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; - polarConfig.device_name = prefs.getString("polar_device_address", ""); + polarConfig.deviceId = prefs.getString("polar_device_address", ""); polarConfig.uid_extension = prefs.getString("uid_extension", ""); sensorhubConfig.add(polarConfig); } @@ -571,20 +569,13 @@ public boolean verify(String arg0, SSLSession arg1) { // Garmin enabled = prefs.getBoolean("garmin_enabled", false); if (enabled) { -// BleConfig bleConf = new BleConfig(); -// bleConf.id = "BLE_NETWORK"; -// bleConf.moduleClass = BleNetwork.class.getCanonicalName(); -// bleConf.androidContext = this.getApplicationContext(); -// bleConf.autoStart = true; -// sensorhubConfig.add(bleConf); - GarminConfig garminConfig = new GarminConfig(); garminConfig.id = "GARMIN"; garminConfig.name = "Garmin [" + deviceName + "]"; garminConfig.autoStart = true; garminConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; -// garminConfig.networkID = bleConf.id; -// garminConfig.deviceAddress = prefs.getString("kestrel_device_address", ""); + garminConfig.sdkLicenseKey = BuildConfig.GARMIN_SDK_KEY; + garminConfig.deviceAddress = prefs.getString("garmin_device_address", ""); sensorhubConfig.add(garminConfig); } @@ -650,6 +641,7 @@ protected void onCreate(Bundle savedInstanceState) MaterialToolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); + toolbarTitle = findViewById(R.id.toolbar_title); if (getSupportActionBar() != null) { getSupportActionBar().setDisplayShowTitleEnabled(false); @@ -674,13 +666,13 @@ protected void onCreate(Bundle savedInstanceState) bottomNav.setOnNavigationItemSelectedListener(item -> { switch (item.getItemId()) { case R.id.dashboard: - switchFragment(homeFragment); + switchFragment(homeFragment, getString(R.string.app_name)); break; case R.id.sensors: - switchFragment(sensorsFragment); + switchFragment(sensorsFragment, getString(R.string.tab_sensors)); break; case R.id.settings: - switchFragment(settingsFragment); + switchFragment(settingsFragment, getString(R.string.tab_settings)); break; } return true; @@ -700,7 +692,7 @@ protected void onCreate(Bundle savedInstanceState) requestBatteryOptimizationExemption(); } - private void switchFragment(Fragment fragment) { + private void switchFragment(Fragment fragment, String title) { if (fragment == activeFragment) return; getSupportFragmentManager() .beginTransaction() @@ -708,6 +700,10 @@ private void switchFragment(Fragment fragment) { .show(fragment) .commit(); activeFragment = fragment; + if (toolbarTitle != null) { + toolbarTitle.setText(title); + } + invalidateOptionsMenu(); } @Override @@ -717,6 +713,16 @@ public boolean onCreateOptionsMenu(Menu menu) return true; } + @Override + public boolean onPrepareOptionsMenu(Menu menu) + { + boolean onDashboard = activeFragment instanceof DashboardFragment; + for (int i = 0; i < menu.size(); i++) { + menu.getItem(i).setVisible(onDashboard); + } + return super.onPrepareOptionsMenu(menu); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { @@ -893,64 +899,55 @@ boolean isPushingSensor(Sensors sensor) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (Sensors.Android.equals(sensor)) { - if (prefs.getBoolean("accel_enabled", false) - && prefs.getStringSet("accel_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("accel_enabled", false)) return true; - if (prefs.getBoolean("gyro_enabled", false) - && prefs.getStringSet("gyro_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("gyro_enabled", false)) return true; - if (prefs.getBoolean("mag_enabled", false) - && prefs.getStringSet("mag_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("mag_enabled", false)) return true; - if (prefs.getBoolean("orient_quat_enabled", false) - && prefs.getStringSet("orient_quat_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("orient_quat_enabled", false)) return true; - if (prefs.getBoolean("orient_euler_enabled", false) - && prefs.getStringSet("orient_euler_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("orient_euler_enabled", false)) return true; - if (prefs.getBoolean("gps_enabled", false) - && prefs.getStringSet("gps_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("gps_enabled", false)) return true; - if (prefs.getBoolean("netloc_enabled", false) - && prefs.getStringSet("netloc_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("netloc_enabled", false)) return true; - if (prefs.getBoolean("cam_enabled", false) - && prefs.getStringSet("cam_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("cam_enabled", false)) return true; - if (prefs.getBoolean("audio_enabled", false) - && prefs.getStringSet("audio_options", Collections.emptySet()).contains("PUSH_REMOTE")) + if (prefs.getBoolean("audio_enabled", false)) return true; } else if (Sensors.TruPulse.equals(sensor) || Sensors.TruPulseSim.equals(sensor)) { - return prefs.getBoolean("trupulse_enabled", false) - && prefs.getStringSet("trupulse_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("trupulse_enabled", false); } else if (Sensors.BLELocation.equals(sensor)) { - return prefs.getBoolean("ble_enable", false) && prefs.getStringSet("ble_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("ble_enable", false); } else if (Sensors.Meshtastic.equals(sensor)) { - return prefs.getBoolean("meshtastic_enabled", false) - && prefs.getStringSet("meshtastic_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("meshtastic_enabled", false); } else if (Sensors.PolarHRMonitor.equals(sensor)) { - return prefs.getBoolean("polar_enabled", false) - && prefs.getStringSet("polar_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("polar_enabled", false); } else if (Sensors.Kestrel.equals(sensor)) { - return prefs.getBoolean("kestrel_enabled", false) - && prefs.getStringSet("kestrel_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("kestrel_enabled", false); } else if (Sensors.Wardriving.equals(sensor)) { - return prefs.getBoolean("wardriving_enabled", false) - && prefs.getStringSet("wardriving_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("wardriving_enabled", false); } else if (Sensors.Controller.equals(sensor)) { - return prefs.getBoolean("controller_enabled", false) - && prefs.getStringSet("controller_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("controller_enabled", false); } else if (Sensors.Template.equals(sensor)) { - return prefs.getBoolean("template_enabled", false) - && prefs.getStringSet("template_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("template_enabled", false); } else if (Sensors.Garmin.equals(sensor)) { - return prefs.getBoolean("garmin_enabled", false) - && prefs.getStringSet("garmin_options", Collections.emptySet()).contains("PUSH_REMOTE"); + return prefs.getBoolean("garmin_enabled", false); } return false; } + boolean isPushingAnySensor() { + for (Sensors sensor : Sensors.values()) { + if (isPushingSensor(sensor)) + return true; + } + return false; + } + boolean shouldServe(SharedPreferences prefs) { Map prefMap = prefs.getAll(); for (Map.Entry pref : prefMap.entrySet()) { diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index 452a06e7..839203ed 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -36,38 +36,36 @@ public class SensorsFragment extends PreferenceFragmentCompat { private static final String[][] SWITCH_DEPENDENTS = { - {"accel_enabled", "accel_options"}, - {"gyro_enabled", "gyro_options"}, - {"mag_enabled", "mag_options"}, - {"orient_quat_enabled", "orient_quat_options"}, - {"orient_euler_enabled","orient_euler_options"}, - {"gps_enabled", "gps_options"}, - {"netloc_enabled", "netloc_options"}, - {"cam_enabled", "cam_options", "video_codec", "video_framerate", "video_resolution", "camera_select"}, - {"video_roll_enabled", "video_roll_options"}, - {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"}, - {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"}, - {"polar_enabled", "polar_device_address", "polar_options"}, - {"kestrel_enabled", "kestrel_device_address", "kestrel_options"}, - {"trupulse_enabled", "trupulse_datasource", "trupulse_options", "trupulse_device_address", "trupulse_simu"}, - {"angel_enabled", "angel_address", "angel_options"}, - {"flirone_enabled", "flir_options"}, - {"ste_radpager_enabled","ste_radpager_options"}, - {"wardriving_enabled", "wardriving_options"}, - {"controller_enabled", "controller_options"}, - {"template_enabled", "template_device_address", "template_options"}, - {"garmin_enabled", "garmin_device_address", "garmin_options"}, - + {"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"}, + {"meshtastic_enabled", "meshtastic_device_address"}, + {"polar_enabled", "polar_device_address"}, + {"kestrel_enabled", "kestrel_device_address"}, + {"trupulse_enabled", "trupulse_datasource", "trupulse_device_address", "trupulse_simu"}, + {"angel_enabled", "angel_address"}, + {"flirone_enabled"}, + {"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", - "garmin_device_address", }; private ArrayList frameRateList = new ArrayList<>(); @@ -176,7 +174,7 @@ private void showManualAddressDialog(String prefKey) { new AlertDialog.Builder(requireContext()) .setTitle("Enter Device Name or Address") - .setMessage("Enter a device name (e.g. \"Ballistic\") or MAC address. Names are matched from the start, case-insensitive.") + .setMessage("Enter a device name (e.g. \"Enviro\", \"TP\") or MAC address. Names are matched from the start, case-insensitive.") .setView(container) .setPositiveButton("OK", (dialog, which) -> { String address = input.getText().toString().trim(); diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java index afc84137..0aedd34a 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java @@ -1,22 +1,20 @@ package org.sensorhub.android; +import android.content.Intent; import android.os.Bundle; -import android.view.LayoutInflater; import android.view.View; -import android.widget.EditText; import android.widget.TextView; -import android.widget.Toast; -import androidx.appcompat.app.AlertDialog; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.appbar.MaterialToolbar; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.materialswitch.MaterialSwitch; import java.util.ArrayList; import java.util.List; @@ -29,6 +27,11 @@ public class ServerProfilesActivity extends AppCompatActivity implements ServerA 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); @@ -42,20 +45,25 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView = findViewById(R.id.server_list); emptyText = findViewById(R.id.empty_text); - FloatingActionButton fab = findViewById(R.id.fab_add_server); + + MaterialButton addButton = findViewById(R.id.btn_add_server); + addButton.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); - fab.setOnClickListener(v -> showServerDialog(null)); - refreshList(); } @Override public void onEditClicked(ServerProfile profile) { - showServerDialog(profile); + Intent intent = new Intent(this, EditServerProfileActivity.class); + intent.putExtra(EditServerProfileActivity.EXTRA_PROFILE_ID, profile.id); + editLauncher.launch(intent); } @Override @@ -76,131 +84,11 @@ public void onDeleteRequested(ServerProfile profile) { .show(); } - private void showServerDialog(ServerProfile existing) { - boolean isEdit = existing != null; - - View dialogView = LayoutInflater.from(this) - .inflate(R.layout.dialog_edit_server_profile, null); - - EditText nameInput = dialogView.findViewById(R.id.edit_name); - EditText hostInput = dialogView.findViewById(R.id.edit_host); - EditText portInput = dialogView.findViewById(R.id.edit_port); - EditText endpointInput = dialogView.findViewById(R.id.edit_endpoint); - EditText usernameInput = dialogView.findViewById(R.id.edit_username); - EditText passwordInput = dialogView.findViewById(R.id.edit_password); - - EditText tokenInput = dialogView.findViewById(R.id.edit_token_endpoint); - EditText clientIdInput = dialogView.findViewById(R.id.edit_client_id); - EditText clientSecretInput = dialogView.findViewById(R.id.edit_client_secret); - - MaterialSwitch tlsSwitch = dialogView.findViewById(R.id.switch_tls); - MaterialSwitch sslSwitch = dialogView.findViewById(R.id.switch_disable_ssl); - MaterialSwitch oauthSwitch = dialogView.findViewById(R.id.switch_oauth); - MaterialSwitch clientModeSwitch = dialogView.findViewById(R.id.switch_client_mode); - - View oauthFields = dialogView.findViewById(R.id.oauth_fields); - - Runnable updateOAuthVisibility = () -> { - boolean show = clientModeSwitch.isChecked() && oauthSwitch.isChecked(); - oauthFields.setVisibility(show ? View.VISIBLE : View.GONE); - }; - - clientModeSwitch.setOnCheckedChangeListener((btn, checked) -> { - oauthSwitch.setVisibility(checked ? View.VISIBLE : View.GONE); - updateOAuthVisibility.run(); - }); - oauthSwitch.setOnCheckedChangeListener((btn, checked) -> - updateOAuthVisibility.run()); - - if (isEdit) { - nameInput.setText(existing.name); - hostInput.setText(existing.host); - portInput.setText(String.valueOf(existing.port)); - endpointInput.setText(existing.endpointPath); - usernameInput.setText(existing.username); - tlsSwitch.setChecked(existing.enableTls); - sslSwitch.setChecked(existing.disableSslCheck); - clientModeSwitch.setChecked(existing.useConSysClient); - oauthSwitch.setChecked(existing.oAuthEnabled); - - passwordInput.setText(repo.getPassword(existing.id)); - tokenInput.setText(repo.getOAuthTokenEndpoint(existing.id)); - clientIdInput.setText(repo.getOAuthClientId(existing.id)); - clientSecretInput.setText(repo.getOAuthClientSecret(existing.id)); - } - - oauthSwitch.setVisibility(clientModeSwitch.isChecked() ? View.VISIBLE : View.GONE); - updateOAuthVisibility.run(); - - AlertDialog dialog = new MaterialAlertDialogBuilder(this) - .setTitle(isEdit ? "Edit Server" : "Add Server") - .setView(dialogView) - .setPositiveButton("Save", null) - .setNegativeButton("Cancel", null) - .create(); - - dialog.show(); - - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> { - 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, "Name, host, and port are required", Toast.LENGTH_SHORT).show(); - return; - } - - if (host.contains(" ") || host.contains("://")) { - Toast.makeText(this, "Host should not include a protocol (e.g. http://)", Toast.LENGTH_SHORT).show(); - return; - } - - int port; - try { - port = Integer.parseInt(portStr); - } catch (NumberFormatException e) { - Toast.makeText(this, "Port should be a number", Toast.LENGTH_SHORT).show(); - return; - } - if (port < 1 || port > 65535) { - Toast.makeText(this, "Port should be between 1 and 65535", Toast.LENGTH_SHORT).show(); - return; - } - - if (!endpoint.isEmpty() && !endpoint.startsWith("/")) { - endpoint = "/" + endpoint; - } - - - ServerProfile profile = isEdit ? existing : new ServerProfile(); - profile.name = name; - profile.host = host; - profile.port = port; - profile.endpointPath = endpoint; - profile.username = usernameInput.getText().toString().trim(); - profile.enableTls = tlsSwitch.isChecked(); - profile.disableSslCheck = sslSwitch.isChecked(); - profile.useConSysClient = clientModeSwitch.isChecked(); - profile.oAuthEnabled = oauthSwitch.isChecked(); - - repo.save(profile); - - repo.setPassword(profile.id, passwordInput.getText().toString().trim()); - repo.setOAuthClientId(profile.id, clientIdInput.getText().toString().trim()); - repo.setOAuthClientSecret(profile.id, clientSecretInput.getText().toString().trim()); - repo.setOAuthTokenEndpoint(profile.id, tokenInput.getText().toString().trim()); - - refreshList(); - dialog.dismiss(); - }); - } - private void refreshList() { servers.clear(); servers.addAll(repo.getAll()); adapter.notifyDataSetChanged(); emptyText.setVisibility(servers.isEmpty() ? View.VISIBLE : View.GONE); + emptyText.setText(servers.isEmpty() ? "No server profiles configured.\nTap Add to create one." : ""); } } diff --git a/sensorhub-android-lib/build.gradle b/sensorhub-android-lib/build.gradle index ff0a9fe9..d53b422d 100644 --- a/sensorhub-android-lib/build.gradle +++ b/sensorhub-android-lib/build.gradle @@ -9,7 +9,7 @@ dependencies { api project(':sensorhub-driver-angelsensor') api project(':sensorhub-driver-android') api project(':sensorhub-driver-kestrel') - api project(':sensorhub-driver-garmin') + api project(':sensorhub-android-garmin') // api project(':sensorhub-driver-flirone') // api project(':sensorhub-client-consys-okhttp') // api project(':sensorhub-android-flirone') diff --git a/sensorhub-android-polar/build.gradle b/sensorhub-android-polar/build.gradle index c8fac684..bbc4c9d2 100644 --- a/sensorhub-android-polar/build.gradle +++ b/sensorhub-android-polar/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.library' description = 'Polar HeartRate Monitors' ext.details = 'Driver for Polar H0/H10 HeartRate Monitors' -version = '2.0.1' +version = '3.0.0' dependencies { //api 'org.sensorhub:sensorhub-core:' + oshCoreVersion diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/AccelerometerOutput.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/AccelerometerOutput.java new file mode 100644 index 00000000..9f4fcf9d --- /dev/null +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/AccelerometerOutput.java @@ -0,0 +1,99 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.polar; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.helper.GeoPosHelper; + +/** + * Output for 3-axis accelerometer data from Polar H10. + * Streams motion data at configurable rates (25/50/100/200 Hz). + * + * @author Kalyn Stricklin + * @since 2025 + */ +public class AccelerometerOutput extends AbstractSensorOutput { + + private static final String SENSOR_OUTPUT_NAME = "accelerometer"; + private static final String SENSOR_OUTPUT_LABEL = "Accelerometer Output"; + private static final Logger logger = LoggerFactory.getLogger(AccelerometerOutput.class); + + private static final double MG_TO_MS2 = 9.80665 / 1000.0; + + DataRecord dataStruct; + DataEncoding dataEncoding; + + protected AccelerometerOutput(Polar parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .description("3-axis accelerometer data from Polar H10") + .addField("samplingTime", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sample Time")) + .addField("acceleration", fac.createAccelerationVector("m/s2")) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0 / 50.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + /** + * @param xMg X-axis acceleration in milligravity (mG) + * @param yMg Y-axis acceleration in milligravity (mG) + * @param zMg Z-axis acceleration in milligravity (mG) + */ + public void setData(int xMg, int yMg, int zMg) { + DataBlock dataBlock = dataStruct.createDataBlock(); + + dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); + dataBlock.setDoubleValue(1, xMg * MG_TO_MS2); + dataBlock.setDoubleValue(2, yMg * MG_TO_MS2); + dataBlock.setDoubleValue(3, zMg * MG_TO_MS2); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } +} diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/ECGOutput.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/ECGOutput.java new file mode 100644 index 00000000..38a26d12 --- /dev/null +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/ECGOutput.java @@ -0,0 +1,94 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.polar; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; + +/** + * Output for raw ECG waveform data from Polar H10. + * Streams electrocardiogram voltage samples at 130 Hz. + * + * @author Kalyn Stricklin + * @since 2025 + */ +public class ECGOutput extends AbstractSensorOutput { + + private static final String SENSOR_OUTPUT_NAME = "ecg"; + private static final String SENSOR_OUTPUT_LABEL = "ECG Output"; + private static final Logger logger = LoggerFactory.getLogger(ECGOutput.class); + + DataRecord dataStruct; + DataEncoding dataEncoding; + + protected ECGOutput(Polar parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + SWEHelper fac = new SWEHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("ECGWaveform")) + .description("Raw electrocardiogram waveform from Polar H10") + .addField("samplingTime", fac.createTime().asSamplingTimeIsoUTC()) + .addField("voltage", fac.createQuantity() + .label("ECG Voltage") + .definition(SWEHelper.getPropertyUri("Voltage")) + .description("ECG voltage sample") + .uom("uV") + .build()) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0 / 130.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + public void setData(int voltageUv) { + DataBlock dataBlock = dataStruct.createDataBlock(); + + dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); + dataBlock.setIntValue(1, voltageUv); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } +} diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PPIOutput.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PPIOutput.java new file mode 100644 index 00000000..751ad7e3 --- /dev/null +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PPIOutput.java @@ -0,0 +1,113 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.polar; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; +import net.opengis.swe.v20.DataRecord; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; + +/** + * Output for Pulse-to-Pulse Interval (PPI) data from Polar devices. + * Provides beat-to-beat intervals for heart rate variability (HRV) analysis. + * + * @author Kalyn Stricklin + * @since 2025 + */ +public class PPIOutput extends AbstractSensorOutput { + + private static final String SENSOR_OUTPUT_NAME = "ppi"; + private static final String SENSOR_OUTPUT_LABEL = "PPI / RR Interval Output"; + private static final Logger logger = LoggerFactory.getLogger(PPIOutput.class); + + DataRecord dataStruct; + DataEncoding dataEncoding; + + protected PPIOutput(Polar parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + SWEHelper fac = new SWEHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("PulseToPulseInterval")) + .description("Beat-to-beat pulse interval data for HRV analysis") + .addField("samplingTime", fac.createTime().asSamplingTimeIsoUTC()) + .addField("ppiInterval", fac.createQuantity() + .label("PPI Interval") + .definition(SWEHelper.getPropertyUri("PulseToPulseInterval")) + .description("Pulse-to-pulse interval (RR interval)") + .uom("ms") + .build()) + .addField("heartRate", fac.createQuantity() + .label("Heart Rate") + .definition(SWEHelper.getPropertyUri("HeartRate")) + .description("Heart rate derived from PPI") + .uom("1/min") + .build()) + .addField("skinContactSupported", fac.createBoolean() + .label("Skin Contact Supported") + .definition(SWEHelper.getPropertyUri("SkinContactSupported")) + .description("Whether the device supports skin contact detection") + .build()) + .addField("skinContactStatus", fac.createBoolean() + .label("Skin Contact Status") + .definition(SWEHelper.getPropertyUri("SkinContactStatus")) + .description("Whether the device has skin contact") + .build()) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + @Override + public double getAverageSamplingPeriod() { + return 1.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } + + public void setData(int ppiMs, int hr, boolean skinContactSupported, boolean skinContactStatus) { + DataBlock dataBlock = dataStruct.createDataBlock(); + + dataBlock.setDoubleValue(0, System.currentTimeMillis() / 1000d); + dataBlock.setIntValue(1, ppiMs); + dataBlock.setIntValue(2, hr); + dataBlock.setBooleanValue(3, skinContactSupported); + dataBlock.setBooleanValue(4, skinContactStatus); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } +} diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java index a73b6586..cc058183 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/Polar.java @@ -15,38 +15,26 @@ Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. package org.sensorhub.impl.sensor.polar; -import android.Manifest; import android.app.Activity; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothManager; import android.content.Context; -import android.content.pm.PackageManager; import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; - import com.polar.sdk.api.PolarBleApi; import com.polar.sdk.api.PolarBleApiCallback; import com.polar.sdk.api.PolarBleApiDefaultImpl; +import com.polar.sdk.api.model.PolarAccelerometerData; import com.polar.sdk.api.model.PolarDeviceInfo; +import com.polar.sdk.api.model.PolarEcgData; import com.polar.sdk.api.model.PolarHrData; +import com.polar.sdk.api.model.PolarPpiData; import net.opengis.sensorml.v20.PhysicalComponent; import org.sensorhub.android.SensorHubService; -import org.sensorhub.android.comm.ble.BleConfig; -import org.sensorhub.android.comm.ble.BleNetwork; -import org.sensorhub.api.comm.ble.GattCallback; -import org.sensorhub.api.comm.ble.IGattCharacteristic; -import org.sensorhub.api.comm.ble.IGattClient; -import org.sensorhub.api.comm.ble.IGattField; -import org.sensorhub.api.comm.ble.IGattService; -import org.sensorhub.api.common.SensorHubException; import org.sensorhub.api.sensor.SensorException; import org.sensorhub.impl.sensor.AbstractSensorModule; import org.sensorhub.impl.sensor.android.SensorMLBuilder; @@ -56,10 +44,24 @@ Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.UUID; +import io.reactivex.rxjava3.disposables.CompositeDisposable; +import io.reactivex.rxjava3.disposables.Disposable; + /** + * OpenSensorHub driver for Polar H9/H10 heart rate monitors. + *

+ * Uses the Polar BLE SDK for all communication, supporting: + *

    + *
  • Heart Rate (all models) via SDK callback
  • + *
  • Battery Level (all models) via SDK callback
  • + *
  • PPI / RR Intervals (all models) via SDK streaming
  • + *
  • ECG waveform at 130 Hz (H10 only) via SDK streaming
  • + *
  • 3-axis accelerometer (H10 only) via SDK streaming
  • + *
* * @author Kalyn Stricklin * @since Jan 13, 2023 @@ -70,24 +72,19 @@ public class Polar extends AbstractSensorModule { private final ArrayList smlComponents; private final SensorMLBuilder smlBuilder; static final Logger logger = LoggerFactory.getLogger(Polar.class.getSimpleName()); - private static final UUID HEARTRATE_CHARACTERISTIC_UUID = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"); - private static final UUID BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb"); - private static final UUID DEVICE_INFORMATION_SERVICE = UUID.fromString("0000180A-0000-1000-8000-00805F9B34FB"); - private static final UUID HEART_RATE_SERVICE = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb"); - private static final UUID BATTERY_SERVICE = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb"); private Context context; private BluetoothAdapter btAdapter; - BatteryOutput batteryOutput; + HeartRateOutput heartRateOutput; - private HandlerThread eventThread; - PolarBleApi api; + BatteryOutput batteryOutput; + PPIOutput ppiOutput; + ECGOutput ecgOutput; + AccelerometerOutput accelerometerOutput; - private boolean btConnected = false; - private BleNetwork bleNetwork; - private IGattClient gattClient; - private IGattCharacteristic heartRateChar; - private IGattCharacteristic batteryLevelChar; + PolarBleApi api; + private boolean deviceConnected = false; + private final CompositeDisposable disposables = new CompositeDisposable(); public Polar() { this.smlComponents = new ArrayList(); @@ -96,171 +93,141 @@ public Polar() { @Override public void doInit() { - logger.info("Initializing Polar heart monitor sensor"); + logger.info("Initializing Polar sensor driver"); this.xmlID = "POLAR_" + Build.SERIAL; this.uniqueID = UID_PREFIX + config.getUidWithExt(); context = SensorHubService.getContext(); - PolarApiCallback(); - - BleConfig bleConfig = new BleConfig(); - bleConfig.androidContext = context; - - bleNetwork = new BleNetwork(); - try { - bleNetwork.init(bleConfig); - bleNetwork.start(); - } catch (SensorHubException e) { - throw new RuntimeException(e); - } + initPolarApi(); heartRateOutput = new HeartRateOutput(this); heartRateOutput.doInit(); addOutput(heartRateOutput, false); + + batteryOutput = new BatteryOutput(this); + batteryOutput.doInit(); + addOutput(batteryOutput, false); + + if (config.enablePpi) { + ppiOutput = new PPIOutput(this); + ppiOutput.doInit(); + addOutput(ppiOutput, false); + } + + // H10 only + if (config.enableEcg) { + ecgOutput = new ECGOutput(this); + ecgOutput.doInit(); + addOutput(ecgOutput, false); + } + + // H10 only + if (config.enableAccelerometer) { + accelerometerOutput = new AccelerometerOutput(this); + accelerometerOutput.doInit(); + addOutput(accelerometerOutput, false); + } } @Override public void doStart() throws SensorException { - if (bleNetwork == null) { - logger.error("BLE network is not initialized"); + if (config.deviceId == null || config.deviceId.isEmpty()) { + throw new SensorException("Polar device ID is required (printed on the device)"); } - if (context.checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_DENIED) { - ActivityCompat.requestPermissions((Activity) context, new String[]{Manifest.permission.BLUETOOTH}, 1); + try { + api.connectToDevice(config.deviceId); + logger.info("Connecting to Polar device: {}", config.deviceId); + } catch (Exception e) { + throw new SensorException("Failed to connect to Polar device: " + config.deviceId, e); } - - bleNetwork.connectGatt(config.device_name, gattCallback); - - eventThread = new HandlerThread("PolarMonitorThread"); - eventThread.start(); - Handler eventHandler = new Handler(eventThread.getLooper()); } @Override public void doStop() { - if (gattClient != null) { - gattClient.disconnect(); - gattClient.close(); - gattClient = null; - } + disposables.clear(); - if (bleNetwork != null) { + if (api != null && config.deviceId != null) { try { - bleNetwork.stop(); - } catch (SensorHubException e) { - logger.error("Error stopping BLE network"); + api.disconnectFromDevice(config.deviceId); + } catch (Exception e) { + logger.error("Error disconnecting from Polar device", e); } - bleNetwork = null; } + + deviceConnected = false; } @Override public boolean isConnected() { - return true; + return deviceConnected; } - - private GattCallback gattCallback = new GattCallback() { - @Override - public void onConnected(IGattClient gatt, int status) { - gattClient = gatt; - btConnected = true; - - logger.info("Polar HR Monitor is connected"); - - gattClient.discoverServices(); - - } - - @Override - public void onDisconnected(IGattClient gatt, int status) { - btConnected = false; - logger.info("Polar HR Monitor is disconnected"); - } - - - @Override - public void onServicesDiscovered(IGattClient gatt, int status) { - IGattService hrService = null; - IGattService batteryService = null; - - for (IGattService service : gattClient.getServices()) { - - UUID uuid = service.getType(); - if (uuid.equals(HEART_RATE_SERVICE)) { - hrService = service; - } else if (uuid.equals(BATTERY_SERVICE)) { - batteryService = service; - } - } - - for (IGattCharacteristic characteristic : hrService.getCharacteristics()) { - UUID uuid = characteristic.getType(); - - if (uuid.equals(HEARTRATE_CHARACTERISTIC_UUID)) { - heartRateChar = characteristic; - } - } - - for (IGattCharacteristic characteristic : batteryService.getCharacteristics()) { - UUID uuid = characteristic.getType(); - - if (uuid.equals(BATTERY_LEVEL_CHARACTERISTIC_UUID)) { - batteryLevelChar = characteristic; - } - } - - if (heartRateChar != null) { - gattClient.setCharacteristicNotification(heartRateChar, true); - } - - if (batteryLevelChar != null) { - gattClient.setCharacteristicNotification(batteryLevelChar, true); - } - - if (batteryLevelChar != null) { - gattClient.readCharacteristic(batteryLevelChar); - } + private void startStreaming(Set availableTypes) { + String deviceId = config.deviceId; + + if (ppiOutput != null && availableTypes.contains(PolarBleApi.PolarDeviceDataType.PPI)) { + Disposable ppiDisposable = api.startPpiStreaming(deviceId) + .subscribe( + ppiData -> { + for (PolarPpiData.PolarPpiSample sample : ppiData.getSamples()) { + ppiOutput.setData( + sample.getPpi(), + sample.getHr(), + sample.getSkinContactSupported(), + sample.getSkinContactStatus() + ); + } + }, + error -> logger.error("PPI streaming error", error) + ); + disposables.add(ppiDisposable); + logger.info("PPI streaming started"); } - @Override - public void onCharacteristicChanged(IGattClient gatt, IGattField characteristic) { - - UUID uuid = characteristic.getType(); - - if (uuid.equals(HEARTRATE_CHARACTERISTIC_UUID)) { - byte[] data = characteristic.getValue().array(); - int hr = parseHeartRate(data); - heartRateOutput.setData(hr); - } - - if (uuid.equals(BATTERY_LEVEL_CHARACTERISTIC_UUID)) { - byte[] data = characteristic.getValue().array(); - int battery = data[0] & 0xFF; -// batteryOutput.setData(battery); - } + // ECG streaming H10 only + if (ecgOutput != null && availableTypes.contains(PolarBleApi.PolarDeviceDataType.ECG)) { + Disposable ecgDisposable = api.requestStreamSettings(deviceId, PolarBleApi.PolarDeviceDataType.ECG) + .flatMapPublisher(settings -> api.startEcgStreaming(deviceId, settings)) + .subscribe( + ecgData -> { + for (PolarEcgData.PolarEcgDataSample sample : ecgData.getSamples()) { + ecgOutput.setData(sample.getVoltage()); + } + }, + error -> logger.error("ECG streaming error", error) + ); + disposables.add(ecgDisposable); + logger.info("ECG streaming started"); } - }; - private int parseHeartRate(byte[] data) { - if (data == null || data.length == 0) - return 0; - - int heartRate = 0; - int format = data[0] & 0x01; - - if (format == 0) { - heartRate = data[1] & 0xFF; - } else { - heartRate = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8); + // Accelerometer streaming H10 only + if (accelerometerOutput != null && availableTypes.contains(PolarBleApi.PolarDeviceDataType.ACC)) { + Disposable accDisposable = api.requestStreamSettings(deviceId, PolarBleApi.PolarDeviceDataType.ACC) + .flatMapPublisher(settings -> api.startAccStreaming(deviceId, settings)) + .subscribe( + accData -> { + for (PolarAccelerometerData.PolarAccelerometerDataSample sample : accData.getSamples()) { + accelerometerOutput.setData( + sample.getX(), + sample.getY(), + sample.getZ() + ); + } + }, + error -> logger.error("Accelerometer streaming error", error) + ); + disposables.add(accDisposable); + logger.info("Accelerometer streaming started"); } - - return heartRate; } - public void PolarApiCallback() { - //set all desired features + /** + * Initializes the Polar BLE SDK with all supported features and sets up callbacks + * for HR, battery, device connection + */ + private void initPolarApi() { Set features = new HashSet<>(Arrays.asList( PolarBleApi.PolarBleSdkFeature.FEATURE_HR, PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_SDK_MODE, @@ -270,78 +237,87 @@ public void PolarApiCallback() { PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_OFFLINE_RECORDING, PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_ONLINE_STREAMING, PolarBleApi.PolarBleSdkFeature.FEATURE_POLAR_DEVICE_TIME_SETUP - )); + api = PolarBleApiDefaultImpl.defaultImplementation(context.getApplicationContext(), features); api.setApiCallback(new PolarBleApiCallback() { + @Override public void batteryLevelReceived(@NonNull String identifier, int level) { super.batteryLevelReceived(identifier, level); - logger.debug("Battery: " + level); + logger.debug("Battery level: {}%", level); + if (batteryOutput != null) { + batteryOutput.setData(level); + } } @Override public void blePowerStateChanged(boolean powered) { super.blePowerStateChanged(powered); if (powered) { - context = SensorHubService.getContext(); - final BluetoothManager bluetoothManager = (BluetoothManager) context.getSystemService(Activity.BLUETOOTH_SERVICE); + BluetoothManager bluetoothManager = + (BluetoothManager) context.getSystemService(Activity.BLUETOOTH_SERVICE); btAdapter = bluetoothManager.getAdapter(); if (btAdapter == null || !btAdapter.isEnabled()) { - Toast.makeText(context, "bluetooth adapter is null or not enabled", Toast.LENGTH_LONG).show(); + logger.warn("Bluetooth adapter is null or not enabled"); } } else { - Toast.makeText(context, "Powered is false, no bluetooth connection", Toast.LENGTH_LONG).show(); + logger.warn("Bluetooth powered off"); } - logger.debug("Bluetooth state changed " + powered); } @Override - public void bleSdkFeatureReady(@NonNull String identifier, @NonNull PolarBleApi.PolarBleSdkFeature feature) { + public void bleSdkFeatureReady(@NonNull String identifier, + @NonNull PolarBleApi.PolarBleSdkFeature feature) { super.bleSdkFeatureReady(identifier, feature); + logger.info("SDK feature ready: {}", feature); } @Override public void deviceConnected(@NonNull PolarDeviceInfo polarDeviceInfo) { super.deviceConnected(polarDeviceInfo); - Toast.makeText(context, "Connected to polar device", Toast.LENGTH_SHORT).show(); - isConnected(); + deviceConnected = true; + logger.info("Connected to Polar device: {}", polarDeviceInfo.getDeviceId()); } @Override public void deviceConnecting(@NonNull PolarDeviceInfo polarDeviceInfo) { super.deviceConnecting(polarDeviceInfo); - Toast.makeText(context, "Connecting to polar device", Toast.LENGTH_SHORT).show(); - logger.debug("connecting" + polarDeviceInfo.getDeviceId()); + logger.info("Connecting to Polar device: {}", polarDeviceInfo.getDeviceId()); } @Override public void deviceDisconnected(@NonNull PolarDeviceInfo polarDeviceInfo) { super.deviceDisconnected(polarDeviceInfo); - Toast.makeText(context, "Disconnected from polar device", Toast.LENGTH_SHORT).show(); - logger.debug("Device disconnected " + polarDeviceInfo.getDeviceId()); + deviceConnected = false; + logger.info("Disconnected from Polar device: {}", polarDeviceInfo.getDeviceId()); } @Override - public void disInformationReceived(@NonNull String identifier, @NonNull UUID uuid, @NonNull String value) { + public void disInformationReceived(@NonNull String identifier, + @NonNull UUID uuid, @NonNull String value) { super.disInformationReceived(identifier, uuid, value); + logger.debug("Device info - {}: {}", uuid, value); } @Override public void hrFeatureReady(@NonNull String identifier) { super.hrFeatureReady(identifier); - Toast.makeText(context, "Heart Rate feature ready for polar device", Toast.LENGTH_SHORT).show(); + logger.info("Heart Rate feature ready"); } @Override - public void hrNotificationReceived(@NonNull String identifier, @NonNull PolarHrData.PolarHrSample data) { + public void hrNotificationReceived(@NonNull String identifier, + @NonNull PolarHrData.PolarHrSample data) { super.hrNotificationReceived(identifier, data); - logger.debug("HR notifications received"); - int currentHR = data.getHr(); - logger.debug("Current HR: " + currentHR); - int restingHR = data.getRrsMs().get(0); - logger.debug("RRS: " + restingHR); + int hr = data.getHr(); + heartRateOutput.setData(hr); + + List rrsMs = data.getRrsMs(); + if (rrsMs != null && !rrsMs.isEmpty()) { + logger.trace("HR: {} bpm, RR intervals: {}", hr, rrsMs); + } } @Override @@ -355,11 +331,12 @@ public void sdkModeFeatureAvailable(@NonNull String identifier) { } @Override - public void streamingFeaturesReady(@NonNull String identifier, @NonNull Set features) { + public void streamingFeaturesReady(@NonNull String identifier, + @NonNull Set features) { super.streamingFeaturesReady(identifier, features); + logger.info("Streaming features ready: {}", features); + startStreaming(features); } - }); } - } diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java index 42a4a193..e8f576b1 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarConfig.java @@ -35,8 +35,16 @@ public PolarConfig() this.moduleClass = Polar.class.getCanonicalName(); } - public String device_name; + /** Polar device ID printed on the device (e.g. "A1B2C3D4") */ + public String deviceId; public String uid_extension; + public boolean enablePpi = true; + + /** Enable ECG waveform streaming (H10 only) */ + public boolean enableEcg = false; + + /** Enable accelerometer streaming (H10 only) */ + public boolean enableAccelerometer = false; public static String getUid() { Context context = SensorHubService.getContext(); diff --git a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java index 1ec1a479..0a62108b 100644 --- a/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java +++ b/sensorhub-android-polar/src/main/java/org/sensorhub/impl/sensor/polar/PolarDescriptor.java @@ -34,14 +34,14 @@ public class PolarDescriptor implements IModuleProvider @Override public String getModuleName() { - return "Polar Heart Rate Driver"; + return "Polar H9/H10 Driver"; } @Override public String getModuleDescription() { - return "Driver supporting Polar H9 and H10 Heart Rate Sensors"; + return "Driver for Polar H9/H10 sensors supporting HR, Battery, PPI, ECG, and Accelerometer via Polar SDK"; } diff --git a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidOrientationEulerOutput.java b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidOrientationEulerOutput.java index 232f594e..9fb868e0 100644 --- a/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidOrientationEulerOutput.java +++ b/sensorhub-driver-android/src/main/java/org/sensorhub/impl/sensor/android/AndroidOrientationEulerOutput.java @@ -100,7 +100,7 @@ public void onSensorChanged(SensorEvent e) AndroidOrientationQuatOutput.getQuaternionFromVector(att, e.values); att.normalize(); - // Y direction in phone ref frame + // Y direction in phone ref frame ( the top of the screen ) look.x = 0; look.y = 1; look.z = 0; diff --git a/settings.gradle b/settings.gradle index 49363b4f..d9ac7b45 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,7 +30,6 @@ def repos = [ // 'sensors/others/sensorhub-driver-meshtastic', 'sensors/weather/sensorhub-driver-kestrel', 'sensors/health/sensorhub-driver-angelsensor', - 'sensors/health/sensorhub-driver-garmin', 'processing/sensorhub-process-vecmath', 'processing/sensorhub-process-geoloc' ], From 3f435256cab0944bb961c75159bfdf64a4801185 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Tue, 2 Jun 2026 18:28:48 -0500 Subject: [PATCH 24/26] Move meshtastic message to its own card similar to video and remove from overflow menu --- .../res/layout/fragment_dashboard.xml | 69 +++++++++++++++ sensorhub-android-app/res/menu/main.xml | 7 -- .../sensorhub/android/DashboardFragment.java | 84 +++++++++++++++++++ .../org/sensorhub/android/MainActivity.java | 54 ------------ 4 files changed, 153 insertions(+), 61 deletions(-) diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml index b916bc37..c544846c 100644 --- a/sensorhub-android-app/res/layout/fragment_dashboard.xml +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -87,6 +87,75 @@ + + + + + + + + + + + + + + + + + + + + - - { @@ -69,6 +76,8 @@ public class DashboardFragment extends Fragment implements TextureView.SurfaceTe private TextureView textureView; private MaterialCardView videoStatusCard; private MaterialButton btnToggleVideo; + private MaterialCardView meshtasticCard; + private View videoStatusDot; private FloatingActionButton fab; private LinearLayout serverStatusContainer; @@ -110,6 +119,9 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat btnToggleVideo.setOnClickListener(v -> toggleVideoPreview()); + meshtasticCard = view.findViewById(R.id.meshtastic_card); + view.findViewById(R.id.btn_meshtastic_msg).setOnClickListener(v -> showMeshtasticDialog()); + serverStatusContainer = view.findViewById(R.id.server_status_container); fab = view.findViewById(R.id.fab_toggle); @@ -131,6 +143,7 @@ public void onResume() { if (provider.isOshStarted()) { startRefreshingStatus(); updateVideoStatusCard(); + updateMeshtasticCard(); } } @@ -164,6 +177,7 @@ private void stopHub() { hideVideoPreview(); clearTextureView(); videoStatusCard.setVisibility(View.GONE); + if (meshtasticCard != null) meshtasticCard.setVisibility(View.GONE); newStatusMessage("SensorHub Stopped"); requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -271,6 +285,7 @@ private void pollHubReady() { serverCardViews.clear(); startRefreshingStatus(); updateVideoStatusCard(); + updateMeshtasticCard(); if (videoPreviewVisible) showVideo(); } @@ -588,6 +603,75 @@ protected void showVideo() { } } + private void updateMeshtasticCard() { + if (meshtasticCard == null) return; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + boolean meshtasticEnabled = prefs.getBoolean("meshtastic_enabled", false); + boolean show = meshtasticEnabled && provider.isOshStarted(); + meshtasticCard.setVisibility(show ? View.VISIBLE : View.GONE); + + if (show) { + View dot = meshtasticCard.findViewById(R.id.meshtastic_status_dot); + if (dot != null && dot.getBackground() instanceof GradientDrawable) { + GradientDrawable bg = (GradientDrawable) dot.getBackground(); + bg.setColor(ContextCompat.getColor(requireContext(), R.color.status_started)); + } + } + } + + private void showMeshtasticDialog() { + View dialogView = LayoutInflater.from(requireContext()) + .inflate(R.layout.dialog_meshtastic, null); + + EditText messageInput = dialogView.findViewById(R.id.msg_input); + EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("Send Meshtastic Message") + .setView(dialogView) + .setPositiveButton("Send", (dialog, id) -> { + String msg = messageInput.getText().toString(); + String destinationId = destinationIdText.getText().toString(); + sendMeshtasticMessage(msg, destinationId); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void sendMeshtasticMessage(String message, String nodeId) { + SensorHubService service = provider.getBoundService(); + if (service == null || service.getSensorHub() == null) { + Toast.makeText(requireContext(), "SensorHub not running", Toast.LENGTH_SHORT).show(); + return; + } + + try { + ModuleRegistry reg = (ModuleRegistry) service.getSensorHub().getModuleRegistry(); + MeshtasticSensor meshy = reg.getModuleByType(MeshtasticSensor.class); + + IStreamingControlInterface textMessageControl = + meshy.getCommandInputs().get(TextMessageControl.NAME); + + DataBlock cmdData = textMessageControl.getCommandDescription().createDataBlock(); + cmdData.setStringValue(0, message); + cmdData.setIntValue(1, Integer.parseInt(nodeId)); + + String deviceID = android.provider.Settings.Secure.getString( + requireContext().getContentResolver(), + android.provider.Settings.Secure.ANDROID_ID); + + var cmd = new CommandData.Builder() + .withCommandStream(BigId.NONE) + .withSender(deviceID) + .withParams(cmdData) + .build(); + + textMessageControl.submitCommand(cmd); + Toast.makeText(requireContext(), "Message sent", Toast.LENGTH_SHORT).show(); + } catch (Exception e) { + Toast.makeText(requireContext(), "Failed to send message", Toast.LENGTH_SHORT).show(); + } + } @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { if (videoPreviewVisible) showVideo(); diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 2302fb16..2d521090 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -87,8 +87,6 @@ import android.view.MotionEvent; import org.sensorhub.impl.sensor.kestrel.KestrelConfig; import org.sensorhub.impl.sensor.meshtastic.MeshtasticConfig; -import org.sensorhub.impl.sensor.meshtastic.MeshtasticSensor; -import org.sensorhub.impl.sensor.meshtastic.control.TextMessageControl; import org.sensorhub.impl.sensor.polar.PolarConfig; import org.sensorhub.impl.sensor.ste.STERadPagerConfig; import org.sensorhub.impl.sensor.template.TemplateConfig; @@ -732,11 +730,6 @@ public boolean onOptionsItemSelected(MenuItem item) showAboutPopup(); return true; } - else if (id == R.id.action_meshtastic) - { - showMeshtasticDialog(); - return true; - } else if(id == R.id.action_status) { Intent statusIntent = new Intent(this, AppStatusActivity.class); @@ -828,53 +821,6 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) { return super.dispatchGenericMotionEvent(event); } - - protected void showMeshtasticDialog() { - LayoutInflater inflater = getLayoutInflater(); - View dialogView = inflater.inflate(R.layout.dialog_meshtastic, null); - - EditText messageInput = dialogView.findViewById(R.id.msg_input); - EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); - - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); - builder.setTitle("Send Meshtastic Message"); - builder.setView(dialogView); - - builder.setPositiveButton("Send", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - String msg = messageInput.getText().toString(); - String destinationId = destinationIdText.getText().toString(); - try { - sendMeshtasticMessage(msg, destinationId); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - - builder.setNegativeButton("Cancel", null); - builder.show(); - } - - private void sendMeshtasticMessage(String message, String nodeId) throws IOException { - ModuleRegistry reg = boundService.getSensorHub().getModuleRegistry(); - MeshtasticSensor meshy = reg.getModuleByType(MeshtasticSensor.class); - - IStreamingControlInterface textMessageControl = meshy.getCommandInputs().get(TextMessageControl.NAME); - - DataBlock cmdData = textMessageControl.getCommandDescription().createDataBlock(); - cmdData.setStringValue(0, message); - cmdData.setIntValue(1, Integer.parseInt(nodeId)); - - var cmd = new CommandData.Builder() - .withCommandStream(BigId.NONE) - .withSender(deviceID) - .withParams(cmdData) - .build(); - - textMessageControl.submitCommand(cmd); - } - protected void showAboutPopup() { String version = "?"; From cb2e99138510298d1708db5fc9646286379fcd3d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Fri, 5 Jun 2026 08:39:36 -0500 Subject: [PATCH 25/26] added app preferences with language and help support --- sensorhub-android-app/AndroidManifest.xml | 18 +- .../res/color/toggle_text_selector.xml | 4 +- .../res/drawable/bg_pref_bottom.xml | 8 + .../res/drawable/bg_pref_mid.xml | 5 + .../res/drawable/bg_pref_single.xml | 6 + .../res/drawable/bg_pref_top.xml | 8 + sensorhub-android-app/res/drawable/ic_add.xml | 12 +- .../res/drawable/ic_arrow_back.xml | 4 +- .../res/drawable/ic_chevron_right.xml | 9 + .../res/drawable/ic_edit.xml | 3 +- .../res/drawable/ic_flip_camera.xml | 10 + .../res/drawable/ic_help.xml | 9 + .../res/drawable/ic_home.xml | 4 +- .../res/drawable/ic_info.xml | 4 +- .../res/drawable/ic_language.xml | 9 + .../res/drawable/ic_message.xml | 4 +- .../res/drawable/ic_notifications.xml | 9 + .../res/drawable/ic_play.xml | 4 +- .../res/drawable/ic_sensors.xml | 4 +- .../res/drawable/ic_settings.xml | 4 +- .../res/drawable/ic_stop.xml | 4 +- .../res/drawable/ic_tune.xml | 9 + .../layout/activity_edit_server_profile.xml | 58 ++- .../res/layout/activity_help_faq.xml | 36 ++ .../res/layout/activity_main.xml | 51 ++- .../res/layout/activity_server_profiles.xml | 35 +- .../res/layout/fragment_app_preferences.xml | 34 ++ .../res/layout/fragment_dashboard.xml | 21 +- .../res/layout/fragment_sensors.xml | 2 +- .../res/layout/item_faq_entry.xml | 56 +++ .../res/layout/item_faq_header.xml | 15 + .../res/layout/item_server_profile.xml | 5 +- .../res/layout/pref_edit_bottom.xml | 28 ++ .../res/layout/pref_icon_bottom.xml | 42 ++ .../res/layout/pref_icon_mid.xml | 42 ++ .../res/layout/pref_icon_top.xml | 42 ++ .../res/layout/pref_switch_bottom.xml | 46 ++ .../res/layout/pref_switch_mid.xml | 46 ++ .../res/layout/pref_switch_top.xml | 46 ++ .../res/layout/pref_value_bottom.xml | 30 ++ .../res/layout/pref_value_mid.xml | 30 ++ .../res/layout/pref_value_single.xml | 30 ++ .../res/layout/pref_value_top.xml | 30 ++ sensorhub-android-app/res/menu/main.xml | 12 - .../res/values-de/strings.xml | 419 ++++++++++++++++++ .../res/values-de/strings_app_status.xml | 21 + .../res/values-de/strings_help_faq.xml | 75 ++++ .../res/values-es/strings.xml | 187 ++++++++ .../res/values-es/strings_app_status.xml | 21 + .../res/values-es/strings_help_faq.xml | 63 +++ .../res/values-fr/strings.xml | 419 ++++++++++++++++++ .../res/values-fr/strings_app_status.xml | 21 + .../res/values-fr/strings_help_faq.xml | 75 ++++ .../res/values-it/strings.xml | 419 ++++++++++++++++++ .../res/values-it/strings_app_status.xml | 21 + .../res/values-it/strings_help_faq.xml | 75 ++++ .../res/values-pt/strings.xml | 419 ++++++++++++++++++ .../res/values-pt/strings_app_status.xml | 21 + .../res/values-pt/strings_help_faq.xml | 75 ++++ .../res/values-zh-rTW/strings.xml | 350 +++++++-------- .../strings_activity_user_settings.xml | 6 - .../res/values-zh-rTW/strings_app_status.xml | 1 - .../res/values-zh-rTW/strings_help_faq.xml | 73 +++ sensorhub-android-app/res/values/strings.xml | 119 ++++- .../res/values/strings_help_faq.xml | 73 +++ .../res/xml/locales_config.xml | 10 + sensorhub-android-app/res/xml/pref_app.xml | 58 +++ .../res/xml/pref_sensors.xml | 42 +- .../res/xml/pref_settings.xml | 27 +- .../android/AppPreferencesActivity.java | 108 +++++ .../android/AppPreferencesFragment.java | 128 ++++++ .../sensorhub/android/DashboardFragment.java | 47 +- .../sensorhub/android/HelpFaqActivity.java | 142 ++++++ .../org/sensorhub/android/MainActivity.java | 224 +++------- .../sensorhub/android/SensorsFragment.java | 16 +- .../sensorhub/android/SettingsFragment.java | 29 +- .../EditServerProfileActivity.java | 16 +- .../android/{ => server}/ServerAdapter.java | 4 +- .../android/{ => server}/ServerProfile.java | 2 +- .../{ => server}/ServerProfileRepository.java | 3 +- .../{ => server}/ServerProfilesActivity.java | 23 +- 81 files changed, 4105 insertions(+), 615 deletions(-) create mode 100644 sensorhub-android-app/res/drawable/bg_pref_bottom.xml create mode 100644 sensorhub-android-app/res/drawable/bg_pref_mid.xml create mode 100644 sensorhub-android-app/res/drawable/bg_pref_single.xml create mode 100644 sensorhub-android-app/res/drawable/bg_pref_top.xml create mode 100644 sensorhub-android-app/res/drawable/ic_chevron_right.xml create mode 100644 sensorhub-android-app/res/drawable/ic_flip_camera.xml create mode 100644 sensorhub-android-app/res/drawable/ic_help.xml create mode 100644 sensorhub-android-app/res/drawable/ic_language.xml create mode 100644 sensorhub-android-app/res/drawable/ic_notifications.xml create mode 100644 sensorhub-android-app/res/drawable/ic_tune.xml create mode 100644 sensorhub-android-app/res/layout/activity_help_faq.xml create mode 100644 sensorhub-android-app/res/layout/fragment_app_preferences.xml create mode 100644 sensorhub-android-app/res/layout/item_faq_entry.xml create mode 100644 sensorhub-android-app/res/layout/item_faq_header.xml create mode 100644 sensorhub-android-app/res/layout/pref_edit_bottom.xml create mode 100644 sensorhub-android-app/res/layout/pref_icon_bottom.xml create mode 100644 sensorhub-android-app/res/layout/pref_icon_mid.xml create mode 100644 sensorhub-android-app/res/layout/pref_icon_top.xml create mode 100644 sensorhub-android-app/res/layout/pref_switch_bottom.xml create mode 100644 sensorhub-android-app/res/layout/pref_switch_mid.xml create mode 100644 sensorhub-android-app/res/layout/pref_switch_top.xml create mode 100644 sensorhub-android-app/res/layout/pref_value_bottom.xml create mode 100644 sensorhub-android-app/res/layout/pref_value_mid.xml create mode 100644 sensorhub-android-app/res/layout/pref_value_single.xml create mode 100644 sensorhub-android-app/res/layout/pref_value_top.xml create mode 100644 sensorhub-android-app/res/values-de/strings.xml create mode 100644 sensorhub-android-app/res/values-de/strings_app_status.xml create mode 100644 sensorhub-android-app/res/values-de/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/values-es/strings.xml create mode 100644 sensorhub-android-app/res/values-es/strings_app_status.xml create mode 100644 sensorhub-android-app/res/values-es/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/values-fr/strings.xml create mode 100644 sensorhub-android-app/res/values-fr/strings_app_status.xml create mode 100644 sensorhub-android-app/res/values-fr/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/values-it/strings.xml create mode 100644 sensorhub-android-app/res/values-it/strings_app_status.xml create mode 100644 sensorhub-android-app/res/values-it/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/values-pt/strings.xml create mode 100644 sensorhub-android-app/res/values-pt/strings_app_status.xml create mode 100644 sensorhub-android-app/res/values-pt/strings_help_faq.xml delete mode 100644 sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml create mode 100644 sensorhub-android-app/res/values-zh-rTW/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/values/strings_help_faq.xml create mode 100644 sensorhub-android-app/res/xml/locales_config.xml create mode 100644 sensorhub-android-app/res/xml/pref_app.xml create mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppPreferencesActivity.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java create mode 100644 sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java rename sensorhub-android-app/src/org/sensorhub/android/{ => server}/EditServerProfileActivity.java (91%) rename sensorhub-android-app/src/org/sensorhub/android/{ => server}/ServerAdapter.java (97%) rename sensorhub-android-app/src/org/sensorhub/android/{ => server}/ServerProfile.java (98%) rename sensorhub-android-app/src/org/sensorhub/android/{ => server}/ServerProfileRepository.java (98%) rename sensorhub-android-app/src/org/sensorhub/android/{ => server}/ServerProfilesActivity.java (82%) diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index d5716724..c19d1841 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -40,7 +40,8 @@ android:label="@string/app_name" android:largeHeap="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true"> + android:usesCleartextTraffic="true" + android:localeConfig="@xml/locales_config"> + + diff --git a/sensorhub-android-app/res/color/toggle_text_selector.xml b/sensorhub-android-app/res/color/toggle_text_selector.xml index 185b538c..b8f3a3cb 100644 --- a/sensorhub-android-app/res/color/toggle_text_selector.xml +++ b/sensorhub-android-app/res/color/toggle_text_selector.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/bg_pref_bottom.xml b/sensorhub-android-app/res/drawable/bg_pref_bottom.xml new file mode 100644 index 00000000..72292a03 --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_pref_bottom.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable/bg_pref_mid.xml b/sensorhub-android-app/res/drawable/bg_pref_mid.xml new file mode 100644 index 00000000..7a82207a --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_pref_mid.xml @@ -0,0 +1,5 @@ + + + + diff --git a/sensorhub-android-app/res/drawable/bg_pref_single.xml b/sensorhub-android-app/res/drawable/bg_pref_single.xml new file mode 100644 index 00000000..2c713c20 --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_pref_single.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable/bg_pref_top.xml b/sensorhub-android-app/res/drawable/bg_pref_top.xml new file mode 100644 index 00000000..0ab588f2 --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_pref_top.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable/ic_add.xml b/sensorhub-android-app/res/drawable/ic_add.xml index 9f83b8fb..3b69ed3b 100644 --- a/sensorhub-android-app/res/drawable/ic_add.xml +++ b/sensorhub-android-app/res/drawable/ic_add.xml @@ -1,5 +1,9 @@ - - - - + + diff --git a/sensorhub-android-app/res/drawable/ic_arrow_back.xml b/sensorhub-android-app/res/drawable/ic_arrow_back.xml index 14401611..eea9cdfb 100644 --- a/sensorhub-android-app/res/drawable/ic_arrow_back.xml +++ b/sensorhub-android-app/res/drawable/ic_arrow_back.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_chevron_right.xml b/sensorhub-android-app/res/drawable/ic_chevron_right.xml new file mode 100644 index 00000000..b556d9bd --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_chevron_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_edit.xml b/sensorhub-android-app/res/drawable/ic_edit.xml index 6238d358..eb79c354 100644 --- a/sensorhub-android-app/res/drawable/ic_edit.xml +++ b/sensorhub-android-app/res/drawable/ic_edit.xml @@ -3,9 +3,8 @@ android:height="24dp" android:viewportWidth="960" android:viewportHeight="960" - android:tint="#FFFFFF" android:alpha="0.8"> + android:fillColor="@color/md_theme_onSurface"/> diff --git a/sensorhub-android-app/res/drawable/ic_flip_camera.xml b/sensorhub-android-app/res/drawable/ic_flip_camera.xml new file mode 100644 index 00000000..35706e9c --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_flip_camera.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_help.xml b/sensorhub-android-app/res/drawable/ic_help.xml new file mode 100644 index 00000000..e46face1 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_help.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_home.xml b/sensorhub-android-app/res/drawable/ic_home.xml index ebccd6ca..0372c0ef 100644 --- a/sensorhub-android-app/res/drawable/ic_home.xml +++ b/sensorhub-android-app/res/drawable/ic_home.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/ic_info.xml b/sensorhub-android-app/res/drawable/ic_info.xml index 0355cbed..893c3c9c 100644 --- a/sensorhub-android-app/res/drawable/ic_info.xml +++ b/sensorhub-android-app/res/drawable/ic_info.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_language.xml b/sensorhub-android-app/res/drawable/ic_language.xml new file mode 100644 index 00000000..2d435586 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_language.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_message.xml b/sensorhub-android-app/res/drawable/ic_message.xml index baad9323..8dbabe89 100644 --- a/sensorhub-android-app/res/drawable/ic_message.xml +++ b/sensorhub-android-app/res/drawable/ic_message.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_notifications.xml b/sensorhub-android-app/res/drawable/ic_notifications.xml new file mode 100644 index 00000000..d615ef2c --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_notifications.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_play.xml b/sensorhub-android-app/res/drawable/ic_play.xml index ce91a8b0..61f27bd4 100644 --- a/sensorhub-android-app/res/drawable/ic_play.xml +++ b/sensorhub-android-app/res/drawable/ic_play.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_sensors.xml b/sensorhub-android-app/res/drawable/ic_sensors.xml index 1c73501b..1c2dfc76 100644 --- a/sensorhub-android-app/res/drawable/ic_sensors.xml +++ b/sensorhub-android-app/res/drawable/ic_sensors.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_settings.xml b/sensorhub-android-app/res/drawable/ic_settings.xml index 7926ba39..1fa773fa 100644 --- a/sensorhub-android-app/res/drawable/ic_settings.xml +++ b/sensorhub-android-app/res/drawable/ic_settings.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_stop.xml b/sensorhub-android-app/res/drawable/ic_stop.xml index 41823516..7fe5c0df 100644 --- a/sensorhub-android-app/res/drawable/ic_stop.xml +++ b/sensorhub-android-app/res/drawable/ic_stop.xml @@ -3,8 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="#FFFFFF"> + > diff --git a/sensorhub-android-app/res/drawable/ic_tune.xml b/sensorhub-android-app/res/drawable/ic_tune.xml new file mode 100644 index 00000000..1c17316f --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_tune.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/layout/activity_edit_server_profile.xml b/sensorhub-android-app/res/layout/activity_edit_server_profile.xml index c2b6618c..b42e6990 100644 --- a/sensorhub-android-app/res/layout/activity_edit_server_profile.xml +++ b/sensorhub-android-app/res/layout/activity_edit_server_profile.xml @@ -37,7 +37,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" - app:cardBackgroundColor="@color/md_theme_surface" + app:cardBackgroundColor="@color/surface_card" app:cardCornerRadius="@dimen/card_corner_radius" app:cardElevation="0dp" app:strokeColor="@color/md_theme_outline" @@ -52,10 +52,8 @@ + android:text="@string/btn_csapi_client" /> + android:text="@string/btn_sost_client" /> @@ -98,7 +96,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" - app:cardBackgroundColor="@color/md_theme_surface" + app:cardBackgroundColor="@color/surface_card" app:cardCornerRadius="@dimen/card_corner_radius" app:cardElevation="0dp" app:strokeColor="@color/md_theme_outline" @@ -113,10 +111,8 @@ @@ -165,7 +161,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:hint="Port" + android:hint="@string/hint_port" style="@style/Widget.Material3.TextInputLayout.OutlinedBox" android:layout_marginBottom="8dp"> @@ -205,7 +201,7 @@ android:id="@+id/switch_disable_ssl" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="Disable SSL Validation" + android:text="@string/switch_disable_ssl" android:textColor="@color/md_theme_onSurface" android:paddingTop="4dp" android:paddingBottom="4dp" @@ -218,7 +214,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="12dp" - app:cardBackgroundColor="@color/md_theme_surface" + app:cardBackgroundColor="@color/surface_card" app:cardCornerRadius="@dimen/card_corner_radius" app:cardElevation="0dp" app:strokeColor="@color/md_theme_outline" @@ -233,10 +229,8 @@ + android:text="@string/auth_none" /> + android:text="@string/auth_basic" /> + android:text="@string/auth_oauth" /> @@ -291,7 +285,7 @@ + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/activity_main.xml b/sensorhub-android-app/res/layout/activity_main.xml index c511d319..f7ce38a5 100644 --- a/sensorhub-android-app/res/layout/activity_main.xml +++ b/sensorhub-android-app/res/layout/activity_main.xml @@ -27,27 +27,44 @@ app:titleCentered="false"> + android:gravity="center_vertical"> - + - + + + + + + diff --git a/sensorhub-android-app/res/layout/activity_server_profiles.xml b/sensorhub-android-app/res/layout/activity_server_profiles.xml index 401d15fb..17114b10 100644 --- a/sensorhub-android-app/res/layout/activity_server_profiles.xml +++ b/sensorhub-android-app/res/layout/activity_server_profiles.xml @@ -17,25 +17,8 @@ android:layout_height="?attr/actionBarSize" android:background="@color/toolbar_bg" app:titleTextColor="@color/toolbar_title" - app:title="Server Profiles" - app:navigationIcon="@drawable/ic_arrow_back"> - - - - + app:title="@string/manage_servers" + app:navigationIcon="@drawable/ic_arrow_back" /> @@ -49,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:text="No server profiles configured.\nTap Add to create one." + android:text="@string/empty_server_profiles" android:textColor="@color/md_theme_onSurfaceVariant" android:textSize="14sp" android:visibility="gone" /> @@ -63,4 +46,16 @@ + + diff --git a/sensorhub-android-app/res/layout/fragment_app_preferences.xml b/sensorhub-android-app/res/layout/fragment_app_preferences.xml new file mode 100644 index 00000000..89698646 --- /dev/null +++ b/sensorhub-android-app/res/layout/fragment_app_preferences.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml index c544846c..2982c170 100644 --- a/sensorhub-android-app/res/layout/fragment_dashboard.xml +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -58,7 +58,7 @@ @@ -73,12 +73,23 @@ + + @@ -124,7 +135,7 @@ @@ -133,7 +144,7 @@ android:id="@+id/meshtastic_info" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Connected" + android:text="@string/meshtastic_connected" android:textColor="@color/md_theme_onSurfaceVariant" android:textSize="12sp" android:layout_marginTop="2dp" /> @@ -145,7 +156,7 @@ style="@style/Widget.Material3.Button.TonalButton" android:layout_width="wrap_content" android:layout_height="36dp" - android:text="Message" + android:text="@string/btn_message" android:textSize="12sp" app:icon="@drawable/ic_message" app:iconSize="16dp" diff --git a/sensorhub-android-app/res/layout/fragment_sensors.xml b/sensorhub-android-app/res/layout/fragment_sensors.xml index 90421d75..a05aacb2 100644 --- a/sensorhub-android-app/res/layout/fragment_sensors.xml +++ b/sensorhub-android-app/res/layout/fragment_sensors.xml @@ -15,7 +15,7 @@ android:layout_height="match_parent"> android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" - android:text="Sensors" /> + android:text="@string/tab_sensors" /> diff --git a/sensorhub-android-app/res/layout/item_faq_entry.xml b/sensorhub-android-app/res/layout/item_faq_entry.xml new file mode 100644 index 00000000..6fe1b4a8 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_faq_entry.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_faq_header.xml b/sensorhub-android-app/res/layout/item_faq_header.xml new file mode 100644 index 00000000..3e721b24 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_faq_header.xml @@ -0,0 +1,15 @@ + + diff --git a/sensorhub-android-app/res/layout/item_server_profile.xml b/sensorhub-android-app/res/layout/item_server_profile.xml index 79f91884..45fa407b 100644 --- a/sensorhub-android-app/res/layout/item_server_profile.xml +++ b/sensorhub-android-app/res/layout/item_server_profile.xml @@ -9,8 +9,8 @@ android:layout_marginTop="6dp" android:layout_marginBottom="6dp" app:cardCornerRadius="@dimen/card_corner_radius" - app:cardElevation="@dimen/card_elevation" - app:cardBackgroundColor="@color/md_theme_background" + app:cardElevation="0dp" + app:cardBackgroundColor="@color/surface_card" app:strokeColor="@color/md_theme_outline" app:strokeWidth="1dp"> @@ -66,6 +66,7 @@ android:background="?attr/selectableItemBackgroundBorderless" android:src="@drawable/ic_edit" android:scaleType="centerInside" + app:tint="@color/text_secondary" android:contentDescription="Edit server profile" /> diff --git a/sensorhub-android-app/res/layout/pref_edit_bottom.xml b/sensorhub-android-app/res/layout/pref_edit_bottom.xml new file mode 100644 index 00000000..d8bb64b8 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_edit_bottom.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_icon_bottom.xml b/sensorhub-android-app/res/layout/pref_icon_bottom.xml new file mode 100644 index 00000000..74a07152 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_icon_bottom.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_icon_mid.xml b/sensorhub-android-app/res/layout/pref_icon_mid.xml new file mode 100644 index 00000000..39477d6b --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_icon_mid.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_icon_top.xml b/sensorhub-android-app/res/layout/pref_icon_top.xml new file mode 100644 index 00000000..fe97f1c5 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_icon_top.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_switch_bottom.xml b/sensorhub-android-app/res/layout/pref_switch_bottom.xml new file mode 100644 index 00000000..514d43bf --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_switch_bottom.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_switch_mid.xml b/sensorhub-android-app/res/layout/pref_switch_mid.xml new file mode 100644 index 00000000..4141a24f --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_switch_mid.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_switch_top.xml b/sensorhub-android-app/res/layout/pref_switch_top.xml new file mode 100644 index 00000000..9bf22283 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_switch_top.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_value_bottom.xml b/sensorhub-android-app/res/layout/pref_value_bottom.xml new file mode 100644 index 00000000..320800fe --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_value_bottom.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_value_mid.xml b/sensorhub-android-app/res/layout/pref_value_mid.xml new file mode 100644 index 00000000..07da4fc6 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_value_mid.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_value_single.xml b/sensorhub-android-app/res/layout/pref_value_single.xml new file mode 100644 index 00000000..e7ae672b --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_value_single.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/sensorhub-android-app/res/layout/pref_value_top.xml b/sensorhub-android-app/res/layout/pref_value_top.xml new file mode 100644 index 00000000..b36f6c71 --- /dev/null +++ b/sensorhub-android-app/res/layout/pref_value_top.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/sensorhub-android-app/res/menu/main.xml b/sensorhub-android-app/res/menu/main.xml index 2d3281de..11b31362 100644 --- a/sensorhub-android-app/res/menu/main.xml +++ b/sensorhub-android-app/res/menu/main.xml @@ -3,16 +3,4 @@ xmlns:tools="http://schemas.android.com/tools" tools:context="org.sensorhub.android.MainActivity"> - - - - diff --git a/sensorhub-android-app/res/values-de/strings.xml b/sensorhub-android-app/res/values-de/strings.xml new file mode 100644 index 00000000..1b205774 --- /dev/null +++ b/sensorhub-android-app/res/values-de/strings.xml @@ -0,0 +1,419 @@ + + + OpenSensorHub + Einstellungen konfigurieren und die Wiedergabetaste drücken, um SmartHub zu starten + Einstellungen + SmartHub starten + SmartHub stoppen + App-Status + Über + Proxy starten + Proxy stoppen + Meshtastic-Nachricht + Android-Sensor + Angel Gesundheitsmonitor + Meshtastic-Radio + Netzwerk-Scan + Polar Herzfrequenzmonitor + Kestrel Wetterstation + USB-Controller + TruPulse Entfernungsmesser + Flirone + STE Strahlungsmessgerät + Vorlage + Beschleunigungsmesser + Gyroskop + Magnetometer + Orientierung (Quaternionen) + Orientierung (Euler-Winkel) + GPS-Position + Netzwerk-Position + Videodaten + Video-Rollwinkel + Audio + + Gerätename + Geräte-IP-Adresse + Serverprofile + Server verwalten + Dienste + SOS-Dienst + Connected Systems-Dienst + Erkennungsdienst + + Beschleunigungsmesser-Bewegungsdaten in Echtzeit streamen + Gyroskop-Rotationsdaten in Echtzeit streamen + Magnetometer-Richtungs- und Magnetfelddaten streamen + Geräteorientierungs- und Lagedaten streamen + GPS-basierte Standort- und Bewegungsdaten streamen + Netzwerkbasierte Standortdaten streamen + Live-Video von der Gerätekamera streamen + Geräte-Rollmetadaten an Video-Frame-Header anhängen + Live-Audio vom Gerätemikrofon streamen + Beim Start über Bluetooth mit einem Meshtastic-Gerät verbinden + Scan-Daten nahegelegener drahtloser Netzwerke streamen + Über Bluetooth LE mit einem Polar-Herzfrequenzsensor verbinden + Umweltdaten von einer Kestrel-Wetterstation streamen + Beim Start mit einem unterstützten USB-Controller verbinden + Entfernungs- und Winkeldaten von einem TruPulse-Entfernungsmesser streamen + Simulierte TruPulse-Messungen für Tests verwenden + Biometrische Daten vom Angel-Sensor streamen + Wärmebilder von einer angeschlossenen FLIR One-Kamera streamen + Beim Start über Bluetooth LE mit einem STE RadPager verbinden + Vorlagen-Sensortreiber für Entwicklung und Tests aktivieren + Optionen für den Datenversand + OGC-Standard-REST-API, die Sensordaten dieses Geräts bereitstellt. + Legacy-API, die Sensordaten als XML über HTTP bereitstellt + Dienst, der Erkennung basierend auf definierbaren Regelsätzen bereitstellt + Tippen, um Geräteadresse auszuwählen oder einzugeben + Sensoren UID-Erweiterung + Optional — Server hinzufügen, um Daten remote zu senden. Ohne Server werden Daten lokal bereitgestellt. + + + JPEG + H264 + H265 + VP9 + VP8 + + + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + + + Quaternion + Euler + + + QUATERNION + EULER + + + QUATERNION + + + + GPS + Netzwerk + + + GPS + NETWORK + + + GPS + + + + Abrufbar + Lokal speichern + Remote senden + + + FETCH_LOCAL + STORE_LOCAL + PUSH_REMOTE + + + PUSH_REMOTE + + + TruPulse-Gerätename + + Physisches Gerät streamen + Virtuelles Gerät simulieren + + + STREAM + SIMULATED + + STREAM + + + Meldeobjekt auswählen + Straßensperrung + Überschwemmung + Medizinisch + Hilfe + + + + Nächster Beacon + Trilateration + + + NEAREST + TRILATERATION + + + NEAREST + + + + GPS + Laser-Entfernungsmesser + Netzwerk + + + Lagebericht + NAME: + BESCHREIBUNG: + AUFNEHMEN + ZURÜCKSETZEN + BERICHT SENDEN + Ein Name für den Bericht + + Radius: + Breite: + Länge: + Fuß + + + Auswählen... + Öffentlich + Alle + + + + Auswählen... + Öffnen + Schließen + + + Aktion: + Ref.-Nr.: + + Typ: + + + Auswählen... + Kanaldrainage + Landoberfläche + + + + Auswählen... + Messgerät + Visuell + Modell + + + Merkmalstyp: + Tiefe: + Beob.-Modus: + + Medizinischen Zustand beschreiben... + Messwert eingeben (BD, Temp., usw.)... + Notfall (J/N) + + + Auswählen... + Umwelt + Gesundheit + Sicherheit + Dienste + + + + Auswählen... + 5 + 4 + 3 + 2 + 1 + + + Hilfsart: + Anz. Personen: + Dringlichkeit: + Benötigte Hilfe beschreiben... + Geben Sie Ihren Namen oder Ihre ID ein + + + Auswählen... + Person + Fahrzeug + Gerät + + + + Auswählen... + GPS + Bluetooth-Beacon + WiFi + Mobilfunk + UWB + N/V + + + Verfolgte Ressource: + Verfolgungsmethode: + Ressourcen-ID eingeben + Ressourcen-Bezeichnung eingeben + + + AAC + AMR-NB + AMR-WB + FLAC + VORBIS + OPUS + PCM + + + + 0.172 + 0.204 + 0.222 + 0.224 + 0.257 + + + + Zoll + mil + tmoa + smoa + Klicks + cm + + + + L + R + + + + + + + + + + + Sensoren + Startseite + Einstellungen + App-Einstellungen + + Nachricht eingeben! + Nachricht eingeben! + Zielknoten-ID eingeben (Ganzzahl) + Zielknoten-ID eingeben (Ganzzahl) + + + Benachrichtigungen + Sprache + Version + Über die App + Hilfe / FAQ + Sprache + Eine Softwareplattform zum Aufbau intelligenter Sensornetzwerke und des Internets der Dinge + + + INTEGRIERT + Bluetooth-Peripheriegeräte + Sonstige + Codec + Bildrate + Auflösung + Kamera + Abtastrate + Bitrate (kbps) + Meshtastic-Gerät auswählen + Polar HR-Gerät auswählen + Kestrel-Gerät auswählen + TruPulse-Datenquelle + TruPulse-Gerät auswählen + Simulierte TruPulse-Daten verwenden + Vorlagengerät auswählen + Eine ID, die am Ende der UID jedes Systems angehängt wird + URL zur Discovery Rules.txt + + + Video-Stream + Kamera wechseln + Anzeigen + Ausblenden + Meshtastic + Verbunden + Nachricht + Sensoren + Keine Sensoren für Remote-Versand konfiguriert + Ausführungsname + Bitte geben Sie den Namen für diese Ausführung ein + Meshtastic-Nachricht senden + + + Keine Serverprofile konfiguriert.\nTippen Sie auf +, um eines zu erstellen. + Serverprofil hinzufügen + Server löschen + \"%s\" entfernen? + + + CLIENT + CS API-Client + SOS-T-Client + VERBINDUNG + Servername + Host / IP + Port + Endpunkt-Pfad + TLS aktivieren + SSL-Validierung deaktivieren + AUTHENTIFIZIERUNG + Keine + Einfach + OAuth + Benutzername + Passwort + Token-Endpunkt + Client-ID + Client-Geheimnis + Server hinzufügen + + + OK + Abbrechen + Löschen + Senden + Gerät auswählen + Gerätename oder -adresse eingeben + Geben Sie einen Gerätenamen (z.B. \"Ballistic\") oder eine MAC-Adresse ein. Namen werden vom Anfang an verglichen, ohne Berücksichtigung der Groß-/Kleinschreibung. + Server bearbeiten + Garmin Wearable + Garmin-Gerät auswählen + z.B. Ballistic oder AA:BB:CC:DD:EE:FF + Name oder Adresse manuell eingeben… + Nach neuen Geräten suchen… + Kamera wechseln + Gesundheits-Biometriedaten vom Garmin Wearable streamen + + + SensorHub wird gestoppt + SensorHub gestoppt + SensorHub wird gestartet… + SensorHub konnte nicht gestartet werden + Video-Konfigurationsfehler: Einstellungen überprüfen + Überprüfen Sie die Video-Einstellungen und stellen Sie sicher, dass die Auflösung für das ausgewählte Preset festgelegt wurde. + SensorHub läuft nicht + Nachricht gesendet + Nachricht konnte nicht gesendet werden + Profil nicht gefunden + Name, Host und Port sind erforderlich + Host sollte kein Protokoll enthalten (z.B. http://) + Port muss eine Zahl sein + Port muss zwischen 1 und 65535 liegen + + diff --git a/sensorhub-android-app/res/values-de/strings_app_status.xml b/sensorhub-android-app/res/values-de/strings_app_status.xml new file mode 100644 index 00000000..4437dea1 --- /dev/null +++ b/sensorhub-android-app/res/values-de/strings_app_status.xml @@ -0,0 +1,21 @@ + + + App-Status + + + Initialisierung + Initialisiert + Starten + Gestartet + Stoppen + Gestoppt + Unbekannt + + + SOS-Dienststatus + ConSys-Dienststatus + Erkennungsdienststatus + HTTP-Serverstatus + Android-Sensorstatus + Android-Sensorspeicherstatus + diff --git a/sensorhub-android-app/res/values-de/strings_help_faq.xml b/sensorhub-android-app/res/values-de/strings_help_faq.xml new file mode 100644 index 00000000..94c998fa --- /dev/null +++ b/sensorhub-android-app/res/values-de/strings_help_faq.xml @@ -0,0 +1,75 @@ + + + + + Erste Schritte + Erste Schritte + Erste Schritte + + Serverkonfiguration + Serverkonfiguration + Serverkonfiguration + Serverkonfiguration + + Sensoren + Sensoren + + Dienste + + Fehlerbehebung + Fehlerbehebung + + Allgemein + Allgemein + Allgemein + + + + + Was ist OpenSensorHub EdgeX? + Wie verbinde ich mich mit einem Server? + Wie starte ich das Streaming von Sensordaten? + + Was ist der Unterschied zwischen ConSysAPI und SOS-T? + Wie richte ich die OAuth-Authentifizierung ein? + Was bewirkt \"SSL-Überprüfung überspringen\"? + Kann ich mich mit mehreren Servern verbinden? + + Wie verbinde ich einen Bluetooth-Sensor? + Was ist die UID-Erweiterung? + + Was ist der Erkennungsdienst? + + Was bedeuten die Statusfarben? + Meine Sensoren streamen nicht. Was soll ich überprüfen? + + Wie ändere ich das App-Design? + Wo finde ich die IP-Adresse meines Geräts? + Welche Android-Version wird benötigt? + + + + + OpenSensorHub EdgeX ist eine App zur Aggregation und zum Streaming von Sensordaten. Sie sammelt Daten von den integrierten Sensoren Ihres Geräts und verbundenen Bluetooth-Peripheriegeräten und streamt diese Daten dann in Echtzeit an einen OpenSensorHub-Server. Sie verwandelt Ihr Android-Gerät in einen Edge-Computing-Knoten für IoT-Sensornetzwerke. + Gehen Sie zum Tab Einstellungen, tippen Sie auf \"Server verwalten\" und dann auf die Schaltfläche Hinzufügen. Geben Sie einen Namen für das Profil, die Serveradresse, den Port und den Endpoint-Pfad ein. Sie können auch die Authentifizierung konfigurieren (Benutzername/Passwort oder OAuth) und TLS/SSL aktivieren, wenn Ihr Server dies erfordert. + Aktivieren Sie zunächst die gewünschten Sensoren im Tab Sensoren. Stellen Sie dann sicher, dass mindestens ein Serverprofil im Tab Einstellungen konfiguriert und aktiviert ist. Tippen Sie abschließend auf die Wiedergabetaste im Dashboard, um das Streaming zu starten. Sie können den Status jedes Dienstes auf dem App-Status-Bildschirm überwachen. + + Connected Systems API (ConSysAPI) ist die moderne OGC-Standard-REST-API zur Veröffentlichung von Sensordaten. Sie ist die empfohlene Option für neue Installationen. SOS-T (Sensor Observation Service – Transaktional) ist ein veraltetes XML-basiertes Protokoll. Verwenden Sie SOS-T nur, wenn Ihr Server ConSysAPI nicht unterstützt. + Aktivieren Sie beim Bearbeiten eines Serverprofils die OAuth-Option und füllen Sie die Token-Endpoint-URL, Client-ID und das Client-Secret aus, die Ihnen Ihr Serveradministrator bereitgestellt hat. OAuth wird verwendet, wenn Ihr Server eine tokenbasierte Authentifizierung anstelle von einfachen Benutzername/Passwort-Anmeldedaten erfordert. + \"SSL-Überprüfung überspringen\" deaktiviert die Zertifikatsvalidierung bei der Verbindung über TLS/SSL. Dies ist nützlich für Entwicklungs- oder Testumgebungen, die selbstsignierte Zertifikate verwenden. Aktivieren Sie dies nicht in der Produktion, da es die Verbindung für Man-in-the-Middle-Angriffe anfällig macht. + Ja. Sie können unter Einstellungen > Server verwalten mehrere Serverprofile erstellen. Jedes Profil kann einzeln aktiviert oder deaktiviert werden. Beim Streaming werden die Daten gleichzeitig an alle aktivierten Serverprofile gesendet. + + Koppeln Sie zunächst das Bluetooth-Gerät über die Bluetooth-Einstellungen des Systems mit Ihrem Android-Gerät. Aktivieren Sie dann im Tab Sensoren den entsprechenden Sensorschalter und wählen Sie Ihr Gerät aus der Geräteauswahl, wenn Sie dazu aufgefordert werden. Stellen Sie sicher, dass Bluetooth eingeschaltet und das Peripheriegerät eingeschaltet und in Reichweite ist. + Die UID-Erweiterung ist ein optionaler Bezeichner, der der eindeutigen ID jedes Sensordatenstroms hinzugefügt wird. Sie hilft, die Daten dieses spezifischen Geräts zu unterscheiden, wenn mehrere Geräte an denselben Server streamen. Sie können sie im Tab Sensoren unter \"Sensoren UID-Erweiterung\" einstellen. + + Der Erkennungsdienst ermöglicht es der App, andere Sensoren und Geräte im Netzwerk mithilfe konfigurierbarer Regelsätze automatisch zu entdecken. Sie können im Tab Einstellungen eine URL zu einer Erkennungsregeldatei angeben. Dies ist nützlich bei größeren Installationen, bei denen sich Geräte automatisch finden müssen. + + Auf dem App-Status-Bildschirm:\n\n\u2022 Grün – Der Dienst ist gestartet und läuft normal.\n\u2022 Orange – Der Dienst wird initialisiert oder gestartet.\n\u2022 Rot – Der Dienst ist gestoppt oder hat einen Fehler festgestellt.\n\u2022 Grau – Der Status ist unbekannt oder der Dienst wurde nicht gestartet. + Überprüfen Sie Folgendes:\n\n1. Stellen Sie sicher, dass die gewünschten Sensoren im Tab Sensoren aktiviert sind.\n2. Überprüfen Sie, ob mindestens ein Serverprofil in den Einstellungen konfiguriert und aktiviert ist.\n3. Bestätigen Sie, dass die erforderlichen Dienste (ConSysAPI oder SOS) in den Einstellungen aktiviert sind.\n4. Prüfen Sie den App-Status-Bildschirm auf Dienste, die rot oder grau angezeigt werden.\n5. Stellen Sie sicher, dass alle erforderlichen Berechtigungen (Standort, Kamera, Bluetooth usw.) in den Android-Einstellungen erteilt wurden.\n6. Überprüfen Sie, ob Ihr Gerät eine Netzwerkverbindung hat und den Server erreichen kann. + + Gehen Sie zu den App-Einstellungen (tippen Sie auf das Zahnradsymbol in der Dashboard-Symbolleiste) und tippen Sie dann auf \"Erscheinungsbild\". Sie können zwischen Systemstandard (folgt dem Gerätedesign), Hell oder Dunkel wählen. + Die IP-Adresse Ihres Geräts wird in den App-Einstellungen unter dem Feld \"Geräte-IP-Adresse\" angezeigt. Dies ist die Adresse, die andere Geräte oder Dienste verwenden können, um Ihr Gerät im lokalen Netzwerk zu erreichen. + OpenSensorHub EdgeX erfordert Android 14 (API-Level 34) oder höher. + + + diff --git a/sensorhub-android-app/res/values-es/strings.xml b/sensorhub-android-app/res/values-es/strings.xml new file mode 100644 index 00000000..22acc997 --- /dev/null +++ b/sensorhub-android-app/res/values-es/strings.xml @@ -0,0 +1,187 @@ + + + OpenSensorHub + Configure los ajustes y toque el botón de reproducción para iniciar SmartHub + Ajustes + Iniciar SmartHub + Detener SmartHub + Estado de la app + Acerca de + Iniciar proxy + Detener proxy + Mensaje Meshtastic + Sensor Android + Monitor de salud Angel + Radio Meshtastic + Escaneo de redes + Monitor cardíaco Polar + Estación Kestrel + Controlador USB + Telémetro TruPulse + Flirone + Detector de radiación STE + Plantilla + Acelerómetro + Giroscopio + Magnetómetro + Orientación (cuaterniones) + Orientación (ángulos de Euler) + Ubicación GPS + Ubicación por red + Datos de vídeo + Rotación de vídeo + Audio + + Nombre del dispositivo + Dirección IP del dispositivo + Perfiles de servidor + Administrar servidores + Servicios + Servicio SOS + Servicio Connected Systems + Servicio de descubrimiento + + Transmitir datos de movimiento del acelerómetro en tiempo real + Transmitir datos de rotación del giroscopio en tiempo real + Transmitir datos de dirección y campo magnético del magnetómetro + Transmitir datos de orientación y actitud del dispositivo + Transmitir datos de ubicación y movimiento por GPS + Transmitir datos de ubicación basados en red + Transmitir vídeo en vivo desde la cámara del dispositivo + Adjuntar metadatos de rotación del dispositivo a los encabezados de fotogramas + Transmitir audio en vivo desde el micrófono del dispositivo + Conectar a un dispositivo Meshtastic por Bluetooth al iniciar + Transmitir datos de escaneo de redes inalámbricas cercanas + Conectar a un sensor cardíaco Polar por Bluetooth LE + Transmitir datos ambientales desde una estación Kestrel + Conectar a un controlador USB compatible al iniciar + Transmitir datos de distancia y ángulo desde un telémetro TruPulse + Usar mediciones simuladas de TruPulse para pruebas + Transmitir datos biométricos desde el Angel Sensor + Transmitir imágenes térmicas desde una cámara FLIR One conectada + Conectar a un STE RadPager por Bluetooth LE al iniciar + Habilitar el controlador de sensor de plantilla para desarrollo y pruebas + Opciones para enviar datos + API REST estándar OGC que sirve los datos de sensores de este dispositivo. + API heredada que sirve datos de sensores en XML por HTTP + Servicio que proporciona descubrimiento basado en conjuntos de reglas definibles + Toque para seleccionar o ingresar la dirección del dispositivo + Extensión UID de sensores + Opcional — agregue servidores para enviar datos de forma remota. Sin servidores, los datos se sirven localmente. + + Sensores + Inicio + Ajustes + Preferencias de la app + + ¡Escriba un mensaje! + ¡Escriba un mensaje! + Ingrese el ID del nodo de destino (entero) + Ingrese el ID del nodo de destino (entero) + + + Notificaciones + Idioma + Versión + Acerca de la aplicación + Ayuda / Preguntas frecuentes + Idioma + Una plataforma de software para construir redes de sensores inteligentes y el Internet de las Cosas + + + INTEGRADOS + Periféricos Bluetooth + Otros + Códec + Tasa de fotogramas + Resolución + Cámara + Tasa de muestreo + Tasa de bits (kbps) + Seleccionar dispositivo Meshtastic + Seleccionar dispositivo Polar HR + Seleccionar dispositivo Kestrel + Fuente de datos TruPulse + Seleccionar dispositivo TruPulse + Usar datos simulados de TruPulse + Seleccionar dispositivo de plantilla + Un ID adjunto al final del UID de cada sistema + URL del archivo de reglas de descubrimiento + + + Transmisión de vídeo + Cambiar cámara + Mostrar + Ocultar + Meshtastic + Conectado + Mensaje + Sensores + No hay sensores configurados para envío remoto + Nombre de ejecución + Ingrese el nombre para esta ejecución + Enviar mensaje Meshtastic + + + No hay perfiles de servidor configurados.\nToque + para crear uno. + Agregar perfil de servidor + Eliminar servidor + ¿Eliminar \"%s\"? + + + CLIENTE + Cliente CS API + Cliente SOS-T + CONEXIÓN + Nombre del servidor + Host / IP + Puerto + Ruta del endpoint + Habilitar TLS + Deshabilitar validación SSL + AUTENTICACIÓN + Ninguno + Básico + OAuth + Usuario + Contraseña + Endpoint de token + ID de cliente + Secreto de cliente + Agregar servidor + + + Aceptar + Cancelar + Eliminar + Enviar + Seleccionar dispositivo + Ingrese nombre o dirección del dispositivo + Ingrese un nombre de dispositivo (ej. \"Ballistic\") o dirección MAC. Los nombres se comparan desde el inicio, sin distinción de mayúsculas. + Editar servidor + Wearable Garmin + Seleccionar dispositivo Garmin + ej. Ballistic o AA:BB:CC:DD:EE:FF + Ingresar nombre o dirección manualmente… + Buscar nuevos dispositivos… + Cambiar cámara + Transmitir datos biométricos de salud desde un wearable Garmin + + + Deteniendo SensorHub + SensorHub detenido + Iniciando SensorHub… + SensorHub no pudo iniciar + Error de configuración de vídeo: Revise los ajustes + Revise los ajustes de vídeo y asegúrese de que la resolución para el preset seleccionado esté configurada. + SensorHub no está en ejecución + Mensaje enviado + Error al enviar el mensaje + Perfil no encontrado + Nombre, host y puerto son obligatorios + El host no debe incluir un protocolo (ej. http://) + El puerto debe ser un número + El puerto debe estar entre 1 y 65535 + + + diff --git a/sensorhub-android-app/res/values-es/strings_app_status.xml b/sensorhub-android-app/res/values-es/strings_app_status.xml new file mode 100644 index 00000000..e6d9a1fc --- /dev/null +++ b/sensorhub-android-app/res/values-es/strings_app_status.xml @@ -0,0 +1,21 @@ + + + Estado de la app + + + Inicializando + Inicializado + Iniciando + Iniciado + Deteniendo + Detenido + Desconocido + + + Estado del servicio SOS + Estado del servicio ConSys + Estado del servicio de descubrimiento + Estado del servidor HTTP + Estado del sensor Android + Estado del almacenamiento de sensores + diff --git a/sensorhub-android-app/res/values-es/strings_help_faq.xml b/sensorhub-android-app/res/values-es/strings_help_faq.xml new file mode 100644 index 00000000..e20b76b1 --- /dev/null +++ b/sensorhub-android-app/res/values-es/strings_help_faq.xml @@ -0,0 +1,63 @@ + + + + + Primeros pasos + Primeros pasos + Primeros pasos + Configuración del servidor + Configuración del servidor + Configuración del servidor + Configuración del servidor + Sensores + Sensores + Servicios + Solución de problemas + Solución de problemas + General + General + General + + + + ¿Qué es OpenSensorHub EdgeX? + ¿Cómo me conecto a un servidor? + ¿Cómo empiezo a transmitir datos de sensores? + ¿Cuál es la diferencia entre ConSysAPI y SOS-T? + ¿Cómo configuro la autenticación OAuth? + ¿Qué hace \"Omitir verificación SSL\"? + ¿Puedo conectarme a múltiples servidores? + ¿Cómo conecto un sensor Bluetooth? + ¿Qué es la extensión UID? + ¿Qué es el servicio de descubrimiento? + ¿Qué significan los colores de estado? + Mis sensores no están transmitiendo. ¿Qué debo verificar? + ¿Cómo cambio el tema de la app? + ¿Dónde puedo encontrar la dirección IP de mi dispositivo? + ¿Qué versión de Android se requiere? + + + + OpenSensorHub EdgeX es una aplicación de agregación y transmisión de sensores. Recopila datos de los sensores integrados de su dispositivo y periféricos Bluetooth conectados, y luego transmite esos datos a un servidor OpenSensorHub en tiempo real. Convierte su dispositivo Android en un nodo de computación en el borde para redes de sensores IoT. + Vaya a la pestaña Ajustes, toque \"Administrar servidores\" y luego toque el botón de agregar. Ingrese un nombre para el perfil, la dirección del servidor, puerto y ruta del endpoint. También puede configurar la autenticación (usuario/contraseña u OAuth) y habilitar TLS/SSL si su servidor lo requiere. + Primero, habilite los sensores deseados en la pestaña Sensores. Luego, asegúrese de tener al menos un perfil de servidor configurado y habilitado en la pestaña Ajustes. Finalmente, toque el botón de reproducción en el Panel para iniciar la transmisión. Puede monitorear el estado de cada servicio desde la pantalla de Estado de la app. + + Connected Systems API (ConSysAPI) es la API REST estándar OGC moderna para publicar datos de sensores. Es la opción recomendada para nuevas implementaciones. SOS-T (Servicio de Observación de Sensores – Transaccional) es un protocolo XML heredado. Use SOS-T solo si su servidor no soporta ConSysAPI. + Al editar un perfil de servidor, habilite el interruptor OAuth y complete la URL del endpoint de token, ID de cliente y secreto de cliente proporcionados por el administrador de su servidor. OAuth se usa cuando su servidor requiere autenticación basada en tokens en lugar de credenciales básicas de usuario/contraseña. + \"Omitir verificación SSL\" deshabilita la validación de certificados al conectar por TLS/SSL. Es útil para entornos de desarrollo o pruebas que usan certificados autofirmados. No lo habilite en producción, ya que hace la conexión vulnerable a ataques de intermediario. + Sí. Puede crear múltiples perfiles de servidor en Ajustes > Administrar servidores. Cada perfil puede habilitarse o deshabilitarse individualmente. Al transmitir, los datos se envían a todos los perfiles de servidor habilitados simultáneamente. + + Primero, empareje el dispositivo Bluetooth con su dispositivo Android a través de los ajustes del sistema Bluetooth. Luego, en la pestaña Sensores, habilite el interruptor del sensor correspondiente y seleccione su dispositivo del selector si se le solicita. Asegúrese de que el Bluetooth esté encendido y el periférico esté encendido y dentro del alcance. + La extensión UID es un identificador opcional que se agrega al ID único de cada flujo de sensor. Ayuda a distinguir los datos de este dispositivo específico cuando múltiples dispositivos transmiten al mismo servidor. Puede configurarlo en la pestaña Sensores bajo \"Extensión UID de sensores\". + + El servicio de descubrimiento permite que la app descubra automáticamente otros sensores y dispositivos en la red usando conjuntos de reglas configurables. Puede proporcionar una URL a un archivo de reglas de descubrimiento en la pestaña Ajustes. Es útil en implementaciones más grandes donde los dispositivos necesitan encontrarse automáticamente. + + En la pantalla de Estado de la app:\n\n\u2022 Verde – El servicio está iniciado y funcionando normalmente.\n\u2022 Naranja – El servicio se está inicializando o arrancando.\n\u2022 Rojo – El servicio está detenido o encontró un error.\n\u2022 Gris – El estado es desconocido o el servicio no se ha iniciado. + Verifique lo siguiente:\n\n1. Asegúrese de que los sensores deseados estén habilitados en la pestaña Sensores.\n2. Verifique que al menos un perfil de servidor esté configurado y habilitado en Ajustes.\n3. Confirme que los servicios requeridos (ConSysAPI o SOS) estén habilitados en Ajustes.\n4. Revise la pantalla de Estado de la app para ver si algún servicio muestra rojo o gris.\n5. Asegúrese de que todos los permisos requeridos (Ubicación, Cámara, Bluetooth, etc.) hayan sido otorgados en los Ajustes de Android.\n6. Verifique que su dispositivo tenga conectividad de red y pueda alcanzar el servidor. + + Vaya a Preferencias de la app (toque el ícono de engranaje en la barra de herramientas del Panel), luego toque \"Apariencia\". Puede elegir entre Predeterminado del sistema (sigue el tema de su dispositivo), Claro u Oscuro. + La dirección IP de su dispositivo se muestra en Preferencias de la app, bajo el campo \"Dirección IP del dispositivo\". Esta es la dirección que otros dispositivos o servicios pueden usar para contactar su dispositivo en la red local. + OpenSensorHub EdgeX requiere Android 14 (nivel de API 34) o superior. + + + diff --git a/sensorhub-android-app/res/values-fr/strings.xml b/sensorhub-android-app/res/values-fr/strings.xml new file mode 100644 index 00000000..85716bf5 --- /dev/null +++ b/sensorhub-android-app/res/values-fr/strings.xml @@ -0,0 +1,419 @@ + + + OpenSensorHub + Configurez les paramètres et appuyez sur le bouton lecture pour démarrer SmartHub + Paramètres + Démarrer SmartHub + Arrêter SmartHub + État de l\'application + À propos + Démarrer le proxy + Arrêter le proxy + Message Meshtastic + Capteur Android + Moniteur de santé Angel + Radio Meshtastic + Wardriving + Moniteur cardiaque Polar + Station météo Kestrel + Contrôleur USB + Télémètre TruPulse + Flirone + Dosimètre STE RadPager + Modèle + Accéléromètre + Gyroscope + Magnétomètre + Orientation (Quaternions) + Orientation (Angles d\'Euler) + Localisation GPS + Localisation réseau + Données vidéo + Roulis vidéo + Audio + + Nom de l\'appareil + Adresse IP de l\'appareil + Profils serveur + Gérer les serveurs + Services + Service SOS + Service Systèmes connectés + Service de découverte + + Diffuser en temps réel les données de mouvement de l\'accéléromètre + Diffuser en temps réel les données de rotation du gyroscope + Diffuser les données de cap et de champ magnétique du magnétomètre + Diffuser les données d\'orientation et d\'attitude de l\'appareil + Diffuser les données de localisation et de déplacement par GPS + Diffuser les données de localisation par réseau + Diffuser la vidéo en direct depuis la caméra de l\'appareil + Joindre les métadonnées de roulis de l\'appareil aux en-têtes d\'images vidéo + Diffuser l\'audio en direct depuis le microphone de l\'appareil + Se connecter à un appareil Meshtastic via Bluetooth au démarrage + Diffuser les données de scan des réseaux sans fil à proximité + Se connecter à un capteur de fréquence cardiaque Polar via Bluetooth LE + Diffuser les données environnementales d\'une station météo Kestrel + Se connecter à un contrôleur USB compatible au démarrage + Diffuser les données de distance et d\'angle depuis un télémètre TruPulse + Utiliser des mesures TruPulse simulées pour les tests + Diffuser les données biométriques du capteur Angel + Diffuser les images thermiques depuis une caméra FLIR One connectée + Se connecter à un STE RadPager via Bluetooth LE au démarrage + Activer le pilote de capteur modèle pour le développement et les tests + Options pour l\'envoi de données + API REST standard OGC servant les données de capteur de cet appareil. + API héritée servant les données de capteur en XML via HTTP + Service fournissant la découverte basée sur des règles définissables + Appuyer pour sélectionner ou saisir l\'adresse de l\'appareil + Extension UID des capteurs + Optionnel — ajoutez des serveurs pour envoyer des données à distance. Sans serveurs, les données sont servies localement. + + + JPEG + H264 + H265 + VP9 + VP8 + + + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + + + Quaternion + Euler + + + QUATERNION + EULER + + + QUATERNION + + + + GPS + Réseau + + + GPS + NETWORK + + + GPS + + + + Récupérable + Stocker localement + Envoyer à distance + + + FETCH_LOCAL + STORE_LOCAL + PUSH_REMOTE + + + PUSH_REMOTE + + + Nom de l\'appareil TruPulse + + Appareil physique en flux + Simuler un appareil virtuel + + + STREAM + SIMULATED + + STREAM + + + Sélectionner un élément à signaler + Fermeture de rue + Inondation + Médical + Aide + + + + Balise la plus proche + Trilatération + + + NEAREST + TRILATERATION + + + NEAREST + + + + GPS + Télémètre laser + Réseau + + + Rapport de situation + NOM : + DESCRIPTION : + CAPTURER + RÉINITIALISER + SOUMETTRE LE RAPPORT + Un nom pour le rapport + + Rayon : + Lat : + Lon : + Ft. + + + Sélectionner... + Public + Tous + + + + Sélectionner... + Ouvrir + Fermer + + + Action : + Réf. ID : + + Type : + + + Sélectionner... + Drainage de canal + Surface terrestre + + + + Sélectionner... + Mètre + Visuel + Modèle + + + Type de caractéristique : + Profondeur : + Mode d\'obs. : + + Décrire la condition médicale... + Entrer la mesure (TA, Temp., etc.)... + Urgence (V/F) + + + Sélectionner... + Environnement + Santé + Sécurité + Services + + + + Sélectionner... + 5 + 4 + 3 + 2 + 1 + + + Type d\'aide : + Nb de personnes : + Urgence : + Décrire l\'aide nécessaire... + Entrez votre nom ou identifiant + + + Sélectionner... + Personne + Véhicule + Appareil + + + + Sélectionner... + GPS + Balise Bluetooth + WiFi + Cellulaire + UWB + S/O + + + Ressource suivie : + Méthode de suivi : + Entrer l\'identifiant de la ressource + Entrer le libellé de la ressource + + + AAC + AMR-NB + AMR-WB + FLAC + VORBIS + OPUS + PCM + + + + 0.172 + 0.204 + 0.222 + 0.224 + 0.257 + + + + pouces + mil + tmoa + smoa + clics + cm + + + + G + D + + + + + + + + + + + Capteurs + Accueil + Paramètres + Préférences de l\'application + + Entrez un message ! + Entrez un message ! + Entrez l\'ID du nœud de destination (entier) + Entrez l\'ID du nœud de destination (entier) + + + Notifications + Langue + Version + À propos de l\'application + Aide / FAQ + Langue + Une plateforme logicielle pour construire des réseaux de capteurs intelligents et l\'Internet des objets + + + SUR L\'APPAREIL + Périphériques Bluetooth + Autres + Codec + Fréquence d\'images + Résolution + Caméra + Fréquence d\'échantillonnage + Débit binaire (kbps) + Sélectionner l\'appareil Meshtastic + Sélectionner l\'appareil Polar HR + Sélectionner l\'appareil Kestrel + Source de données TruPulse + Sélectionner l\'appareil TruPulse + Utiliser les données TruPulse simulées + Sélectionner l\'appareil modèle + Un identifiant ajouté à la fin de l\'UID de chaque système + URL vers le fichier Discovery Rules.txt + + + Flux vidéo + Changer de caméra + Afficher + Masquer + Meshtastic + Connecté + Message + Capteurs + Aucun capteur configuré pour l\'envoi à distance + Nom de la session + Veuillez entrer le nom de cette session + Envoyer un message Meshtastic + + + Aucun profil serveur configuré.\nAppuyez sur + pour en créer un. + Ajouter un profil serveur + Supprimer le serveur + Supprimer \"%s\" ? + + + CLIENT + Client CS API + Client SOS-T + CONNEXION + Nom du serveur + Hôte / IP + Port + Chemin du point de terminaison + Activer TLS + Désactiver la validation SSL + AUTHENTIFICATION + Aucune + Basique + OAuth + Nom d\'utilisateur + Mot de passe + Point de terminaison du jeton + ID client + Secret client + Ajouter un serveur + + + OK + Annuler + Supprimer + Envoyer + Sélectionner l\'appareil + Entrer le nom ou l\'adresse de l\'appareil + Entrez un nom d\'appareil (ex. \"Ballistic\") ou une adresse MAC. Les noms sont comparés depuis le début, sans distinction de casse. + Modifier le serveur + Wearable Garmin + Sélectionner l\'appareil Garmin + ex. Ballistic ou AA:BB:CC:DD:EE:FF + Saisir le nom ou l\'adresse manuellement… + Rechercher de nouveaux appareils… + Changer de caméra + Diffuser les données biométriques de santé depuis un wearable Garmin + + + Arrêt de SensorHub + SensorHub arrêté + Démarrage de SensorHub… + Échec du démarrage de SensorHub + Erreur de configuration vidéo : Vérifiez les paramètres + Vérifiez les paramètres vidéo et assurez-vous que la résolution du préréglage sélectionné est définie. + SensorHub n\'est pas en cours d\'exécution + Message envoyé + Échec de l\'envoi du message + Profil introuvable + Le nom, l\'hôte et le port sont obligatoires + L\'hôte ne doit pas inclure de protocole (ex. http://) + Le port doit être un nombre + Le port doit être compris entre 1 et 65535 + + \ No newline at end of file diff --git a/sensorhub-android-app/res/values-fr/strings_app_status.xml b/sensorhub-android-app/res/values-fr/strings_app_status.xml new file mode 100644 index 00000000..c0075b48 --- /dev/null +++ b/sensorhub-android-app/res/values-fr/strings_app_status.xml @@ -0,0 +1,21 @@ + + + État de l\'application + + + Initialisation + Initialisé + Démarrage + Démarré + Arrêt + Arrêté + Inconnu + + + État du service SOS + État du service ConSys + État du service de découverte + État du serveur HTTP + État des capteurs Android + État du stockage des capteurs Android + diff --git a/sensorhub-android-app/res/values-fr/strings_help_faq.xml b/sensorhub-android-app/res/values-fr/strings_help_faq.xml new file mode 100644 index 00000000..bad9ec05 --- /dev/null +++ b/sensorhub-android-app/res/values-fr/strings_help_faq.xml @@ -0,0 +1,75 @@ + + + + + Premiers pas + Premiers pas + Premiers pas + + Configuration du serveur + Configuration du serveur + Configuration du serveur + Configuration du serveur + + Capteurs + Capteurs + + Services + + Dépannage + Dépannage + + Général + Général + Général + + + + + Qu\'est-ce qu\'OpenSensorHub EdgeX ? + Comment me connecter à un serveur ? + Comment commencer à diffuser les données des capteurs ? + + Quelle est la différence entre ConSysAPI et SOS-T ? + Comment configurer l\'authentification OAuth ? + Que fait « Ignorer la vérification SSL » ? + Puis-je me connecter à plusieurs serveurs ? + + Comment connecter un capteur Bluetooth ? + Qu\'est-ce que l\'extension UID ? + + Qu\'est-ce que le service de découverte ? + + Que signifient les couleurs d\'état ? + Mes capteurs ne diffusent pas. Que dois-je vérifier ? + + Comment changer le thème de l\'application ? + Où trouver l\'adresse IP de mon appareil ? + Quelle version d\'Android est requise ? + + + + + OpenSensorHub EdgeX est une application d\'agrégation et de diffusion de capteurs. Elle collecte les données des capteurs intégrés de votre appareil et des périphériques Bluetooth connectés, puis les diffuse en temps réel vers un serveur OpenSensorHub. Elle transforme votre appareil Android en nœud de calcul en périphérie pour les réseaux de capteurs IoT. + Allez dans l\'onglet Paramètres, appuyez sur « Gérer les serveurs », puis sur le bouton d\'ajout. Saisissez un nom pour le profil, l\'adresse du serveur, le port et le chemin du point d\'accès. Vous pouvez également configurer l\'authentification (nom d\'utilisateur/mot de passe ou OAuth) et activer TLS/SSL si votre serveur l\'exige. + D\'abord, activez les capteurs souhaités dans l\'onglet Capteurs. Ensuite, assurez-vous qu\'au moins un profil de serveur est configuré et activé dans l\'onglet Paramètres. Enfin, appuyez sur le bouton lecture sur le tableau de bord pour commencer la diffusion. Vous pouvez surveiller l\'état de chaque service depuis l\'écran État de l\'application. + + Connected Systems API (ConSysAPI) est l\'API REST standard OGC moderne pour publier les données de capteurs. C\'est l\'option recommandée pour les nouveaux déploiements. SOS-T (Sensor Observation Service – Transactionnel) est un protocole hérité basé sur XML. Utilisez SOS-T uniquement si votre serveur ne prend pas en charge ConSysAPI. + Lors de la modification d\'un profil de serveur, activez l\'option OAuth, puis renseignez l\'URL du point d\'accès de jeton, l\'ID client et le secret client fournis par l\'administrateur de votre serveur. OAuth est utilisé lorsque votre serveur nécessite une authentification par jeton plutôt que des identifiants nom d\'utilisateur/mot de passe classiques. + « Ignorer la vérification SSL » désactive la validation des certificats lors de la connexion via TLS/SSL. Cela est utile pour les environnements de développement ou de test utilisant des certificats auto-signés. Ne l\'activez pas en production, car cela rend la connexion vulnérable aux attaques de type « homme du milieu ». + Oui. Vous pouvez créer plusieurs profils de serveur dans Paramètres > Gérer les serveurs. Chaque profil peut être activé ou désactivé individuellement. Lors de la diffusion, les données sont envoyées simultanément à tous les profils de serveur activés. + + D\'abord, appairez l\'appareil Bluetooth avec votre appareil Android via les paramètres Bluetooth du système. Ensuite, dans l\'onglet Capteurs, activez le commutateur du capteur correspondant et sélectionnez votre appareil dans le sélecteur si demandé. Assurez-vous que le Bluetooth est activé et que le périphérique est allumé et à portée. + L\'extension UID est un identifiant optionnel ajouté à l\'identifiant unique de chaque flux de capteur. Elle permet de distinguer les données de cet appareil spécifique lorsque plusieurs appareils diffusent vers le même serveur. Vous pouvez la définir dans l\'onglet Capteurs sous « Extension UID des capteurs ». + + Le service de découverte permet à l\'application de découvrir automatiquement d\'autres capteurs et appareils sur le réseau à l\'aide d\'ensembles de règles configurables. Vous pouvez fournir une URL vers un fichier de règles de découverte dans l\'onglet Paramètres. Cela est utile dans les déploiements plus importants où les appareils doivent se trouver automatiquement. + + Sur l\'écran État de l\'application :\n\n\u2022 Vert – Le service est démarré et fonctionne normalement.\n\u2022 Orange – Le service est en cours d\'initialisation ou de démarrage.\n\u2022 Rouge – Le service est arrêté ou a rencontré une erreur.\n\u2022 Gris – L\'état est inconnu ou le service n\'a pas été démarré. + Vérifiez les points suivants :\n\n1. Assurez-vous que les capteurs souhaités sont activés dans l\'onglet Capteurs.\n2. Vérifiez qu\'au moins un profil de serveur est configuré et activé dans les Paramètres.\n3. Confirmez que les services requis (ConSysAPI ou SOS) sont activés dans les Paramètres.\n4. Consultez l\'écran État de l\'application pour repérer les services en rouge ou en gris.\n5. Assurez-vous que toutes les autorisations requises (Localisation, Caméra, Bluetooth, etc.) ont été accordées dans les paramètres Android.\n6. Vérifiez que votre appareil dispose d\'une connexion réseau et peut atteindre le serveur. + + Allez dans Préférences de l\'application (appuyez sur l\'icône d\'engrenage dans la barre d\'outils du tableau de bord), puis appuyez sur « Apparence ». Vous pouvez choisir entre Système par défaut (suit le thème de votre appareil), Clair ou Sombre. + L\'adresse IP de votre appareil est affichée dans les Préférences de l\'application, sous le champ « Adresse IP de l\'appareil ». C\'est l\'adresse que d\'autres appareils ou services peuvent utiliser pour atteindre votre appareil sur le réseau local. + OpenSensorHub EdgeX nécessite Android 14 (niveau d\'API 34) ou supérieur. + + + diff --git a/sensorhub-android-app/res/values-it/strings.xml b/sensorhub-android-app/res/values-it/strings.xml new file mode 100644 index 00000000..52da53b4 --- /dev/null +++ b/sensorhub-android-app/res/values-it/strings.xml @@ -0,0 +1,419 @@ + + + OpenSensorHub + Configura le impostazioni e tocca il pulsante di riproduzione per avviare SmartHub + Impostazioni + Avvia SmartHub + Ferma SmartHub + Stato dell\'app + Informazioni + Avvia proxy + Ferma proxy + Messaggio Meshtastic + Sensore Android + Monitor salute Angel + Radio Meshtastic + Scansione reti + Monitor cardiaco Polar + Stazione meteo Kestrel + Controller USB + Telemetro TruPulse + Flirone + Dosimetro STE RadPager + Modello + Accelerometro + Giroscopio + Magnetometro + Orientamento (Quaternioni) + Orientamento (Angoli di Eulero) + Posizione GPS + Posizione da rete + Dati video + Rollio video + Audio + + Nome dispositivo + Indirizzo IP del dispositivo + Profili server + Gestisci server + Servizi + Servizio SOS + Servizio Connected Systems + Servizio di scoperta + + Trasmetti dati di movimento dell\'accelerometro in tempo reale + Trasmetti dati di rotazione del giroscopio in tempo reale + Trasmetti dati di direzione e campo magnetico del magnetometro + Trasmetti dati di orientamento e assetto del dispositivo + Trasmetti dati di posizione e movimento via GPS + Trasmetti dati di posizione basati sulla rete + Trasmetti video in diretta dalla fotocamera del dispositivo + Allega metadati di rollio del dispositivo alle intestazioni dei fotogrammi video + Trasmetti audio in diretta dal microfono del dispositivo + Connetti a un dispositivo Meshtastic via Bluetooth all\'avvio + Trasmetti dati di scansione delle reti wireless vicine + Connetti a un sensore cardiaco Polar via Bluetooth LE + Trasmetti dati ambientali da una stazione meteo Kestrel + Connetti a un controller USB compatibile all\'avvio + Trasmetti dati di distanza e angolo da un telemetro TruPulse + Usa misurazioni simulate di TruPulse per i test + Trasmetti dati biometrici dal sensore Angel + Trasmetti immagini termiche da una fotocamera FLIR One collegata + Connetti a un STE RadPager via Bluetooth LE all\'avvio + Abilita il driver sensore modello per sviluppo e test + Opzioni per l\'invio dei dati + API REST standard OGC che serve i dati dei sensori di questo dispositivo. + API legacy che serve dati dei sensori in XML via HTTP + Servizio che fornisce scoperta basata su set di regole definibili + Tocca per selezionare o inserire l\'indirizzo del dispositivo + Estensione UID sensori + Opzionale — aggiungi server per inviare dati da remoto. Senza server, i dati sono serviti localmente. + + + JPEG + H264 + H265 + VP9 + VP8 + + + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + + + Quaternione + Eulero + + + QUATERNION + EULER + + + QUATERNION + + + + GPS + Rete + + + GPS + NETWORK + + + GPS + + + + Recuperabile + Archivia localmente + Invia da remoto + + + FETCH_LOCAL + STORE_LOCAL + PUSH_REMOTE + + + PUSH_REMOTE + + + Nome dispositivo TruPulse + + Dispositivo fisico in streaming + Simula dispositivo virtuale + + + STREAM + SIMULATED + + STREAM + + + Seleziona elemento da segnalare + Chiusura stradale + Alluvione + Medico + Soccorso + + + + Beacon più vicino + Trilaterazione + + + NEAREST + TRILATERATION + + + NEAREST + + + + GPS + Telemetro laser + Rete + + + Rapporto situazione + NOME: + DESCRIZIONE: + CATTURA + REIMPOSTA + INVIA RAPPORTO + Un nome per il rapporto + + Raggio: + Lat: + Lon: + Piedi + + + Seleziona... + Pubblico + Tutti + + + + Seleziona... + Apri + Chiudi + + + Azione: + Rif. ID: + + Tipo: + + + Seleziona... + Drenaggio canale + Superficie terrestre + + + + Seleziona... + Misuratore + Visivo + Modello + + + Tipo di elemento: + Profondità: + Modo oss.: + + Descrivi condizione medica... + Inserisci misurazione (PA, Temp., ecc.)... + Emergenza (V/F) + + + Seleziona... + Ambiente + Salute + Sicurezza + Servizi + + + + Seleziona... + 5 + 4 + 3 + 2 + 1 + + + Tipo di soccorso: + N. persone: + Urgenza: + Descrivi il soccorso necessario... + Inserisci il tuo nome o identificativo + + + Seleziona... + Persona + Veicolo + Dispositivo + + + + Seleziona... + GPS + Beacon Bluetooth + WiFi + Cellulare + UWB + N/D + + + Risorsa tracciata: + Metodo di tracciamento: + Inserisci ID risorsa + Inserisci etichetta risorsa + + + AAC + AMR-NB + AMR-WB + FLAC + VORBIS + OPUS + PCM + + + + 0.172 + 0.204 + 0.222 + 0.224 + 0.257 + + + + pollici + mil + tmoa + smoa + clic + cm + + + + S + D + + + + + + + + + + + Sensori + Home + Impostazioni + Preferenze dell\'app + + Inserisci un messaggio! + Inserisci un messaggio! + Inserisci l\'ID del nodo di destinazione (intero) + Inserisci l\'ID del nodo di destinazione (intero) + + + Notifiche + Lingua + Versione + Informazioni sull\'app + Aiuto / FAQ + Lingua + Una piattaforma software per costruire reti di sensori intelligenti e l\'Internet delle Cose + + + INTEGRATI + Periferiche Bluetooth + Altri + Codec + Frequenza fotogrammi + Risoluzione + Fotocamera + Frequenza di campionamento + Bitrate (kbps) + Seleziona dispositivo Meshtastic + Seleziona dispositivo Polar HR + Seleziona dispositivo Kestrel + Sorgente dati TruPulse + Seleziona dispositivo TruPulse + Usa dati simulati di TruPulse + Seleziona dispositivo modello + Un ID aggiunto alla fine dell\'UID di ogni sistema + URL del file Discovery Rules.txt + + + Trasmissione video + Cambia fotocamera + Mostra + Nascondi + Meshtastic + Connesso + Messaggio + Sensori + Nessun sensore configurato per l\'invio remoto + Nome esecuzione + Inserisci il nome per questa esecuzione + Invia messaggio Meshtastic + + + Nessun profilo server configurato.\nTocca + per crearne uno. + Aggiungi profilo server + Elimina server + Rimuovere \"%s\"? + + + CLIENT + Client CS API + Client SOS-T + CONNESSIONE + Nome server + Host / IP + Porta + Percorso endpoint + Abilita TLS + Disabilita validazione SSL + AUTENTICAZIONE + Nessuno + Base + OAuth + Nome utente + Password + Endpoint token + ID client + Segreto client + Aggiungi server + + + OK + Annulla + Elimina + Invia + Seleziona dispositivo + Inserisci nome o indirizzo del dispositivo + Inserisci un nome dispositivo (es. \"Ballistic\") o indirizzo MAC. I nomi vengono confrontati dall\'inizio, senza distinzione tra maiuscole e minuscole. + Modifica server + Wearable Garmin + Seleziona dispositivo Garmin + es. Ballistic o AA:BB:CC:DD:EE:FF + Inserisci nome o indirizzo manualmente… + Cerca nuovi dispositivi… + Cambia fotocamera + Trasmetti dati biometrici sanitari dal wearable Garmin + + + Arresto di SensorHub + SensorHub arrestato + Avvio di SensorHub… + Avvio di SensorHub fallito + Errore configurazione video: Controlla le impostazioni + Controlla le impostazioni video e assicurati che la risoluzione per il preset selezionato sia stata impostata. + SensorHub non è in esecuzione + Messaggio inviato + Invio del messaggio fallito + Profilo non trovato + Nome, host e porta sono obbligatori + L\'host non deve includere un protocollo (es. http://) + La porta deve essere un numero + La porta deve essere compresa tra 1 e 65535 + + diff --git a/sensorhub-android-app/res/values-it/strings_app_status.xml b/sensorhub-android-app/res/values-it/strings_app_status.xml new file mode 100644 index 00000000..91c3b866 --- /dev/null +++ b/sensorhub-android-app/res/values-it/strings_app_status.xml @@ -0,0 +1,21 @@ + + + Stato dell\'app + + + Inizializzazione + Inizializzato + Avvio + Avviato + Arresto + Fermato + Sconosciuto + + + Stato servizio SOS + Stato servizio ConSys + Stato servizio di scoperta + Stato server HTTP + Stato sensori Android + Stato archiviazione sensori Android + diff --git a/sensorhub-android-app/res/values-it/strings_help_faq.xml b/sensorhub-android-app/res/values-it/strings_help_faq.xml new file mode 100644 index 00000000..3c3e90e2 --- /dev/null +++ b/sensorhub-android-app/res/values-it/strings_help_faq.xml @@ -0,0 +1,75 @@ + + + + + Per iniziare + Per iniziare + Per iniziare + + Configurazione del server + Configurazione del server + Configurazione del server + Configurazione del server + + Sensori + Sensori + + Servizi + + Risoluzione dei problemi + Risoluzione dei problemi + + Generale + Generale + Generale + + + + + Cos\'è OpenSensorHub EdgeX? + Come mi collego a un server? + Come inizio a trasmettere i dati dei sensori? + + Qual è la differenza tra ConSysAPI e SOS-T? + Come configuro l\'autenticazione OAuth? + Cosa fa \"Ignora verifica SSL\"? + Posso collegarmi a più server? + + Come collego un sensore Bluetooth? + Cos\'è l\'estensione UID? + + Cos\'è il servizio di scoperta? + + Cosa significano i colori di stato? + I miei sensori non trasmettono. Cosa devo verificare? + + Come cambio il tema dell\'app? + Dove posso trovare l\'indirizzo IP del mio dispositivo? + Quale versione di Android è richiesta? + + + + + OpenSensorHub EdgeX è un\'applicazione di aggregazione e trasmissione dati dai sensori. Raccoglie i dati dai sensori integrati del dispositivo e dalle periferiche Bluetooth collegate, quindi trasmette quei dati a un server OpenSensorHub in tempo reale. Trasforma il tuo dispositivo Android in un nodo di edge computing per le reti di sensori IoT. + Vai alla scheda Impostazioni, tocca \"Gestisci server\", quindi tocca il pulsante aggiungi. Inserisci un nome per il profilo, l\'indirizzo del server, la porta e il percorso dell\'endpoint. Puoi anche configurare l\'autenticazione (nome utente/password o OAuth) e abilitare TLS/SSL se il tuo server lo richiede. + Prima, abilita i sensori desiderati nella scheda Sensori. Poi, assicurati di avere almeno un profilo server configurato e abilitato nella scheda Impostazioni. Infine, tocca il pulsante di riproduzione nella Dashboard per avviare la trasmissione. Puoi monitorare lo stato di ogni servizio dalla schermata Stato dell\'app. + + Connected Systems API (ConSysAPI) è la moderna API REST standard OGC per pubblicare dati dei sensori. È l\'opzione consigliata per le nuove installazioni. SOS-T (Servizio di Osservazione dei Sensori – Transazionale) è un protocollo XML legacy. Usa SOS-T solo se il tuo server non supporta ConSysAPI. + Durante la modifica di un profilo server, abilita l\'opzione OAuth, quindi compila l\'URL dell\'endpoint token, l\'ID client e il segreto client forniti dall\'amministratore del tuo server. OAuth viene usato quando il tuo server richiede l\'autenticazione basata su token anziché le credenziali base nome utente/password. + \"Ignora verifica SSL\" disabilita la validazione dei certificati durante la connessione tramite TLS/SSL. È utile per ambienti di sviluppo o test che utilizzano certificati autofirmati. Non abilitarlo in produzione, poiché rende la connessione vulnerabile ad attacchi man-in-the-middle. + Sì. Puoi creare più profili server in Impostazioni > Gestisci server. Ogni profilo può essere abilitato o disabilitato individualmente. Durante la trasmissione, i dati vengono inviati simultaneamente a tutti i profili server abilitati. + + Prima, associa il dispositivo Bluetooth al tuo dispositivo Android tramite le impostazioni Bluetooth del sistema. Poi, nella scheda Sensori, abilita l\'interruttore del sensore corrispondente e seleziona il tuo dispositivo dal selettore se richiesto. Assicurati che il Bluetooth sia attivato e che la periferica sia accesa e nel raggio d\'azione. + L\'estensione UID è un identificatore opzionale aggiunto all\'ID univoco di ogni flusso del sensore. Aiuta a distinguere i dati di questo specifico dispositivo quando più dispositivi trasmettono allo stesso server. Puoi impostarla nella scheda Sensori sotto \"Estensione UID sensori\". + + Il servizio di scoperta consente all\'app di scoprire automaticamente altri sensori e dispositivi sulla rete utilizzando set di regole configurabili. Puoi fornire un URL a un file di regole di scoperta nella scheda Impostazioni. È utile nelle installazioni più grandi dove i dispositivi devono trovarsi automaticamente. + + Nella schermata Stato dell\'app:\n\n\u2022 Verde – Il servizio è avviato e funziona normalmente.\n\u2022 Arancione – Il servizio si sta inizializzando o avviando.\n\u2022 Rosso – Il servizio è fermo o ha riscontrato un errore.\n\u2022 Grigio – Lo stato è sconosciuto o il servizio non è stato avviato. + Verifica quanto segue:\n\n1. Assicurati che i sensori desiderati siano abilitati nella scheda Sensori.\n2. Verifica che almeno un profilo server sia configurato e abilitato nelle Impostazioni.\n3. Conferma che i servizi richiesti (ConSysAPI o SOS) siano abilitati nelle Impostazioni.\n4. Controlla la schermata Stato dell\'app per eventuali servizi che mostrano rosso o grigio.\n5. Assicurati che tutte le autorizzazioni necessarie (Posizione, Fotocamera, Bluetooth, ecc.) siano state concesse nelle Impostazioni Android.\n6. Verifica che il tuo dispositivo abbia connettività di rete e possa raggiungere il server. + + Vai alle Preferenze dell\'app (tocca l\'icona dell\'ingranaggio nella barra degli strumenti della Dashboard), quindi tocca \"Aspetto\". Puoi scegliere tra Predefinito di sistema (segue il tema del dispositivo), Chiaro o Scuro. + L\'indirizzo IP del tuo dispositivo è visualizzato nelle Preferenze dell\'app, sotto il campo \"Indirizzo IP del dispositivo\". Questo è l\'indirizzo che altri dispositivi o servizi possono usare per raggiungere il tuo dispositivo sulla rete locale. + OpenSensorHub EdgeX richiede Android 14 (livello API 34) o superiore. + + + diff --git a/sensorhub-android-app/res/values-pt/strings.xml b/sensorhub-android-app/res/values-pt/strings.xml new file mode 100644 index 00000000..78e5adb3 --- /dev/null +++ b/sensorhub-android-app/res/values-pt/strings.xml @@ -0,0 +1,419 @@ + + + OpenSensorHub + Configure as definições e toque no botão de reprodução para iniciar o SmartHub + Configurações + Iniciar SmartHub + Parar SmartHub + Status do aplicativo + Sobre + Iniciar proxy + Parar proxy + Mensagem Meshtastic + Sensor Android + Monitor de saúde Angel + Rádio Meshtastic + Varredura de redes + Monitor cardíaco Polar + Estação meteorológica Kestrel + Controlador USB + Telêmetro TruPulse + Flirone + Dosímetro STE RadPager + Modelo + Acelerômetro + Giroscópio + Magnetômetro + Orientação (Quaternions) + Orientação (Ângulos de Euler) + Localização GPS + Localização por rede + Dados de vídeo + Rotação de vídeo + Áudio + + Nome do dispositivo + Endereço IP do dispositivo + Perfis de servidor + Gerenciar servidores + Serviços + Serviço SOS + Serviço Connected Systems + Serviço de descoberta + + Transmitir dados de movimento do acelerômetro em tempo real + Transmitir dados de rotação do giroscópio em tempo real + Transmitir dados de direção e campo magnético do magnetômetro + Transmitir dados de orientação e atitude do dispositivo + Transmitir dados de localização e movimento por GPS + Transmitir dados de localização baseados em rede + Transmitir vídeo ao vivo da câmera do dispositivo + Anexar metadados de rotação do dispositivo aos cabeçalhos de quadros de vídeo + Transmitir áudio ao vivo do microfone do dispositivo + Conectar a um dispositivo Meshtastic via Bluetooth ao iniciar + Transmitir dados de varredura de redes sem fio próximas + Conectar a um sensor cardíaco Polar via Bluetooth LE + Transmitir dados ambientais de uma estação meteorológica Kestrel + Conectar a um controlador USB compatível ao iniciar + Transmitir dados de distância e ângulo de um telêmetro TruPulse + Usar medições simuladas do TruPulse para testes + Transmitir dados biométricos do Angel Sensor + Transmitir imagens térmicas de uma câmera FLIR One conectada + Conectar a um STE RadPager via Bluetooth LE ao iniciar + Habilitar o driver de sensor modelo para desenvolvimento e testes + Opções para envio de dados + API REST padrão OGC servindo dados de sensores deste dispositivo. + API legada servindo dados de sensores em XML via HTTP + Serviço que fornece descoberta baseada em conjuntos de regras definíveis + Toque para selecionar ou inserir o endereço do dispositivo + Extensão UID dos sensores + Opcional — adicione servidores para enviar dados remotamente. Sem servidores, os dados são servidos localmente. + + + JPEG + H264 + H265 + VP9 + VP8 + + + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + + + Quaternion + Euler + + + QUATERNION + EULER + + + QUATERNION + + + + GPS + Rede + + + GPS + NETWORK + + + GPS + + + + Recuperável + Armazenar localmente + Enviar remotamente + + + FETCH_LOCAL + STORE_LOCAL + PUSH_REMOTE + + + PUSH_REMOTE + + + Nome do dispositivo TruPulse + + Dispositivo físico em fluxo + Simular dispositivo virtual + + + STREAM + SIMULATED + + STREAM + + + Selecionar item a reportar + Fechamento de rua + Inundação + Médico + Assistência + + + + Beacon mais próximo + Trilateração + + + NEAREST + TRILATERATION + + + NEAREST + + + + GPS + Telêmetro a laser + Rede + + + Relatório de situação + NOME: + DESCRIÇÃO: + CAPTURAR + REDEFINIR + ENVIAR RELATÓRIO + Um nome para o relatório + + Raio: + Lat: + Lon: + Pés + + + Selecionar... + Público + Todos + + + + Selecionar... + Abrir + Fechar + + + Ação: + Ref. ID: + + Tipo: + + + Selecionar... + Drenagem de canal + Superfície terrestre + + + + Selecionar... + Medidor + Visual + Modelo + + + Tipo de feição: + Profundidade: + Modo obs.: + + Descrever condição médica... + Inserir medição (PA, Temp., etc.)... + Emergência (V/F) + + + Selecionar... + Ambiental + Saúde + Segurança + Serviços + + + + Selecionar... + 5 + 4 + 3 + 2 + 1 + + + Tipo de assistência: + Nº de pessoas: + Urgência: + Descrever assistência necessária... + Insira seu nome ou identificação + + + Selecionar... + Pessoa + Veículo + Dispositivo + + + + Selecionar... + GPS + Beacon Bluetooth + WiFi + Celular + UWB + N/D + + + Recurso rastreado: + Método de rastreamento: + Inserir ID do recurso + Inserir rótulo do recurso + + + AAC + AMR-NB + AMR-WB + FLAC + VORBIS + OPUS + PCM + + + + 0.172 + 0.204 + 0.222 + 0.224 + 0.257 + + + + polegadas + mil + tmoa + smoa + cliques + cm + + + + E + D + + + + + + + + + + + Sensores + Início + Configurações + Preferências do aplicativo + + Digite uma mensagem! + Digite uma mensagem! + Insira o ID do nó de destino (inteiro) + Insira o ID do nó de destino (inteiro) + + + Notificações + Idioma + Versão + Sobre o aplicativo + Ajuda / Perguntas frequentes + Idioma + Uma plataforma de software para construir redes de sensores inteligentes e a Internet das Coisas + + + INTEGRADOS + Periféricos Bluetooth + Outros + Codec + Taxa de quadros + Resolução + Câmera + Taxa de amostragem + Taxa de bits (kbps) + Selecionar dispositivo Meshtastic + Selecionar dispositivo Polar HR + Selecionar dispositivo Kestrel + Fonte de dados TruPulse + Selecionar dispositivo TruPulse + Usar dados simulados do TruPulse + Selecionar dispositivo modelo + Um ID anexado ao final do UID de cada sistema + URL do arquivo Discovery Rules.txt + + + Transmissão de vídeo + Alternar câmera + Mostrar + Ocultar + Meshtastic + Conectado + Mensagem + Sensores + Nenhum sensor configurado para envio remoto + Nome da execução + Insira o nome para esta execução + Enviar mensagem Meshtastic + + + Nenhum perfil de servidor configurado.\nToque + para criar um. + Adicionar perfil de servidor + Excluir servidor + Remover \"%s\"? + + + CLIENTE + Cliente CS API + Cliente SOS-T + CONEXÃO + Nome do servidor + Host / IP + Porta + Caminho do endpoint + Habilitar TLS + Desabilitar validação SSL + AUTENTICAÇÃO + Nenhum + Básico + OAuth + Usuário + Senha + Endpoint de token + ID do cliente + Segredo do cliente + Adicionar servidor + + + OK + Cancelar + Excluir + Enviar + Selecionar dispositivo + Inserir nome ou endereço do dispositivo + Insira um nome de dispositivo (ex. \"Ballistic\") ou endereço MAC. Os nomes são comparados desde o início, sem distinção de maiúsculas. + Editar servidor + Wearable Garmin + Selecionar dispositivo Garmin + ex. Ballistic ou AA:BB:CC:DD:EE:FF + Inserir nome ou endereço manualmente… + Procurar novos dispositivos… + Alternar câmera + Transmitir dados biométricos de saúde do wearable Garmin + + + Parando SensorHub + SensorHub parado + Iniciando SensorHub… + Falha ao iniciar SensorHub + Erro de configuração de vídeo: Verifique as configurações + Verifique as configurações de vídeo e certifique-se de que a resolução para o preset selecionado foi definida. + SensorHub não está em execução + Mensagem enviada + Falha ao enviar mensagem + Perfil não encontrado + Nome, host e porta são obrigatórios + O host não deve incluir um protocolo (ex. http://) + A porta deve ser um número + A porta deve estar entre 1 e 65535 + + diff --git a/sensorhub-android-app/res/values-pt/strings_app_status.xml b/sensorhub-android-app/res/values-pt/strings_app_status.xml new file mode 100644 index 00000000..a4207d1a --- /dev/null +++ b/sensorhub-android-app/res/values-pt/strings_app_status.xml @@ -0,0 +1,21 @@ + + + Status do aplicativo + + + Inicializando + Inicializado + Iniciando + Iniciado + Parando + Parado + Desconhecido + + + Status do serviço SOS + Status do serviço ConSys + Status do serviço de descoberta + Status do servidor HTTP + Status dos sensores Android + Status do armazenamento de sensores Android + diff --git a/sensorhub-android-app/res/values-pt/strings_help_faq.xml b/sensorhub-android-app/res/values-pt/strings_help_faq.xml new file mode 100644 index 00000000..93a79287 --- /dev/null +++ b/sensorhub-android-app/res/values-pt/strings_help_faq.xml @@ -0,0 +1,75 @@ + + + + + Primeiros passos + Primeiros passos + Primeiros passos + + Configuração do servidor + Configuração do servidor + Configuração do servidor + Configuração do servidor + + Sensores + Sensores + + Serviços + + Solução de problemas + Solução de problemas + + Geral + Geral + Geral + + + + + O que é o OpenSensorHub EdgeX? + Como me conecto a um servidor? + Como começo a transmitir dados dos sensores? + + Qual é a diferença entre ConSysAPI e SOS-T? + Como configuro a autenticação OAuth? + O que faz \"Ignorar verificação SSL\"? + Posso conectar a vários servidores? + + Como conecto um sensor Bluetooth? + O que é a extensão UID? + + O que é o serviço de descoberta? + + O que significam as cores de status? + Meus sensores não estão transmitindo. O que devo verificar? + + Como mudo o tema do aplicativo? + Onde posso encontrar o endereço IP do meu dispositivo? + Qual versão do Android é necessária? + + + + + OpenSensorHub EdgeX é um aplicativo de agregação e transmissão de sensores. Ele coleta dados dos sensores integrados do seu dispositivo e periféricos Bluetooth conectados, e então transmite esses dados para um servidor OpenSensorHub em tempo real. Ele transforma seu dispositivo Android em um nó de computação de borda para redes de sensores IoT. + Vá até a aba Configurações, toque em \"Gerenciar servidores\" e depois toque no botão de adicionar. Insira um nome para o perfil, o endereço do servidor, porta e caminho do endpoint. Você também pode configurar a autenticação (usuário/senha ou OAuth) e habilitar TLS/SSL se seu servidor exigir. + Primeiro, habilite os sensores desejados na aba Sensores. Em seguida, certifique-se de que pelo menos um perfil de servidor esteja configurado e habilitado na aba Configurações. Por fim, toque no botão de reprodução no Painel para iniciar a transmissão. Você pode monitorar o status de cada serviço na tela de Status do aplicativo. + + Connected Systems API (ConSysAPI) é a API REST padrão OGC moderna para publicar dados de sensores. É a opção recomendada para novas implantações. SOS-T (Serviço de Observação de Sensores – Transacional) é um protocolo XML legado. Use SOS-T apenas se seu servidor não suportar ConSysAPI. + Ao editar um perfil de servidor, habilite a opção OAuth e preencha a URL do endpoint de token, ID do cliente e segredo do cliente fornecidos pelo administrador do seu servidor. OAuth é usado quando seu servidor requer autenticação baseada em tokens em vez de credenciais básicas de usuário/senha. + \"Ignorar verificação SSL\" desabilita a validação de certificados ao conectar via TLS/SSL. Isso é útil para ambientes de desenvolvimento ou teste que usam certificados autoassinados. Não habilite isso em produção, pois torna a conexão vulnerável a ataques man-in-the-middle. + Sim. Você pode criar vários perfis de servidor em Configurações > Gerenciar servidores. Cada perfil pode ser habilitado ou desabilitado individualmente. Ao transmitir, os dados são enviados para todos os perfis de servidor habilitados simultaneamente. + + Primeiro, emparelhe o dispositivo Bluetooth com seu dispositivo Android através das configurações de Bluetooth do sistema. Em seguida, na aba Sensores, habilite o interruptor do sensor correspondente e selecione seu dispositivo no seletor, se solicitado. Certifique-se de que o Bluetooth esteja ligado e o periférico esteja ligado e ao alcance. + A extensão UID é um identificador opcional adicionado ao ID único de cada fluxo de sensor. Ela ajuda a distinguir os dados deste dispositivo específico quando vários dispositivos estão transmitindo para o mesmo servidor. Você pode configurá-la na aba Sensores em \"Extensão UID dos sensores\". + + O serviço de descoberta permite que o aplicativo descubra automaticamente outros sensores e dispositivos na rede usando conjuntos de regras configuráveis. Você pode fornecer uma URL para um arquivo de regras de descoberta na aba Configurações. Isso é útil em implantações maiores onde os dispositivos precisam se encontrar automaticamente. + + Na tela de Status do aplicativo:\n\n\u2022 Verde – O serviço está iniciado e funcionando normalmente.\n\u2022 Laranja – O serviço está inicializando ou iniciando.\n\u2022 Vermelho – O serviço está parado ou encontrou um erro.\n\u2022 Cinza – O status é desconhecido ou o serviço não foi iniciado. + Verifique o seguinte:\n\n1. Certifique-se de que os sensores desejados estão habilitados na aba Sensores.\n2. Verifique se pelo menos um perfil de servidor está configurado e habilitado nas Configurações.\n3. Confirme que os serviços necessários (ConSysAPI ou SOS) estão habilitados nas Configurações.\n4. Verifique a tela de Status do aplicativo para serviços mostrando vermelho ou cinza.\n5. Certifique-se de que todas as permissões necessárias (Localização, Câmera, Bluetooth, etc.) foram concedidas nas Configurações do Android.\n6. Verifique se seu dispositivo tem conectividade de rede e pode alcançar o servidor. + + Vá até Preferências do aplicativo (toque no ícone de engrenagem na barra de ferramentas do Painel) e toque em \"Aparência\". Você pode escolher entre Padrão do sistema (segue o tema do seu dispositivo), Claro ou Escuro. + O endereço IP do seu dispositivo é exibido nas Preferências do aplicativo, no campo \"Endereço IP do dispositivo\". Este é o endereço que outros dispositivos ou serviços podem usar para alcançar seu dispositivo na rede local. + OpenSensorHub EdgeX requer Android 14 (nível de API 34) ou superior. + + + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings.xml b/sensorhub-android-app/res/values-zh-rTW/strings.xml index 59d961f9..d2a3b5dd 100644 --- a/sensorhub-android-app/res/values-zh-rTW/strings.xml +++ b/sensorhub-android-app/res/values-zh-rTW/strings.xml @@ -1,7 +1,7 @@ OpenSensorHub - 設定參數並點擊播放按鈕以啟動 SmartHub + 配置設定並點擊播放按鈕以啟動 SmartHub 設定 啟動 SmartHub 停止 SmartHub @@ -11,211 +11,177 @@ 停止代理 Meshtastic 訊息 Android 感測器 - TruPulse 測距感測器 - Meshtastic 感測器 - Angel 感測器 - Flirone 感測器 + Angel 健康監測器 + Meshtastic 無線電 + 無線網路掃描 Polar 心率監測器 - 控制器 - 紅隼感測器 - 雷達偵測感測器 - STE輻射尋呼機 感測器 - 範本 感測器 - 加速度計資料 - 陀螺儀資料 - 磁力計資料 - 方向資料(四元數) - 方向資料(歐拉角) - GPS 定位資料 - 網路定位資料 - 影片資料 - 影片滾動資料 - 音訊資料 + Kestrel 氣象儀 + USB 控制器 + TruPulse 測距儀 + Flirone + STE 輻射偵測器 + 範本 + 加速度計 + 陀螺儀 + 磁力計 + 方向(四元數) + 方向(歐拉角) + GPS 定位 + 網路定位 + 影像資料 + 影像翻轉 + 音訊 裝置名稱 裝置 IP 位址 - 伺服器設定檔 + 伺服器配置 管理伺服器 服務 - 啟用 SOS 服務 - 啟用連線系統服務 - 啟用探索服務 - 執行名稱 - - 啟用加速度計資料串流 - 啟用陀螺儀資料串流 - 啟用磁力計資料串流 - 啟用方向資料串流 - 啟用 GPS 定位資料串流 - 啟用網路定位資料串流 - 啟用影片資料串流 - 在影片幀標頭中包含影片滾��資料 - 啟用音訊資料串流 - 啟用 Meshtastic 資料串流(感測器須在啟動時透過藍牙連接) - 啟用雷達偵測感測器串流 - 啟用 Polar 心���感測器串流(感測器須透過藍牙 LE 連接) - 啟用紅隼氣象儀串流 - 啟用 USB 控制器串流(控制器須在啟動時透過 USB 連接) - 啟用 TruPulse 測距儀資料串流(感測器須在啟動時透過藍牙連接) - 使用模擬 TruPulse 資料取代實際感測器資料 - 啟用 Angel 感測器健康資料串流(感測器須在啟動時透過藍牙 LE 連接) - 啟用 FLIR One 熱像儀資料串流(透過 USB 連接時) - 啟用 STE RadPager 資料串流��感測器須在啟動時透過藍牙 LE 連接) - 啟用範本驅動程式串流 - Stream biometric data from the Garmin Wearable Sensor - - - - - - 資料推送選項 + SOS 服務 + Connected Systems 服務 + 探索服務 + + 串流即時加速度計運動資料 + 串流即時陀螺儀旋轉資料 + 串流磁力計方向和磁場資料 + 串流裝置方向和姿態資料 + 串流 GPS 定位和移動資料 + 串流網路定位資料 + 從裝置相機串流即時影像 + 將裝置翻轉中繼資料附加到影像幀標頭 + 從裝置麥克風串流即時音訊 + 啟動時透過藍牙連接 Meshtastic 裝置 + 串流附近無線網路掃描資料 + 透過藍牙低功耗連接 Polar 心率感測器 + 從 Kestrel 氣象儀串流環境資料 + 啟動時連接支援的 USB 控制器 + 從 TruPulse 測距儀串流距離和角度資料 + 使用模擬 TruPulse 測量資料進行測試 + 從 Angel Sensor 串流生物特徵資料 + 從連接的 FLIR One 相機串流熱影像 + 啟動時透過藍牙低功耗連接 STE RadPager + 啟用範本感測器驅動程式用於開發和測試 + 推送資料選項 + 提供此裝置感測器資料的 OGC 標準 REST API。 + 透過 HTTP 以 XML 格式提供感測器資料的舊版 API + 基於可定義規則集提供探索功能的服務 點擊以選擇或輸入裝置位址 - 感測器 UID 延伸 - 選用 — 新增伺服器以遠端推送資料。未設定伺服器時,資料僅於本機提供。 - - - 四元數 - 歐拉角 - - - - GPS - 網路 - - - - 可擷取 - 本機儲存 - 遠端推送 - - - Trupulse 裝置名稱 - - 串流實體裝置 - 模擬虛擬裝置 - - 溪流 - - - - 選擇報告項目 - 道路封閉 - 淹水 - 醫療 - 救助 - - - - 最近信標 - 三邊測量 - - - - GPS - 雷射測距儀 - 網路 - - - 現場報告 - 名稱: - 描述: - 拍攝 - 重設 - 提交報告 - 報告名稱 - - 半徑: - 緯度: - 經度: - 英尺 - - - 選擇... - 公共 - 全部 - + 感測器 UID 擴充 + 選填 — 新增伺服器以遠端推送資料。如無伺服器,資料將在本機提供。 - - 選擇... - 開放 - 關閉 - - - 動作: - 參考編號: - 類型: - - - 選擇... - 渠道排水 - 地表 - - - - 選擇... - 儀器 - 目視 - 模型 - - - 特徵類型: - 深度: - 觀測模式: - - 描述醫療狀況... - 輸入測量值(血壓、體溫等)... - 緊急狀況(是/否) - - - 選擇... - 環境 - 健康 - 安全 - 服務 - - - 救助類型: - 人數: - 緊急程度: - 描述所需救助... - 輸入您的姓名或編號 - - - 選擇... - 人員 - 車輛 - 裝置 - - - - 選擇... - GPS - 藍牙信標 - WiFi - 行動網路 - UWB - - - - 追蹤資源: - 追蹤方法: - 輸入資源編號 - 輸入資源標籤 - - - 英寸 - 密耳 - tmoa - smoa - - 公分 - 感測器 首頁 設定 + 應用程式偏好設定 + 輸入訊息! 輸入訊息! 輸入目標節點 ID(整數) 輸入目標節點 ID(整數) + + + 通知 + 語言 + 版本 + 關於應用程式 + 幫助 / 常見問題 + 語言 + 用於建構智慧感測器網路和物聯網的軟體平台 + + + 內建裝置 + 藍牙周邊裝置 + 其他 + 編解碼器 + 幀率 + 解析度 + 相機 + 取樣率 + 位元率 (kbps) + 選擇 Meshtastic 裝置 + 選擇 Polar HR 裝置 + 選擇 Kestrel 裝置 + TruPulse 資料來源 + 選擇 TruPulse 裝置 + 使用模擬 TruPulse 資料 + 選擇範本裝置 + 附加到每個系統 UID 末端的識別碼 + 探索規則檔案的 URL + + + 影像串流 + 切換相機 + 顯示 + 隱藏 + Meshtastic + 已連接 + 訊息 + 感測器 + 沒有設定為遠端推送的感測器 + 執行名稱 + 請輸入此次執行的名稱 + 傳送 Meshtastic 訊息 + + + 尚未配置伺服器。\n點擊 + 建立一個。 + 新增伺服器配置 + 刪除伺服器 + 移除「%s」? + + + 用戶端 + CS API 用戶端 + SOS-T 用戶端 + 連線 + 伺服器名稱 + 主機 / IP + 連接埠 + 端點路徑 + 啟用 TLS + 停用 SSL 驗證 + 認證 + + 基本 + OAuth + 使用者名稱 + 密碼 + 權杖端點 + 用戶端 ID + 用戶端密鑰 + 新增伺服器 + + + 確定 + 取消 + 刪除 + 傳送 + 選擇裝置 + 輸入裝置名稱或位址 + 輸入裝置名稱(例如「Ballistic」)或 MAC 位址。名稱從開頭比對,不分大小寫。 + 編輯伺服器 + Garmin 穿戴裝置 + 選擇 Garmin 裝置 + 例如 Ballistic 或 AA:BB:CC:DD:EE:FF + 手動輸入名稱或位址… + 掃描新裝置… + 切換相機 + 從 Garmin 穿戴裝置串流健康生物特徵資料 + + + 正在停止 SensorHub + SensorHub 已停止 + 正在啟動 SensorHub… + SensorHub 啟動失敗 + 影像配置錯誤:請檢查設定 + 請檢查影像設定,確認所選預設的解析度已設定。 + SensorHub 未在執行 + 訊息已傳送 + 訊息傳送失敗 + 找不到配置 + 名稱、主機和連接埠為必填 + 主機不應包含協定(例如 http://) + 連接埠應為數字 + 連接埠應介於 1 到 65535 之間 + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml b/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml deleted file mode 100644 index 28e4904f..00000000 --- a/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - 設定 - 一般 - 感測器 - diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml index 29d5bc40..d8a77793 100644 --- a/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml +++ b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml @@ -1,6 +1,5 @@ - 應用程式狀態 diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_help_faq.xml b/sensorhub-android-app/res/values-zh-rTW/strings_help_faq.xml new file mode 100644 index 00000000..ccef7007 --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings_help_faq.xml @@ -0,0 +1,73 @@ + + + + + 入門指南 + 入門指南 + 入門指南 + + 伺服器配置 + 伺服器配置 + 伺服器配置 + 伺服器配置 + + 感測器 + 感測器 + + 服務 + + 疑難排解 + 疑難排解 + + 一般 + 一般 + 一般 + + + + 什麼是 OpenSensorHub EdgeX? + 如何連接到伺服器? + 如何開始串流感測器資料? + + ConSysAPI 和 SOS-T 有什麼區別? + 如何設定 OAuth 認證? + 「跳過 SSL 驗證」是什麼意思? + 我可以連接多個伺服器嗎? + + 如何連接藍牙感測器? + 什麼是 UID 擴充? + + 什麼是探索服務? + + 狀態顏色代表什麼意思? + 我的感測器沒有在串流。該檢查什麼? + + 如何更改應用程式主題? + 在哪裡可以找到我的裝置 IP 位址? + 需要什麼 Android 版本? + + + + OpenSensorHub EdgeX 是一款感測器匯集和串流應用程式。它從您裝置的內建感測器和連接的藍牙周邊裝置收集資料,然後即時將資料串流到 OpenSensorHub 伺服器。它將您的 Android 裝置轉變為物聯網感測器網路的邊緣運算節點。 + 前往「設定」標籤頁,點擊「管理伺服器」,然後點擊新增按鈕。輸入配置名稱、伺服器主機位址、連接埠和端點路徑。您還可以配置認證(使用者名稱/密碼或 OAuth),如果您的伺服器需要,也可以啟用 TLS/SSL。 + 首先,在「感測器」標籤頁中啟用您想要的感測器。然後,確保在「設定」標籤頁中至少配置並啟用了一個伺服器配置。最後,在儀表板上點擊播放按鈕開始串流。您可以從應用程式狀態畫面監控每個服務的狀態。 + + Connected Systems API (ConSysAPI) 是用於發布感測器資料的現代 OGC 標準 REST API,是新部署的建議選項。SOS-T(感測器觀測服務 – 交易式)是舊版的 XML 協定。僅在您的伺服器不支援 ConSysAPI 時使用 SOS-T。 + 編輯伺服器配置時,啟用 OAuth 開關,然後填入伺服器管理員提供的權杖端點 URL、用戶端 ID 和用戶端密鑰。當您的伺服器需要基於權杖的認證而非基本使用者名稱/密碼時,使用 OAuth。 + 「跳過 SSL 驗證」會在透過 TLS/SSL 連接時停用憑證驗證。這對於使用自簽憑證的開發或測試環境很有用。請勿在生產環境中啟用此選項,因為它會使連接容易受到中間人攻擊。 + 可以。您可以在「設定」>「管理伺服器」下建立多個伺服器配置。每個配置可以個別啟用或停用。串流時,資料會同時傳送到所有已啟用的伺服器配置。 + + 首先,透過系統藍牙設定將藍牙裝置與您的 Android 裝置配對。然後,在「感測器」標籤頁中啟用對應的感測器開關,如果出現提示,請選擇您的裝置。確保藍牙已開啟且周邊裝置已開機並在範圍內。 + UID 擴充是附加到每個感測器串流唯一 ID 的可選識別碼。當多個裝置串流到同一伺服器時,它有助於區分來自此特定裝置的資料。您可以在「感測器」標籤頁的「感測器 UID 擴充」下設定。 + + 探索服務允許應用程式使用可配置的規則集自動探索網路上的其他感測器和裝置。您可以在「設定」標籤頁中提供探索規則檔案的 URL。這在較大的部署中很有用,裝置需要自動相互發現時。 + + 在應用程式狀態畫面上:\n\n\u2022 綠色 – 服務已啟動並正常運行。\n\u2022 橙色 – 服務正在初始化或啟動中。\n\u2022 紅色 – 服務已停止或遇到錯誤。\n\u2022 灰色 – 狀態未知或服務尚未啟動。 + 請檢查以下事項:\n\n1. 確保您想要的感測器已在「感測器」標籤頁中啟用。\n2. 驗證至少有一個伺服器配置已在「設定」中配置並啟用。\n3. 確認所需的服務(ConSysAPI 或 SOS)已在「設定」中啟用。\n4. 檢查應用程式狀態畫面,查看是否有服務顯示紅色或灰色。\n5. 確保已在 Android 設定中授予所有必要的權限(定位、相機、藍牙等)。\n6. 驗證您的裝置具有網路連線能力並可以連接到伺服器。 + + 前往「應用程式偏好設定」(點擊儀表板工具列上的齒輪圖示),然後點擊「外觀」。您可以選擇系統預設(跟隨您的裝置主題)、淺色或深色。 + 您的裝置 IP 位址顯示在「應用程式偏好設定」的「裝置 IP 位址」欄位中。這是其他裝置或服務在區域網路上連接您裝置時使用的位址。 + OpenSensorHub EdgeX 需要 Android 14(API 等級 34)或更高版本。 + + + diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 2610b173..aebba8f0 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -9,7 +9,7 @@ About Start Proxy Stop Proxy - Meshtastic Text Message + Meshtastic Msg Android Sensor Angel Health Monitor Meshtastic Radio @@ -41,8 +41,6 @@ Connected Systems Service Discovery Service - Run Name - Stream real-time accelerometer motion data Stream real-time gyroscope rotation data Stream magnetometer heading and magnetic field data @@ -59,20 +57,20 @@ Connect to a supported USB controller during startup Stream distance and angle data from a TruPulse rangefinder Use simulated TruPulse measurements for testing + Stream health biometric data from Garmin Wearable Stream biometric data from the Angel Sensor Stream thermal imagery from a connected FLIR One camera Connect to an STE RadPager over Bluetooth LE during startup Enable the template sensor driver for development and testing - Stream biometric data from the Garmin Wearable Sensor Options for pushing data OGC standard REST API serving this device’s sensor data. Legacy API serving sensor data as XML over HTTP Service providing discovery based on definable rulesets Tap to select or enter device address - Sensors UID Extension Optional — add servers to push data remotely. Without servers, data is served locally. + JPEG H264 @@ -310,8 +308,119 @@ Sensors Home Settings + App Preferences + Enter a message! Enter a message! Enter the destination Node ID (integer) Enter the destination Node ID (integer) + + + Notifications + Language + Version + About application + Help / FAQ + Language + A software platform for building smart sensor networks and the Internet of Things + + + ON-DEVICE + Bluetooth Peripherals + Others + Codec + Frame Rate + Resolution + Camera + Sample Rate + Bitrate (kbps) + Select Meshtastic Device + Select Polar HR Device + Select Kestrel Device + TruPulse Data Source + Select TruPulse Device + Use Simulated TruPulse Data + Select template Device + An ID attached to the end of each systems UID + URL to Discovery Rules.txt + + + Video Stream + Switch Camera + Show + Hide + Meshtastic + Connected + Message + Sensors + No Sensors Set to Push Remotely + Run Name + Please enter the name for this run + Send Meshtastic Message + + + No server profiles configured.\nTap + to create one. + Add server profile + Delete Server + Remove \"%s\"? + + + CLIENT + CS API Client + SOS-T Client + CONNECTION + Server name + Host / IP + Port + Endpoint path + Enable TLS + Disable SSL Validation + AUTHENTICATION + None + Basic + OAuth + Username + Password + Token Endpoint + Client ID + Client Secret + Add Server + + + OK + Cancel + Delete + Send + Select Device + Enter Device Name or Address + Enter a device name (e.g. \"Ballistic\") or MAC address. Names are matched from the start, case-insensitive. + Edit Server + Garmin Wearable + Select Garmin Device + e.g. Ballistic or AA:BB:CC:DD:EE:FF + Enter name or address manually… + Scan for new devices… + Switch Camera + + + Stopping SensorHub + SensorHub Stopped + Starting SensorHub… + SensorHub failed to start + Video Config Error: Check Settings + Check Video Settings and ensure the resolution for the selected preset has been set. + SensorHub not running + Message sent + Failed to send message + Profile not found + Name, host, and port are required + Host should not include a protocol (e.g. http://) + Port should be a number + Port should be between 1 and 65535 + + + License Key Required + Garmin Health SDK license key is not configured. Add it to gradle.properties. + Garmin SDK Error + Failed to initialize Garmin Health SDK: %s diff --git a/sensorhub-android-app/res/values/strings_help_faq.xml b/sensorhub-android-app/res/values/strings_help_faq.xml new file mode 100644 index 00000000..cdfd0ba9 --- /dev/null +++ b/sensorhub-android-app/res/values/strings_help_faq.xml @@ -0,0 +1,73 @@ + + + + + Getting Started + Getting Started + Getting Started + + Server Configuration + Server Configuration + Server Configuration + Server Configuration + + Sensors + Sensors + + Services + + Troubleshooting + Troubleshooting + + General + General + General + + + + + What is OpenSensorHub? + How do I connect to a server? + How do I start streaming sensor data? + + What is the difference between ConSysAPI and SOS-T? + How do I set up OAuth authentication? + What does \"Skip SSL Verification\" do? + Can I connect to multiple servers? + + How do I connect a Bluetooth sensor? + What is the UID Extension? + + What is the Discovery Service? + + What do the status colors mean? + My sensors are not streaming. What should I check? + + Where can I find my device IP address? + What Android version is required? + + + + + OpenSensorHub is a sensor aggregation and streaming app. It collects data from your device\'s built-in sensors and connected Bluetooth peripherals, then streams that data to an OpenSensorHub server in real time. It turns your Android device into an edge computing node for IoT sensor networks. + 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. You can also configure authentication (username/password or OAuth) and enable TLS/SSL if your server requires it. + First, enable the sensors you want in the Sensors tab. Then, make sure you have at least one server profile configured and enabled in the Settings tab. Finally, tap the play button on the Dashboard to start streaming. You can monitor the status of each service from the App Status screen. + + Connected Systems API (ConSysAPI) is the modern OGC standard REST API for publishing sensor data. It is the recommended option for new deployments. SOS-T (Sensor Observation Service \u2013 Transactional) is a legacy XML-based protocol. Use SOS-T only if your server does not support ConSysAPI. + When editing a server profile, enable the OAuth toggle, then fill in the Token Endpoint URL, Client ID, and Client Secret provided by your server administrator. OAuth is used when your server requires token-based authentication instead of basic username/password credentials. + \"Skip SSL Verification\" disables certificate validation when connecting over TLS/SSL. This is useful for development or testing environments that use self-signed certificates. Do not enable this in production, as it makes the connection vulnerable to man-in-the-middle attacks. + Yes. You can create multiple server profiles under Settings > Manage Servers. Each profile can be individually enabled or disabled. When streaming, data is sent to all enabled server profiles simultaneously. + + First, pair the Bluetooth device with your Android device through the system Bluetooth settings. Then, in the Sensors tab, enable the corresponding sensor toggle and select your device from the device picker if prompted. Make sure Bluetooth is turned on and the peripheral is powered and in range. + The UID Extension is an optional identifier appended to each sensor stream\'s unique ID. It helps distinguish data from this specific device when multiple devices are streaming to the same server. You can set it in the Sensors tab under \"Sensors UID Extension\". + + The Discovery Service allows the app to automatically discover other sensors and devices on the network using configurable rule sets. You can provide a URL to a discovery rule file in the Settings tab. This is useful in larger deployments where devices need to find each other automatically. + + On the App Status screen:\n\n\u2022 Green \u2013 The service is started and running normally.\n\u2022 Orange \u2013 The service is initializing or starting up.\n\u2022 Red \u2013 The service is stopped or encountered an error.\n\u2022 Gray \u2013 The status is unknown or the service has not been started. + Check the following:\n\n1. Make sure the sensors you want are enabled in the Sensors tab.\n2. Verify that at least one server profile is configured and enabled in Settings.\n3. Confirm that the required services (ConSysAPI or SOS) are enabled in Settings.\n4. Check the App Status screen for any services showing red or gray.\n5. Ensure all required permissions (Location, Camera, Bluetooth, etc.) have been granted in Android Settings.\n6. Verify that your device has network connectivity and can reach the server. + + Your device IP address is displayed in App Preferences, under the \"Device IP Address\" field. This is the address other devices or services can use to reach your device on the local network. + OpenSensorHub requires Android 14 (API level 34) or higher. + + + diff --git a/sensorhub-android-app/res/xml/locales_config.xml b/sensorhub-android-app/res/xml/locales_config.xml new file mode 100644 index 00000000..c66cc2b0 --- /dev/null +++ b/sensorhub-android-app/res/xml/locales_config.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_app.xml b/sensorhub-android-app/res/xml/pref_app.xml new file mode 100644 index 00000000..a767bbdc --- /dev/null +++ b/sensorhub-android-app/res/xml/pref_app.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index e01f08b7..0aab7631 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -10,10 +10,10 @@ android:selectAllOnFocus="true" android:singleLine="true" android:title="@string/pref_uid_extension" - android:summary="An ID attached to the end of each systems UID" + android:summary="@string/summary_uid_extension" android:layout="@layout/preference_item" /> - + @@ -106,7 +106,7 @@ - + - + diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml index ea08c6ea..4394087b 100644 --- a/sensorhub-android-app/res/xml/pref_settings.xml +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -3,29 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" > - - - - - + - + diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesActivity.java new file mode 100644 index 00000000..aa694c08 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesActivity.java @@ -0,0 +1,108 @@ +package org.sensorhub.android; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.os.IBinder; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.appbar.MaterialToolbar; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.ModuleConfig; +import org.sensorhub.impl.module.ModuleRegistry; + +import java.util.Collection; + + +public class AppPreferencesActivity extends AppCompatActivity { + + SensorHubService boundService; + + private final ServiceConnection sConn = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + boundService = ((SensorHubService.LocalBinder) service).getService(); + } + + public void onServiceDisconnected(ComponentName className) { + boundService = null; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.fragment_app_preferences); + + MaterialToolbar toolbar = findViewById(R.id.app_prefs_toolbar); + setSupportActionBar(toolbar); + toolbar.setNavigationOnClickListener(v -> 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() { + Intent statusIntent = new Intent(this, AppStatusActivity.class); + + if (boundService != null && boundService.sensorhub != null) { + ModuleRegistry moduleRegistry = boundService.sensorhub.getModuleRegistry(); + Collection> 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); + } +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java new file mode 100644 index 00000000..0aebe706 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java @@ -0,0 +1,128 @@ +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-rTW", "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 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/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java index 44875471..62f2ee97 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -170,7 +170,7 @@ private void updateFabIcon() { } private void stopHub() { - Toast.makeText(requireContext(), "Stopping SensorHub", Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), R.string.stopping_sensorhub, Toast.LENGTH_SHORT).show(); stopRefreshingStatus(); provider.stopSensorHub(); updateFabIcon(); @@ -178,7 +178,7 @@ private void stopHub() { clearTextureView(); videoStatusCard.setVisibility(View.GONE); if (meshtasticCard != null) meshtasticCard.setVisibility(View.GONE); - newStatusMessage("SensorHub Stopped"); + newStatusMessage(getString(R.string.sensorhub_stopped)); requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } @@ -194,12 +194,12 @@ private void clearTextureView() { protected synchronized void showRunNamePopup() { MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireContext()); - alert.setTitle("Run Name"); - alert.setMessage("Please enter the name for this run"); + alert.setTitle(R.string.title_run_name); + alert.setMessage(getString(R.string.msg_enter_run_name)); TextInputLayout inputLayout = new TextInputLayout(requireContext()); inputLayout.setBoxBackgroundMode(TextInputLayout.BOX_BACKGROUND_OUTLINE); - inputLayout.setHint("Run Name"); + inputLayout.setHint(getString(R.string.title_run_name)); TextInputEditText input = new TextInputEditText(inputLayout.getContext()); input.getText().append("Run-"); @@ -213,7 +213,7 @@ protected synchronized void showRunNamePopup() { container.addView(inputLayout); alert.setView(container); - alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + alert.setPositiveButton(R.string.btn_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); String runName = input.getText().toString(); @@ -229,10 +229,10 @@ public void onClick(DialogInterface dialog, int whichButton) { if (cameraInUse && improperVideoSettings) { showVideoConfigErrorPopup(); - newStatusMessage("Video Config Error: Check Settings"); + newStatusMessage(getString(R.string.video_config_error)); } else { - Toast.makeText(requireContext(), "Starting SensorHub...", Toast.LENGTH_SHORT).show(); - newStatusMessage("Starting SensorHub..."); + Toast.makeText(requireContext(), R.string.starting_sensorhub, Toast.LENGTH_SHORT).show(); + newStatusMessage(getString(R.string.starting_sensorhub)); provider.getSostClients().clear(); provider.getConSysClients().clear(); provider.startSensorHub(); @@ -242,7 +242,7 @@ public void onClick(DialogInterface dialog, int whichButton) { } }); - alert.setNegativeButton("Cancel", (dialog, whichButton) -> {}); + alert.setNegativeButton(R.string.btn_cancel, (dialog, whichButton) -> {}); alert.show(); } @@ -292,17 +292,16 @@ private void pollHubReady() { } else if (hubPollAttempts < HUB_POLL_MAX_ATTEMPTS) { displayHandler.postDelayed(this::pollHubReady, HUB_POLL_INTERVAL_MS); } else { - newStatusMessage("SensorHub failed to start"); + newStatusMessage(getString(R.string.sensorhub_start_failed)); updateFabIcon(); } } protected void showVideoConfigErrorPopup() { - String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; new MaterialAlertDialogBuilder(requireContext()) - .setTitle("OpenSensorHub") - .setMessage(message) - .setPositiveButton("OK", (dialog, id) -> {}) + .setTitle(R.string.app_name) + .setMessage(R.string.video_config_error_msg) + .setPositiveButton(R.string.btn_ok, (dialog, id) -> {}) .show(); } @@ -443,7 +442,7 @@ protected synchronized void displayStatus() { if (emptyView == null) { TextView tv = new TextView(requireContext()); tv.setTag("empty_status"); - tv.setText("No Sensors Set to Push Remotely"); + tv.setText(R.string.no_sensors_push); tv.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurfaceVariant)); tv.setTextSize(14); tv.setGravity(android.view.Gravity.CENTER); @@ -580,7 +579,7 @@ private void toggleVideoPreview() { videoPreviewVisible = !videoPreviewVisible; if (videoPreviewVisible) { textureView.setVisibility(View.VISIBLE); - btnToggleVideo.setText("Hide"); + btnToggleVideo.setText(R.string.btn_hide); serverStatusContainer.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme())); showVideo(); } else { @@ -591,7 +590,7 @@ private void toggleVideoPreview() { private void hideVideoPreview() { videoPreviewVisible = false; textureView.setVisibility(View.GONE); - if (btnToggleVideo != null) btnToggleVideo.setText("Show"); + if (btnToggleVideo != null) btnToggleVideo.setText(R.string.btn_show); serverStatusContainer.setBackgroundColor(0x00000000); } @@ -627,21 +626,21 @@ private void showMeshtasticDialog() { EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); new MaterialAlertDialogBuilder(requireContext()) - .setTitle("Send Meshtastic Message") + .setTitle(R.string.title_send_meshtastic) .setView(dialogView) - .setPositiveButton("Send", (dialog, id) -> { + .setPositiveButton(R.string.btn_send, (dialog, id) -> { String msg = messageInput.getText().toString(); String destinationId = destinationIdText.getText().toString(); sendMeshtasticMessage(msg, destinationId); }) - .setNegativeButton("Cancel", null) + .setNegativeButton(R.string.btn_cancel, null) .show(); } private void sendMeshtasticMessage(String message, String nodeId) { SensorHubService service = provider.getBoundService(); if (service == null || service.getSensorHub() == null) { - Toast.makeText(requireContext(), "SensorHub not running", Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), R.string.msg_sensorhub_not_running, Toast.LENGTH_SHORT).show(); return; } @@ -667,9 +666,9 @@ private void sendMeshtasticMessage(String message, String nodeId) { .build(); textMessageControl.submitCommand(cmd); - Toast.makeText(requireContext(), "Message sent", Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), R.string.msg_message_sent, Toast.LENGTH_SHORT).show(); } catch (Exception e) { - Toast.makeText(requireContext(), "Failed to send message", Toast.LENGTH_SHORT).show(); + Toast.makeText(requireContext(), R.string.msg_message_failed, Toast.LENGTH_SHORT).show(); } } @Override diff --git a/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java b/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java new file mode 100644 index 00000000..05828eb2 --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/HelpFaqActivity.java @@ -0,0 +1,142 @@ +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 2d521090..d3948d34 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -15,16 +15,13 @@ package org.sensorhub.android; import android.Manifest; -import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; -import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.location.LocationManager; import android.location.LocationProvider; @@ -32,45 +29,33 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; +import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Settings.Secure; import android.util.Log; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; +import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.WindowManager; -import android.widget.EditText; import android.widget.TextView; -import android.os.PowerManager; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; -//import com.botts.impl.sensor.garmin.GarminConfig; -import com.botts.impl.sensor.garmin.GarminConfig; import com.botts.impl.service.discovery.DiscoveryService; import com.botts.impl.service.discovery.DiscoveryServiceConfig; import com.google.android.material.appbar.MaterialToolbar; import com.google.android.material.bottomnavigation.BottomNavigationView; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import net.opengis.swe.v20.DataBlock; import org.sensorhub.android.comm.BluetoothCommProvider; import org.sensorhub.android.comm.BluetoothCommProviderConfig; import org.sensorhub.android.comm.ble.BleConfig; import org.sensorhub.android.comm.ble.BleNetwork; -import org.sensorhub.api.command.CommandData; -import org.sensorhub.api.command.IStreamingControlInterface; -import org.sensorhub.api.common.BigId; -import org.sensorhub.api.module.IModule; +import org.sensorhub.android.server.ServerProfile; +import org.sensorhub.android.server.ServerProfileRepository; import org.sensorhub.api.module.IModuleConfigRepository; -import org.sensorhub.api.module.ModuleConfig; import org.sensorhub.api.sensor.SensorConfig; import org.sensorhub.impl.client.sost.SOSTClient; import org.sensorhub.impl.client.sost.SOSTClientConfig; -import org.sensorhub.impl.module.ModuleRegistry; import org.sensorhub.impl.datastore.h2.MVObsSystemDatabaseConfig; import org.sensorhub.impl.datastore.view.ObsSystemDatabaseViewConfig; import org.sensorhub.impl.module.InMemoryConfigDb; @@ -82,9 +67,6 @@ import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig.VideoPreset; import org.sensorhub.impl.sensor.controller.ControllerConfig; import org.sensorhub.impl.sensor.controller.ControllerDriver; - -import android.view.KeyEvent; -import android.view.MotionEvent; import org.sensorhub.impl.sensor.kestrel.KestrelConfig; import org.sensorhub.impl.sensor.meshtastic.MeshtasticConfig; import org.sensorhub.impl.sensor.polar.PolarConfig; @@ -107,7 +89,6 @@ import java.io.File; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.HttpURLConnection; @@ -115,7 +96,6 @@ import java.security.cert.X509Certificate; import java.time.Instant; import java.util.ArrayList; -import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -144,7 +124,6 @@ public class MainActivity extends AppCompatActivity implements SensorHubServiceP ArrayList sostClients = new ArrayList<>(); ArrayList conSysClients = new ArrayList<>(); - URL url; AndroidSensorsDriver androidSensors; boolean showVideo; @@ -169,8 +148,7 @@ enum Sensors { Kestrel, Wardriving, Controller, - Template, - Garmin + Template } private final ServiceConnection sConn = new ServiceConnection() @@ -257,18 +235,18 @@ public void updateConfig(SharedPreferences prefs, String runName) if (disableSslCheck) { TrustManager[] trustAllCerts = new TrustManager[]{ - new X509TrustManager() { - public java.security.cert.X509Certificate[] getAcceptedIssuers() { - X509Certificate[] myTrustedAnchors = new X509Certificate[0]; - return myTrustedAnchors; - } - public void checkClientTrusted( - java.security.cert.X509Certificate[] certs, String authType) { - } - public void checkServerTrusted( - java.security.cert.X509Certificate[] certs, String authType) { + new X509TrustManager() { + public java.security.cert.X509Certificate[] getAcceptedIssuers() { + X509Certificate[] myTrustedAnchors = new X509Certificate[0]; + return myTrustedAnchors; + } + public void checkClientTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } + public void checkServerTrusted( + java.security.cert.X509Certificate[] certs, String authType) { + } } - } }; try { @@ -394,7 +372,7 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(sensorsConfig); - if (isPushingAnySensor()) { + if (isPushingSensor(Sensors.Android)) { for (ServerProfile sp : enabledServers) { URL profileUrl = sp.buildClientUrl(); if (profileUrl == null) { @@ -564,19 +542,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorhubConfig.add(templateConfig); } - // Garmin - enabled = prefs.getBoolean("garmin_enabled", false); - if (enabled) { - GarminConfig garminConfig = new GarminConfig(); - garminConfig.id = "GARMIN"; - garminConfig.name = "Garmin [" + deviceName + "]"; - garminConfig.autoStart = true; - garminConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; - garminConfig.sdkLicenseKey = BuildConfig.GARMIN_SDK_KEY; - garminConfig.deviceAddress = prefs.getString("garmin_device_address", ""); - sensorhubConfig.add(garminConfig); - } - //---------- SERVICES --------------------- if (isApiServiceEnabled) { sensorhubConfig.add(conSysApiService); @@ -634,6 +599,8 @@ protected void addCSApiConfig(SensorConfig sensorConf, ServerProfile profile, UR @Override protected void onCreate(Bundle savedInstanceState) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @@ -641,23 +608,43 @@ 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); } - Fragment homeFragment = new DashboardFragment(); - Fragment sensorsFragment = new SensorsFragment(); - Fragment settingsFragment = new SettingsFragment(); + Fragment homeFragment; + Fragment sensorsFragment; + Fragment settingsFragment; - getSupportFragmentManager().beginTransaction() - .add(R.id.flFragment, homeFragment, "dashboard") - .add(R.id.flFragment, sensorsFragment, "sensors") - .add(R.id.flFragment, settingsFragment, "settings") - .hide(sensorsFragment) - .hide(settingsFragment) - .commit(); + if (savedInstanceState == null) { + homeFragment = new DashboardFragment(); + sensorsFragment = new SensorsFragment(); + settingsFragment = new SettingsFragment(); - activeFragment = homeFragment; + getSupportFragmentManager().beginTransaction() + .add(R.id.flFragment, homeFragment, "dashboard") + .add(R.id.flFragment, sensorsFragment, "sensors") + .add(R.id.flFragment, settingsFragment, "settings") + .hide(sensorsFragment) + .hide(settingsFragment) + .commit(); + + activeFragment = homeFragment; + } else { + homeFragment = getSupportFragmentManager().findFragmentByTag("dashboard"); + sensorsFragment = getSupportFragmentManager().findFragmentByTag("sensors"); + settingsFragment = getSupportFragmentManager().findFragmentByTag("settings"); + + if (settingsFragment != null && !settingsFragment.isHidden()) + activeFragment = settingsFragment; + else if (sensorsFragment != null && !sensorsFragment.isHidden()) + activeFragment = sensorsFragment; + else + activeFragment = homeFragment; + } BottomNavigationView bottomNav = findViewById(R.id.bottom_nav); @@ -676,7 +663,9 @@ protected void onCreate(Bundle savedInstanceState) return true; }); - bottomNav.setSelectedItemId(R.id.dashboard); + if (savedInstanceState == null) { + bottomNav.setSelectedItemId(R.id.dashboard); + } hasBluetoothPermissions(); checkForPermissions(); @@ -701,84 +690,8 @@ private void switchFragment(Fragment fragment, String title) { if (toolbarTitle != null) { toolbarTitle.setText(title); } - invalidateOptionsMenu(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) - { - getMenuInflater().inflate(R.menu.main, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) - { - boolean onDashboard = activeFragment instanceof DashboardFragment; - for (int i = 0; i < menu.size(); i++) { - menu.getItem(i).setVisible(onDashboard); - } - return super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) - { - int id = item.getItemId(); - if (id == R.id.action_about) - { - showAboutPopup(); - return true; - } - else if(id == R.id.action_status) { - Intent statusIntent = new Intent(this, AppStatusActivity.class); - - if (boundService != null && boundService.sensorhub != null) { - ModuleRegistry moduleRegistry = boundService.sensorhub.getModuleRegistry(); - Collection> 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); - return true; - } - return super.onOptionsItemSelected(item); - } @Override protected void onDestroy() @@ -821,25 +734,7 @@ public boolean dispatchGenericMotionEvent(MotionEvent event) { return super.dispatchGenericMotionEvent(event); } - protected void showAboutPopup() { - String version = "?"; - - try { - PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); - version = pInfo.versionName; - } catch (PackageManager.NameNotFoundException e) { - log.warn("Could not retrieve package version", e); - } - - String message = "A software platform for building smart sensor networks and the Internet of Things\n\n"; - message += "Version: " + version + "\n"; - AlertDialog.Builder alert = new AlertDialog.Builder(this); - alert.setTitle("OpenSensorHub"); - alert.setMessage(message); - alert.setIcon(R.drawable.ic_launcher); - alert.show(); - } boolean isPushingSensor(Sensors sensor) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); @@ -879,17 +774,6 @@ boolean isPushingSensor(Sensors sensor) { return prefs.getBoolean("controller_enabled", false); } else if (Sensors.Template.equals(sensor)) { return prefs.getBoolean("template_enabled", false); - } else if (Sensors.Garmin.equals(sensor)) { - return prefs.getBoolean("garmin_enabled", false); - } - - return false; - } - - boolean isPushingAnySensor() { - for (Sensors sensor : Sensors.values()) { - if (isPushingSensor(sensor)) - return true; } return false; } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java index 839203ed..2f6ca79e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java @@ -137,13 +137,13 @@ private void showDevicePickerDialog(String prefKey) { } } - names.add("Enter name or address manually..."); + names.add(getString(R.string.manual_entry_option)); addresses.add(null); String[] displayNames = names.toArray(new String[0]); new AlertDialog.Builder(requireContext()) - .setTitle("Select Device") + .setTitle(R.string.title_select_device) .setItems(displayNames, (dialog, which) -> { if (addresses.get(which) == null) { showManualAddressDialog(prefKey); @@ -151,14 +151,14 @@ private void showDevicePickerDialog(String prefKey) { saveDeviceAddress(prefKey, addresses.get(which), displayNames[which]); } }) - .setNegativeButton("Cancel", null) + .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("e.g. Ballistic or AA:BB:CC:DD:EE:FF"); + input.setHint(R.string.hint_manual_device); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); String current = prefs.getString(prefKey, ""); @@ -173,16 +173,16 @@ private void showManualAddressDialog(String prefKey) { container.addView(input); new AlertDialog.Builder(requireContext()) - .setTitle("Enter Device Name or Address") - .setMessage("Enter a device name (e.g. \"Enviro\", \"TP\") or MAC address. Names are matched from the start, case-insensitive.") + .setTitle(R.string.title_enter_device) + .setMessage(R.string.msg_enter_device) .setView(container) - .setPositiveButton("OK", (dialog, which) -> { + .setPositiveButton(R.string.btn_ok, (dialog, which) -> { String address = input.getText().toString().trim(); if (!address.isEmpty()) { saveDeviceAddress(prefKey, address, address); } }) - .setNegativeButton("Cancel", null) + .setNegativeButton(R.string.btn_cancel, null) .show(); } diff --git a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java index bc8a1ea7..ab1d1573 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java @@ -1,19 +1,14 @@ package org.sensorhub.android; -import static android.content.Context.WIFI_SERVICE; - import android.content.Intent; -import android.net.wifi.WifiManager; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.SwitchPreferenceCompat; -import java.math.BigInteger; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.nio.ByteOrder; +import org.sensorhub.android.server.ServerProfileRepository; +import org.sensorhub.android.server.ServerProfilesActivity; /* @@ -25,26 +20,6 @@ public class SettingsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.pref_settings, rootKey); - - 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 = "Unable to get IP Address"; - } - - Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress"); - ipAddressLabel.setSummary(ipAddressString); - manageServerProfiles(); setupDiscoveryToggle(); } diff --git a/sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java b/sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java similarity index 91% rename from sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java rename to sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java index 2856b676..73588ba9 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/EditServerProfileActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/server/EditServerProfileActivity.java @@ -1,4 +1,4 @@ -package org.sensorhub.android; +package org.sensorhub.android.server; import android.os.Bundle; import android.view.View; @@ -12,6 +12,8 @@ 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"; @@ -39,7 +41,7 @@ protected void onCreate(Bundle savedInstanceState) { profile = isEdit ? repo.getById(profileId) : new ServerProfile(); if (isEdit && profile == null) { - Toast.makeText(this, "Profile not found", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_profile_not_found, Toast.LENGTH_SHORT).show(); finish(); return; } @@ -56,7 +58,7 @@ protected void onCreate(Bundle savedInstanceState) { private void setupToolbar() { MaterialToolbar toolbar = findViewById(R.id.edit_profile_toolbar); - toolbar.setTitle(isEdit ? "Edit Server" : "Add Server"); + toolbar.setTitle(isEdit ? getString(R.string.title_edit_server) : getString(R.string.btn_add_server)); toolbar.setNavigationOnClickListener(v -> finish()); } @@ -136,12 +138,12 @@ private void saveProfile() { String endpoint = endpointInput.getText().toString().trim(); if (name.isEmpty() || host.isEmpty() || portStr.isEmpty()) { - Toast.makeText(this, "Name, host, and port are required", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_name_host_port_required, Toast.LENGTH_SHORT).show(); return; } if (host.contains(" ") || host.contains("://")) { - Toast.makeText(this, "Host should not include a protocol (e.g. http://)", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_no_protocol, Toast.LENGTH_SHORT).show(); return; } @@ -149,11 +151,11 @@ private void saveProfile() { try { port = Integer.parseInt(portStr); } catch (NumberFormatException e) { - Toast.makeText(this, "Port should be a number", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_port_number, Toast.LENGTH_SHORT).show(); return; } if (port < 1 || port > 65535) { - Toast.makeText(this, "Port should be between 1 and 65535", Toast.LENGTH_SHORT).show(); + Toast.makeText(this, R.string.msg_port_range, Toast.LENGTH_SHORT).show(); return; } diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java similarity index 97% rename from sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java rename to sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java index fd070e7b..d58772b0 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerAdapter.java +++ b/sensorhub-android-app/src/org/sensorhub/android/server/ServerAdapter.java @@ -1,4 +1,4 @@ -package org.sensorhub.android; +package org.sensorhub.android.server; import android.view.LayoutInflater; import android.view.View; @@ -11,6 +11,8 @@ import com.google.android.material.materialswitch.MaterialSwitch; +import org.sensorhub.android.R; + import java.util.List; public class ServerAdapter extends RecyclerView.Adapter { diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java similarity index 98% rename from sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java rename to sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java index a0fab6f3..43576b9e 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerProfile.java +++ b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfile.java @@ -1,4 +1,4 @@ -package org.sensorhub.android; +package org.sensorhub.android.server; import org.json.JSONException; import org.json.JSONObject; diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java similarity index 98% rename from sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java rename to sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java index a031a373..9453db9f 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerProfileRepository.java +++ b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfileRepository.java @@ -1,4 +1,4 @@ -package org.sensorhub.android; +package org.sensorhub.android.server; import android.content.Context; import android.content.SharedPreferences; @@ -7,6 +7,7 @@ import org.json.JSONArray; import org.json.JSONException; +import org.sensorhub.android.SecurePrefs; import java.util.ArrayList; import java.util.List; diff --git a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java similarity index 82% rename from sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java rename to sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java index 0aedd34a..9fba3146 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/server/ServerProfilesActivity.java @@ -1,4 +1,4 @@ -package org.sensorhub.android; +package org.sensorhub.android.server; import android.content.Intent; import android.os.Bundle; @@ -8,13 +8,14 @@ import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.appcompat.app.AppCompatActivity; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; 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; @@ -46,8 +47,8 @@ protected void onCreate(Bundle savedInstanceState) { recyclerView = findViewById(R.id.server_list); emptyText = findViewById(R.id.empty_text); - MaterialButton addButton = findViewById(R.id.btn_add_server); - addButton.setOnClickListener(v -> { + FloatingActionButton fab = findViewById(R.id.fab_add_server); + fab.setOnClickListener(v -> { Intent intent = new Intent(this, EditServerProfileActivity.class); editLauncher.launch(intent); }); @@ -74,13 +75,13 @@ public void onEnabledToggled(ServerProfile profile, boolean enabled) { @Override public void onDeleteRequested(ServerProfile profile) { new MaterialAlertDialogBuilder(this) - .setTitle("Delete Server") - .setMessage("Remove \"" + profile.name + "\"?") - .setPositiveButton("Delete", (d, w) -> { + .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("Cancel", null) + .setNegativeButton(R.string.btn_cancel, null) .show(); } @@ -89,6 +90,6 @@ private void refreshList() { servers.addAll(repo.getAll()); adapter.notifyDataSetChanged(); emptyText.setVisibility(servers.isEmpty() ? View.VISIBLE : View.GONE); - emptyText.setText(servers.isEmpty() ? "No server profiles configured.\nTap Add to create one." : ""); + emptyText.setText(servers.isEmpty() ? getString(R.string.empty_server_profiles) : ""); } } From 52c49441ba9884c48aa897483caf3a8cceba203d Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Fri, 5 Jun 2026 08:48:36 -0500 Subject: [PATCH 26/26] fixed broken lang --- sensorhub-android-app/res/xml/locales_config.xml | 2 +- .../src/org/sensorhub/android/AppPreferencesFragment.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sensorhub-android-app/res/xml/locales_config.xml b/sensorhub-android-app/res/xml/locales_config.xml index c66cc2b0..88a76c2e 100644 --- a/sensorhub-android-app/res/xml/locales_config.xml +++ b/sensorhub-android-app/res/xml/locales_config.xml @@ -6,5 +6,5 @@ - + diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java index 0aebe706..6c0e483c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppPreferencesFragment.java @@ -25,7 +25,7 @@ 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-rTW", "es", "fr", "de", "it", "pt", }; + 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);