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)
@@ -1253,171 +895,54 @@ 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");
+ } else if (Sensors.Wardriving.equals(sensor)) {
+ return prefs.getBoolean("wardriving_enabled", false)
+ && prefs.getStringSet("wardriving_options", Collections.emptySet()).contains("PUSH_REMOTE");
+ } 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;
}
-
- 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(0x80FFFFFF);
-
- 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(0x80FFFFFF);
}
- }
-
-
- @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;
}
}
@@ -1425,142 +950,110 @@ 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.ACCESS_WIFI_STATE) == PackageManager.PERMISSION_DENIED)
+ permissions.add(Manifest.permission.ACCESS_WIFI_STATE);
+ if (checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED)
+ permissions.add(Manifest.permission.ACCESS_COARSE_LOCATION);
+ if (Build.VERSION.SDK_INT >= 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) {
+ 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;
- 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/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/SecurePrefs.java b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java
new file mode 100644
index 00000000..2b5e98d7
--- /dev/null
+++ b/sensorhub-android-app/src/org/sensorhub/android/SecurePrefs.java
@@ -0,0 +1,123 @@
+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) || 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/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..bc6a932e
--- /dev/null
+++ b/sensorhub-android-app/src/org/sensorhub/android/SensorsFragment.java
@@ -0,0 +1,310 @@
+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_resolution", "camera_select"},
+ {"video_roll_enabled", "video_roll_options"},
+ {"audio_enabled", "audio_options", "audio_codec", "audio_samplerate", "audio_bitrate"},
+ {"meshtastic_enabled", "meshtastic_device_address", "meshtastic_options"},
+ {"polar_enabled", "polar_device_address", "polar_options"},
+ {"kestrel_enabled", "kestrel_device_address", "kestrel_options"},
+ {"trupulse_enabled", "trupulse_datasource", "trupulse_options", "trupulse_device_address", "trupulse_simu"},
+ {"angel_enabled", "angel_address", "angel_options"},
+ {"flirone_enabled", "flir_options"},
+ {"ste_radpager_enabled","ste_radpager_options"},
+ {"wardriving_enabled", "wardriving_options"},
+ {"controller_enabled", "controller_options"},
+ {"template_enabled", "template_device_address", "template_options"},
+
+ };
+
+ /** 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",
+ "template_device_address"
+ };
+
+ private ArrayList frameRateList = new ArrayList<>();
+ private ArrayList resList = new ArrayList<>();
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ setPreferencesFromResource(R.xml.pref_sensors, rootKey);
+
+ for (String[] group : SWITCH_DEPENDENTS) {
+ String switchKey = group[0];
+ SwitchPreferenceCompat switchPref = findPreference(switchKey);
+ if (switchPref == null) continue;
+
+ boolean isChecked = switchPref.isChecked();
+ for (int i = 1; i < group.length; i++) {
+ Preference dep = findPreference(group[i]);
+ if (dep != null) dep.setVisible(isChecked);
+ }
+
+ switchPref.setOnPreferenceChangeListener((preference, newValue) -> {
+ boolean enabled = (boolean) newValue;
+ for (int i = 1; i < group.length; i++) {
+ Preference dep = findPreference(group[i]);
+ if (dep != null) dep.setVisible(enabled);
+ }
+ return true;
+ });
+ }
+
+ setupVideoPreferences();
+ setupAudioPreferences();
+
+ setupBluetoothDevicePickers();
+ }
+
+ private void setupBluetoothDevicePickers() {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
+
+ for (String key : BT_DEVICE_PREF_KEYS) {
+ Preference pref = findPreference(key);
+ if (pref == null) continue;
+
+ String saved = prefs.getString(key, "");
+ if (!saved.isEmpty()) {
+ pref.setSummary(saved);
+ }
+
+ pref.setOnPreferenceClickListener(p -> {
+ showDevicePickerDialog(key);
+ return true;
+ });
+ }
+ }
+
+ private void showDevicePickerDialog(String prefKey) {
+ List names = new ArrayList<>();
+ List addresses = new ArrayList<>();
+
+ // Gather all bonded Bluetooth devices (classic + BLE)
+ BluetoothAdapter btAdapter = getBluetoothAdapter();
+ if (btAdapter != null && btAdapter.isEnabled() && hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) {
+ Set bondedDevices = btAdapter.getBondedDevices();
+ for (BluetoothDevice device : bondedDevices) {
+ String name = device.getName();
+ String mac = device.getAddress();
+ names.add(name != null ? name + " (" + mac + ")" : mac);
+ addresses.add(mac);
+ }
+ }
+
+ names.add("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) {
+ 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");
+
+ 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);
+ }
+ }
+
+
+ private void setupVideoPreferences() {
+ // Camera selection
+ ArrayList cameras = new ArrayList<>();
+ try {
+ for (int i = 0; i < Camera.getNumberOfCameras(); i++) {
+ cameras.add(Integer.toString(i));
+ }
+ } catch (Exception e) {
+ cameras.add("0");
+ }
+
+ ListPreference cameraSelectList = findPreference("camera_select");
+ if (cameraSelectList != null) {
+ cameraSelectList.setEntries(cameras.toArray(new String[0]));
+ cameraSelectList.setEntryValues(cameras.toArray(new String[0]));
+ cameraSelectList.setOnPreferenceChangeListener((preference, newValue) -> {
+ Log.d("CAMERA_SELECT", "New Camera Selected: " + newValue);
+ updateCameraSettings(Integer.parseInt((String) newValue));
+ return true;
+ });
+ }
+
+ // Frame rates and resolutions from camera
+ Camera camera = null;
+ try {
+ camera = Camera.open(0);
+ Camera.Parameters camParams = camera.getParameters();
+ for (int frameRate : camParams.getSupportedPreviewFrameRates())
+ frameRateList.add(Integer.toString(frameRate));
+ for (Camera.Size imgSize : camParams.getSupportedPreviewSizes())
+ resList.add(imgSize.width + "x" + imgSize.height);
+ } catch (Exception e) {
+ frameRateList.add("30");
+ resList.add("640x480");
+ } finally {
+ if (camera != null) camera.release();
+ }
+
+ ListPreference frameRatePrefList = findPreference("video_framerate");
+ if (frameRatePrefList != null) {
+ frameRatePrefList.setEntries(frameRateList.toArray(new String[0]));
+ frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0]));
+ }
+
+ // Resolution list
+ ListPreference resolutionPrefList = findPreference("video_resolution");
+ if (resolutionPrefList != null) {
+ resolutionPrefList.setEntries(resList.toArray(new String[0]));
+ resolutionPrefList.setEntryValues(resList.toArray(new String[0]));
+ if (!resList.isEmpty() && resolutionPrefList.getValue() == null)
+ resolutionPrefList.setValue(resList.get(0));
+ }
+ }
+
+ private void updateCameraSettings(int cameraId) {
+ Camera camera = null;
+ try {
+ frameRateList.clear();
+ resList.clear();
+ camera = Camera.open(cameraId);
+ Camera.Parameters camParams = camera.getParameters();
+ for (int frameRate : camParams.getSupportedPreviewFrameRates())
+ frameRateList.add(Integer.toString(frameRate));
+ for (Camera.Size imgSize : camParams.getSupportedPreviewSizes())
+ resList.add(imgSize.width + "x" + imgSize.height);
+
+ ListPreference frameRatePrefList = findPreference("video_framerate");
+ if (frameRatePrefList != null) {
+ frameRatePrefList.setEntries(frameRateList.toArray(new String[0]));
+ frameRatePrefList.setEntryValues(frameRateList.toArray(new String[0]));
+ }
+ ListPreference resolutionPrefList = findPreference("video_resolution");
+ if (resolutionPrefList != null) {
+ resolutionPrefList.setEntries(resList.toArray(new String[0]));
+ resolutionPrefList.setEntryValues(resList.toArray(new String[0]));
+ }
+ } catch (Exception e) {
+ Log.e("SensorsFragment", "Error updating camera settings", e);
+ } finally {
+ if (camera != null) camera.release();
+ }
+ }
+
+
+ private void setupAudioPreferences() {
+ List sampleRateList = Arrays.asList("8000", "11025", "22050", "44100", "48000");
+ List bitRateList = Arrays.asList("32", "64", "96", "128", "160", "192");
+
+ ListPreference sampleRatePrefList = findPreference("audio_samplerate");
+ if (sampleRatePrefList != null) {
+ sampleRatePrefList.setEntries(sampleRateList.toArray(new String[0]));
+ sampleRatePrefList.setEntryValues(sampleRateList.toArray(new String[0]));
+ }
+
+ ListPreference bitRatePrefList = findPreference("audio_bitrate");
+ if (bitRatePrefList != null) {
+ bitRatePrefList.setEntries(bitRateList.toArray(new String[0]));
+ bitRatePrefList.setEntryValues(bitRateList.toArray(new String[0]));
+ }
+ }
+
+ private BluetoothAdapter getBluetoothAdapter() {
+ BluetoothManager btManager = (BluetoothManager) requireContext().getSystemService(Context.BLUETOOTH_SERVICE);
+ return btManager != null ? btManager.getAdapter() : null;
+ }
+
+ private boolean hasPermission(String permission) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return true;
+ return ActivityCompat.checkSelfPermission(requireContext(), permission) == PackageManager.PERMISSION_GRANTED;
+ }
+}
diff --git a/sensorhub-android-app/src/org/sensorhub/android/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..afc84137
--- /dev/null
+++ b/sensorhub-android-app/src/org/sensorhub/android/ServerProfilesActivity.java
@@ -0,0 +1,206 @@
+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.AlertDialog;
+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();
+
+ AlertDialog dialog = new MaterialAlertDialogBuilder(this)
+ .setTitle(isEdit ? "Edit Server" : "Add Server")
+ .setView(dialogView)
+ .setPositiveButton("Save", null)
+ .setNegativeButton("Cancel", null)
+ .create();
+
+ dialog.show();
+
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(v -> {
+ String name = nameInput.getText().toString().trim();
+ String host = hostInput.getText().toString().trim();
+ String portStr = portInput.getText().toString().trim();
+ String endpoint = endpointInput.getText().toString().trim();
+
+ if (name.isEmpty() || host.isEmpty() || portStr.isEmpty()) {
+ Toast.makeText(this, "Name, host, and port are required", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (host.contains(" ") || host.contains("://")) {
+ Toast.makeText(this, "Host should not include a protocol (e.g. http://)", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ int port;
+ try {
+ port = Integer.parseInt(portStr);
+ } catch (NumberFormatException e) {
+ Toast.makeText(this, "Port should be a number", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (port < 1 || port > 65535) {
+ Toast.makeText(this, "Port should be between 1 and 65535", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ if (!endpoint.isEmpty() && !endpoint.startsWith("/")) {
+ endpoint = "/" + endpoint;
+ }
+
+
+ ServerProfile profile = isEdit ? existing : new ServerProfile();
+ profile.name = name;
+ profile.host = host;
+ profile.port = port;
+ profile.endpointPath = endpoint;
+ profile.username = usernameInput.getText().toString().trim();
+ profile.enableTls = tlsSwitch.isChecked();
+ profile.disableSslCheck = sslSwitch.isChecked();
+ profile.useConSysClient = clientModeSwitch.isChecked();
+ profile.oAuthEnabled = oauthSwitch.isChecked();
+
+ repo.save(profile);
+
+ repo.setPassword(profile.id, passwordInput.getText().toString().trim());
+ repo.setOAuthClientId(profile.id, clientIdInput.getText().toString().trim());
+ repo.setOAuthClientSecret(profile.id, clientSecretInput.getText().toString().trim());
+ repo.setOAuthTokenEndpoint(profile.id, tokenInput.getText().toString().trim());
+
+ refreshList();
+ dialog.dismiss();
+ });
+ }
+
+ private void refreshList() {
+ servers.clear();
+ servers.addAll(repo.getAll());
+ adapter.notifyDataSetChanged();
+ emptyText.setVisibility(servers.isEmpty() ? View.VISIBLE : View.GONE);
+ }
+}
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..bc8a1ea7
--- /dev/null
+++ b/sensorhub-android-app/src/org/sensorhub/android/SettingsFragment.java
@@ -0,0 +1,105 @@
+package org.sensorhub.android;
+
+import static android.content.Context.WIFI_SERVICE;
+
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Bundle;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.SwitchPreferenceCompat;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteOrder;
+
+
+/*
+ * Fragment for settings preferences
+ */
+public class SettingsFragment extends PreferenceFragmentCompat {
+
+ @Override
+ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
+ setPreferencesFromResource(R.xml.pref_settings, rootKey);
+
+
+ WifiManager wifiManager = (WifiManager) getActivity().getApplicationContext().getSystemService(WIFI_SERVICE);
+ int ipAddress = wifiManager.getConnectionInfo().getIpAddress();
+
+ if (ByteOrder.nativeOrder().equals(ByteOrder.LITTLE_ENDIAN)) {
+ ipAddress = Integer.reverseBytes(ipAddress);
+ }
+
+ byte[] ipByteArray = BigInteger.valueOf(ipAddress).toByteArray();
+
+ String ipAddressString;
+ try {
+ ipAddressString = InetAddress.getByAddress(ipByteArray).getHostAddress();
+ } catch (UnknownHostException ex) {
+ ipAddressString = "Unable to get IP Address";
+ }
+
+ Preference ipAddressLabel = getPreferenceScreen().findPreference("nop_ipAddress");
+ ipAddressLabel.setSummary(ipAddressString);
+
+ manageServerProfiles();
+ setupDiscoveryToggle();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ Preference serverPref = findPreference("manage_servers");
+ if (serverPref != null) updateServerProfilesSummary(serverPref);
+ }
+
+ private void manageServerProfiles() {
+ Preference serverPref = findPreference("manage_servers");
+
+ if (serverPref != null) {
+ updateServerProfilesSummary(serverPref);
+ serverPref.setOnPreferenceClickListener(p -> {
+ startActivity(new Intent(requireContext(), ServerProfilesActivity.class));
+ return true;
+ });
+ }
+ }
+
+
+ private void updateServerProfilesSummary(Preference pref) {
+ ServerProfileRepository repo = 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(enabled + " of " + total + " server(s) enabled");
+ }
+ }
+
+ private void setupDiscoveryToggle() {
+ SwitchPreferenceCompat enableDiscovery = findPreference("discovery_service");
+
+ Preference rules = findPreference("rules_link");
+
+ if (enableDiscovery != null) {
+ boolean isDiscovery = enableDiscovery.isChecked();
+ setVisibility(isDiscovery, rules);
+
+ enableDiscovery.setOnPreferenceChangeListener((pref, value) -> {
+ boolean isEnabled = (Boolean) value;
+ setVisibility(isEnabled, rules);
+ return true;
+ });
+ }
+ }
+
+ private void setVisibility(boolean visible, Preference... prefs) {
+ for (Preference p : prefs) {
+ if (p != null) p.setVisible(visible);
+ }
+ }
+}
diff --git a/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java b/sensorhub-android-app/src/org/sensorhub/android/UserSettingsActivity.java
deleted file mode 100644
index d9a621d6..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-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-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 extends IModule>> getModuleClass()
+ {
+ return ControllerDriver.class;
+ }
+
+
+ @Override
+ public Class extends ModuleConfig> 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-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-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-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/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java b/sensorhub-android-service/src/org/sensorhub/android/SensorHubService.java
index 5aab62bb..9ec8354f 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,24 +19,31 @@
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;
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;
@@ -58,10 +66,8 @@ public void onCreate() {
try
{
- // keep handle to Android context so it can be retrieved by OSH components
- SensorHubService.context = getApplicationContext();
+ SensorHubService.appContext = getApplicationContext();
- // create video surface texture here so it's not destroyed when pausing the app
SensorHubService.videoTex = new SurfaceTexture(1);
SensorHubService.videoTex.detachFromGLContext();
@@ -69,8 +75,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();
@@ -82,12 +88,11 @@ public void onCreate() {
msgThread.start();
msgHandler = new Handler(msgThread.getLooper());
- // Start as foreground service with notification
startForegroundService();
}
catch (Exception e)
{
- e.printStackTrace();
+ log.error("Error: " + e.getMessage());
}
}
@@ -169,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) {
- e.printStackTrace();
- // 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();
}
}
@@ -203,7 +220,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();
@@ -226,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();
}
@@ -272,10 +270,24 @@ public void onDestroy()
SensorHubService.videoTex.release();
SensorHubService.videoTex = null;
}
- SensorHubService.context = null;
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)
@@ -304,6 +316,6 @@ public static SurfaceTexture getVideoTexture()
public static Context getContext()
{
- return context;
+ return appContext;
}
}
\ No newline at end of file
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) {
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 extends IModule>> getModuleClass()
+ {
+ return Sensor.class;
+ }
+
+
+ @Override
+ public Class extends ModuleConfig> 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
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 extends IModule>> getModuleClass()
+ {
+ return Wardriving.class;
+ }
+
+
+ @Override
+ public Class extends ModuleConfig> 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
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..7271af1a
--- /dev/null
+++ b/submodules/botts-addons
@@ -0,0 +1 @@
+Subproject commit 7271af1a739256a4170e5281f62efea0a57b41dc
diff --git a/submodules/osh-addons b/submodules/osh-addons
index dfcd8e3f..029d3235 160000
--- a/submodules/osh-addons
+++ b/submodules/osh-addons
@@ -1 +1 @@
-Subproject commit dfcd8e3fcf63acfa421ca292b0315a64bca60735
+Subproject commit 029d3235e45bf3fe91b2ef619ab53fec7f7fc03e
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