From aad64bf304146881f50e1d4f34ab89b3d0ef59c4 Mon Sep 17 00:00:00 2001 From: kalynstricklin Date: Wed, 1 Apr 2026 20:20:41 -0500 Subject: [PATCH 01/21] 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/21] 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 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 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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