diff --git a/.gitmodules b/.gitmodules index 189cbdf3..437f68a0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,3 +6,6 @@ path = submodules/osh-core url = git@github.com:kalynstricklin/osh-core.git branch = update-moduleutils +[submodule "submodules/botts-addons"] + path = submodules/botts-addons + url = git@github.com:Botts-Innovative-Research/botts-addons.git diff --git a/build.gradle b/build.gradle index f632da5f..0e81521b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,9 @@ ext.oshCoreVersion = '2.0.0-beta' ext.compileSdkVersion = 33 -ext.minSdkVersion = 34 +ext.minSdkVersion = 33 ext.targetSdkVersion = 30 -ext.buildToolsVersion = "30.0.2" -version = '3.1.2' +ext.buildToolsVersion = "34.0.0" +version = '4.0.1' buildscript { repositories { diff --git a/sensorhub-android-app/AndroidManifest.xml b/sensorhub-android-app/AndroidManifest.xml index c9f4be8b..0f4432b6 100644 --- a/sensorhub-android-app/AndroidManifest.xml +++ b/sensorhub-android-app/AndroidManifest.xml @@ -32,6 +32,7 @@ + - + android:configChanges="orientation|screenSize" + android:screenOrientation="portrait" + android:exported="false" /> + diff --git a/sensorhub-android-app/build.gradle b/sensorhub-android-app/build.gradle index 9591faf7..f4899e8f 100644 --- a/sensorhub-android-app/build.gradle +++ b/sensorhub-android-app/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.application' description = 'OSH Android App' -ext.details = 'OSH app for Android' +ext.details = 'OSH app for Android' repositories { // maven { @@ -14,21 +14,27 @@ dependencies { implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1' - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support:design:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:2.0.4' - implementation 'android.arch.navigation:navigation-fragment:1.0.0' - implementation 'android.arch.navigation:navigation-ui:1.0.0' - implementation 'com.android.support:support-v4:28.0.0' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.navigation:navigation-fragment:2.5.3' + implementation 'androidx.navigation:navigation-ui:2.5.3' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation 'androidx.preference:preference:1.2.0' implementation project(path: ':sensorhub-datastore-h2') implementation project(path: ':sensorhub-service-consys') + implementation project(path: ':sensorhub-service-discovery') implementation project(':sensorhub-android-ste') implementation project(':sensorhub-android-meshtastic') implementation project(':sensorhub-android-polar') + implementation project(':sensorhub-android-wardriving') + implementation project(':sensorhub-android-controller') + implementation project(':sensorhub-android-template') implementation project(':sensorhub-driver-android') implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'com.github.tony19:logback-android:3.0.0' + } allprojects { @@ -55,8 +61,24 @@ android { targetSdkVersion rootProject.targetSdkVersion versionCode 1 versionName rootProject.version + applicationId "com.georobotix.android" } + //https://developer.android.com/build/build-variants#groovy +// flavorDimensions += "version" // maybe dont need +// productFlavors { +// create("free") { +// dimension = "version" +// applicationIdSuffix = ".free" +// buildConfigField("boolean", "IS_PREMIUM", "false") +// } +// create("premium") { +// dimension = "version" +// applicationIdSuffix = ".premium" +// buildConfigField("boolean", "IS_PREMIUM", "true") +// } +// } + compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 @@ -90,5 +112,6 @@ android { excludes += ["META-INF/INDEX.LIST"] } } + } diff --git a/sensorhub-android-app/res/color/bottom_nav_selector.xml b/sensorhub-android-app/res/color/bottom_nav_selector.xml new file mode 100644 index 00000000..68a6dc27 --- /dev/null +++ b/sensorhub-android-app/res/color/bottom_nav_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/color/switch_thumb_selector.xml b/sensorhub-android-app/res/color/switch_thumb_selector.xml new file mode 100644 index 00000000..d733f2a3 --- /dev/null +++ b/sensorhub-android-app/res/color/switch_thumb_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/color/switch_track_selector.xml b/sensorhub-android-app/res/color/switch_track_selector.xml new file mode 100644 index 00000000..ef973a9a --- /dev/null +++ b/sensorhub-android-app/res/color/switch_track_selector.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml b/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml new file mode 100644 index 00000000..6238d358 --- /dev/null +++ b/sensorhub-android-app/res/drawable-anydpi/ic_edit.xml @@ -0,0 +1,11 @@ + + + diff --git a/sensorhub-android-app/res/drawable-hdpi/ic_edit.png b/sensorhub-android-app/res/drawable-hdpi/ic_edit.png new file mode 100644 index 00000000..12583f04 Binary files /dev/null and b/sensorhub-android-app/res/drawable-hdpi/ic_edit.png differ diff --git a/sensorhub-android-app/res/drawable-mdpi/ic_edit.png b/sensorhub-android-app/res/drawable-mdpi/ic_edit.png new file mode 100644 index 00000000..33be3b87 Binary files /dev/null and b/sensorhub-android-app/res/drawable-mdpi/ic_edit.png differ diff --git a/sensorhub-android-app/res/drawable-xhdpi/ic_edit.png b/sensorhub-android-app/res/drawable-xhdpi/ic_edit.png new file mode 100644 index 00000000..a9ea9313 Binary files /dev/null and b/sensorhub-android-app/res/drawable-xhdpi/ic_edit.png differ diff --git a/sensorhub-android-app/res/drawable-xxhdpi/ic_edit.png b/sensorhub-android-app/res/drawable-xxhdpi/ic_edit.png new file mode 100644 index 00000000..bb79e513 Binary files /dev/null and b/sensorhub-android-app/res/drawable-xxhdpi/ic_edit.png differ diff --git a/sensorhub-android-app/res/drawable/bg_status_chip.xml b/sensorhub-android-app/res/drawable/bg_status_chip.xml new file mode 100644 index 00000000..f5881c76 --- /dev/null +++ b/sensorhub-android-app/res/drawable/bg_status_chip.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/ic_add.xml b/sensorhub-android-app/res/drawable/ic_add.xml new file mode 100644 index 00000000..9f83b8fb --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_add.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/sensorhub-android-app/res/drawable/ic_arrow_back.xml b/sensorhub-android-app/res/drawable/ic_arrow_back.xml new file mode 100644 index 00000000..14401611 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_arrow_back.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_edit.xml b/sensorhub-android-app/res/drawable/ic_edit.xml new file mode 100644 index 00000000..6238d358 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_edit.xml @@ -0,0 +1,11 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_expand_less.xml b/sensorhub-android-app/res/drawable/ic_expand_less.xml new file mode 100644 index 00000000..1e92d2b9 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_expand_less.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_expand_more.xml b/sensorhub-android-app/res/drawable/ic_expand_more.xml new file mode 100644 index 00000000..cf9708d0 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_expand_more.xml @@ -0,0 +1,9 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_home.xml b/sensorhub-android-app/res/drawable/ic_home.xml new file mode 100644 index 00000000..ebccd6ca --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_home.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/drawable/ic_info.xml b/sensorhub-android-app/res/drawable/ic_info.xml new file mode 100644 index 00000000..0355cbed --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_info.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_message.xml b/sensorhub-android-app/res/drawable/ic_message.xml new file mode 100644 index 00000000..baad9323 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_message.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_play.xml b/sensorhub-android-app/res/drawable/ic_play.xml new file mode 100644 index 00000000..ce91a8b0 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_play.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_sensors.xml b/sensorhub-android-app/res/drawable/ic_sensors.xml new file mode 100644 index 00000000..1c73501b --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_sensors.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_settings.xml b/sensorhub-android-app/res/drawable/ic_settings.xml new file mode 100644 index 00000000..7926ba39 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_settings.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/ic_stop.xml b/sensorhub-android-app/res/drawable/ic_stop.xml new file mode 100644 index 00000000..41823516 --- /dev/null +++ b/sensorhub-android-app/res/drawable/ic_stop.xml @@ -0,0 +1,10 @@ + + + diff --git a/sensorhub-android-app/res/drawable/logo.png b/sensorhub-android-app/res/drawable/logo.png new file mode 100644 index 00000000..40a6fd9f Binary files /dev/null and b/sensorhub-android-app/res/drawable/logo.png differ diff --git a/sensorhub-android-app/res/drawable/status_dot_background.xml b/sensorhub-android-app/res/drawable/status_dot_background.xml new file mode 100644 index 00000000..cd4498be --- /dev/null +++ b/sensorhub-android-app/res/drawable/status_dot_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/sensorhub-android-app/res/layout/activity_app_status.xml b/sensorhub-android-app/res/layout/activity_app_status.xml index d8d919c9..69a569ce 100644 --- a/sensorhub-android-app/res/layout/activity_app_status.xml +++ b/sensorhub-android-app/res/layout/activity_app_status.xml @@ -1,134 +1,341 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + android:fitsSystemWindows="true"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/activity_main.xml b/sensorhub-android-app/res/layout/activity_main.xml index 16b74ed8..4193be95 100644 --- a/sensorhub-android-app/res/layout/activity_main.xml +++ b/sensorhub-android-app/res/layout/activity_main.xml @@ -1,34 +1,75 @@ - + - - + android:background="@color/md_theme_background" + tools:context=".MainActivity"> - + android:background="@color/toolbar_bg" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + > + + + + + + + + + - + + + - + android:layout_height="0dp" + app:layout_constraintTop_toBottomOf="@id/appBarLayout" + app:layout_constraintBottom_toTopOf="@id/bottom_nav" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/activity_server_profiles.xml b/sensorhub-android-app/res/layout/activity_server_profiles.xml new file mode 100644 index 00000000..02c516ed --- /dev/null +++ b/sensorhub-android-app/res/layout/activity_server_profiles.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml b/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml new file mode 100644 index 00000000..a2384931 --- /dev/null +++ b/sensorhub-android-app/res/layout/dialog_edit_server_profile.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/dialog_meshtastic.xml b/sensorhub-android-app/res/layout/dialog_meshtastic.xml index f8657c40..bfbedc20 100644 --- a/sensorhub-android-app/res/layout/dialog_meshtastic.xml +++ b/sensorhub-android-app/res/layout/dialog_meshtastic.xml @@ -1,42 +1,50 @@ + android:padding="@dimen/content_padding_large"> - - - - - - - - - + app:boxCornerRadiusTopStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusBottomEnd="8dp" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> + + - - - - - - - + - + app:boxCornerRadiusTopStart="8dp" + app:boxCornerRadiusTopEnd="8dp" + app:boxCornerRadiusBottomStart="8dp" + app:boxCornerRadiusBottomEnd="8dp" + style="@style/Widget.Material3.TextInputLayout.OutlinedBox"> + + + + diff --git a/sensorhub-android-app/res/layout/fragment_dashboard.xml b/sensorhub-android-app/res/layout/fragment_dashboard.xml new file mode 100644 index 00000000..b916bc37 --- /dev/null +++ b/sensorhub-android-app/res/layout/fragment_dashboard.xml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/fragment_sensors.xml b/sensorhub-android-app/res/layout/fragment_sensors.xml new file mode 100644 index 00000000..90421d75 --- /dev/null +++ b/sensorhub-android-app/res/layout/fragment_sensors.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_server_profile.xml b/sensorhub-android-app/res/layout/item_server_profile.xml new file mode 100644 index 00000000..79f91884 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_server_profile.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/item_server_status.xml b/sensorhub-android-app/res/layout/item_server_status.xml new file mode 100644 index 00000000..60bf5e54 --- /dev/null +++ b/sensorhub-android-app/res/layout/item_server_status.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-app/res/layout/preference_item.xml b/sensorhub-android-app/res/layout/preference_item.xml new file mode 100644 index 00000000..47167543 --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_item.xml @@ -0,0 +1,28 @@ + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/preference_list_item.xml b/sensorhub-android-app/res/layout/preference_list_item.xml new file mode 100644 index 00000000..82176640 --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_list_item.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/layout/preference_switch_item.xml b/sensorhub-android-app/res/layout/preference_switch_item.xml new file mode 100644 index 00000000..24ec3c5c --- /dev/null +++ b/sensorhub-android-app/res/layout/preference_switch_item.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/menu/bottom_nav_menu.xml b/sensorhub-android-app/res/menu/bottom_nav_menu.xml new file mode 100644 index 00000000..081fc3e4 --- /dev/null +++ b/sensorhub-android-app/res/menu/bottom_nav_menu.xml @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/sensorhub-android-app/res/menu/main.xml b/sensorhub-android-app/res/menu/main.xml index 4404e5d9..e57c949e 100644 --- a/sensorhub-android-app/res/menu/main.xml +++ b/sensorhub-android-app/res/menu/main.xml @@ -1,40 +1,25 @@ + tools:context="org.sensorhub.android.MainActivity"> + android:id="@+id/action_meshtastic" + android:orderInCategory="103" + android:icon="@drawable/ic_message" + android:title="@string/action_meshtastic" + app:showAsAction="never" /> - - + android:id="@+id/action_about" + android:orderInCategory="120" + android:title="@string/action_about" + app:showAsAction="never" /> - - - + android:title="@string/action_status" + app:showAsAction="never" /> diff --git a/sensorhub-android-app/res/values-v11/styles.xml b/sensorhub-android-app/res/values-v11/styles.xml deleted file mode 100644 index 3c02242a..00000000 --- a/sensorhub-android-app/res/values-v11/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/sensorhub-android-app/res/values-v14/styles.xml b/sensorhub-android-app/res/values-v14/styles.xml deleted file mode 100644 index a91fd037..00000000 --- a/sensorhub-android-app/res/values-v14/styles.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/sensorhub-android-app/res/values-zh-rTW/strings.xml b/sensorhub-android-app/res/values-zh-rTW/strings.xml new file mode 100644 index 00000000..ee4f1bdc --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings.xml @@ -0,0 +1,217 @@ + + + OpenSensorHub + 設定參數並點擊播放按鈕以啟動 SmartHub + 設定 + 啟動 SmartHub + 停止 SmartHub + 應用程式狀態 + 關於 + 啟動代理 + 停止代理 + Meshtastic 訊息 + SOS 設定(必填) + SOS-T 設定(選填) + Android 感測器 + TruPulse 測距感測器 + Meshtastic 感測器 + Angel 感測器 + Flirone 感測器 + Polar 心率監測器 + 控制器 + 紅隼感測器 + 雷達偵測感測器 + STE輻射尋呼機 感測器 + 範本 感測器 + 加速度計資料 + 陀螺儀資料 + 磁力計資料 + 方向資料(四元數) + 方向資料(歐拉角) + GPS 定位資料 + 網路定位資料 + 影片資料 + 影片滾動資料 + 音訊資料 + + 裝置名稱 + 裝置 IP 位址 + 伺服器設定檔 + 管理伺服器 + 服務 + 啟用 SOS 服務 + 啟用連線系統服務 + 啟用探索服務 + 執行名稱 + + 啟用加速度計資料串流 + 啟用陀螺儀資料串流 + 啟用磁力計資料串流 + 啟用方向資料串流 + 啟用 GPS 定位資料串流 + 啟用網路定位資料串流 + 啟用影片資料串流 + 在影片幀標頭中包含影片滾動資料 + 啟用音訊資料串流 + 啟用 Meshtastic 資料串流(感測器須在啟動時透過藍牙連接) + 啟用雷達偵測感測器串流 + 啟用 Polar 心率感測器串流(感測器須透過藍牙 LE 連接) + 啟用紅隼氣象儀串流 + 啟用 USB 控制器串流(控制器須在啟動時透過 USB 連接) + 啟用 TruPulse 測距儀資料串流(感測器須在啟動時透過藍牙連接) + 使用模擬 TruPulse 資料取代實際感測器資料 + 啟用 Angel 感測器健康資料串流(感測器須在啟動時透過藍牙 LE 連接) + 啟用 FLIR One 熱像儀資料串流(透過 USB 連接時) + 啟用 STE RadPager 資料串流(感測器須在啟動時透過藍牙 LE 連接) + 啟用範本驅動程式串流 + 資料推送選項 + 點擊以選擇或輸入裝置位址 + 感測器 UID 延伸 + 新增、編輯或刪除伺服器 + + + 四元數 + 歐拉角 + + + + GPS + 網路 + + + + 可擷取 + 本機儲存 + 遠端推送 + + + Trupulse 裝置名稱 + + 串流實體裝置 + 模擬虛擬裝置 + + STREAM + + + + 選擇報告項目 + 道路封閉 + 淹水 + 醫療 + 救助 + + + + 最近信標 + 三邊測量 + + + + GPS + 雷射測距儀 + 網路 + + + 現場報告 + 名稱: + 描述: + 拍攝 + 重設 + 提交報告 + 報告名稱 + + 半徑: + 緯度: + 經度: + 英尺 + + + 選擇... + 公共 + 全部 + + + + 選擇... + 開放 + 關閉 + + + 動作: + 參考編號: + 類型: + + + 選擇... + 渠道排水 + 地表 + + + + 選擇... + 儀器 + 目視 + 模型 + + + 特徵類型: + 深度: + 觀測模式: + + 描述醫療狀況... + 輸入測量值(血壓、體溫等)... + 緊急狀況(是/否) + + + 選擇... + 環境 + 健康 + 安全 + 服務 + + + 救助類型: + 人數: + 緊急程度: + 描述所需救助... + 輸入您的姓名或編號 + + + 選擇... + 人員 + 車輛 + 裝置 + + + + 選擇... + GPS + 藍牙信標 + WiFi + 行動網路 + UWB + + + + 追蹤資源: + 追蹤方法: + 輸入資源編號 + 輸入資源標籤 + + + 英寸 + 密耳 + tmoa + smoa + + 公分 + + + 感測器 + 首頁 + 設定 + 輸入訊息! + 輸入訊息! + 輸入目標節點 ID(整數) + 輸入目標節點 ID(整數) + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml b/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml new file mode 100644 index 00000000..28e4904f --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings_activity_user_settings.xml @@ -0,0 +1,6 @@ + + + 設定 + 一般 + 感測器 + diff --git a/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml new file mode 100644 index 00000000..29d5bc40 --- /dev/null +++ b/sensorhub-android-app/res/values-zh-rTW/strings_app_status.xml @@ -0,0 +1,22 @@ + + + + 應用程式狀態 + + + 初始化中 + 已初始化 + 啟動中 + 已啟動 + 停止中 + 已停止 + 未知 + + + SOS 服務狀態 + ConSys 服務狀態 + 探索服務狀態 + HTTP 伺服器狀態 + Android 感測器狀態 + Android 感測器儲存狀態 + diff --git a/sensorhub-android-app/res/values/colors.xml b/sensorhub-android-app/res/values/colors.xml new file mode 100644 index 00000000..d04f4a14 --- /dev/null +++ b/sensorhub-android-app/res/values/colors.xml @@ -0,0 +1,73 @@ + + + + #D35400 + #FFFFFF + + #24160A + #E6B89C + + #C24E00 + + #9A9A9A + #0A0A0A + + #242424 + #F5F5F5 + + #B84A0D + #FFFFFF + + #1C0A00 + #E6B89C + + #CF6679 + #1C0006 + + #4A0011 + #FFB3BA + + #0A0A0A + + #F5F5F5 + + #141414 + #F5F5F5 + + #1C1C1C + #9A9A9A + + #2E2E2E + + #0A0A0A + #141414 + #1C1C1C + #242424 + + #F5F5F5 + #9A9A9A + #5A5A5A + + + #D35400 + #C24E00 + #1AD35400 + + #141414 + #F5F5F5 + + #141414 + #D35400 + #6A6A6A + + #4CAF50 + #F44336 + #FF9800 + #5A5A5A + + #1AFFFFFF + #99000000 + + #1C1C1C + + \ No newline at end of file diff --git a/sensorhub-android-app/res/values/dimens.xml b/sensorhub-android-app/res/values/dimens.xml index 32b94bb7..f813aab0 100644 --- a/sensorhub-android-app/res/values/dimens.xml +++ b/sensorhub-android-app/res/values/dimens.xml @@ -1,7 +1,25 @@ - - + 0dp 0dp + + 16dp + 8dp + 24dp + + + 12dp + 2dp + 12dp + + + 12dp + + + 4dp + + + 12dp + 12dp diff --git a/sensorhub-android-app/res/values/strings.xml b/sensorhub-android-app/res/values/strings.xml index 3cab0500..61159a02 100644 --- a/sensorhub-android-app/res/values/strings.xml +++ b/sensorhub-android-app/res/values/strings.xml @@ -1,7 +1,7 @@ - OpenSensorHub SmartHub - Please configure and start SmartHub using the Options Menu + OpenSensorHub + Configure settings and tap the play button to start SmartHub Settings Start SmartHub Stop SmartHub @@ -13,10 +13,63 @@ SOS Settings (Required) SOS-T Settings (Optional) Android Sensor - TruPulse Range Finder Sensor - Meshtastic Sensor Angel Sensor + Meshtastic Sensor + Wardriving Sensor + Polar Heart Monitor Sensor + Kestrel Sensor + Controller Sensor + TruPulse Range Finder Sensor Flirone Sensor + STE Radiation Pager Sensor + Template Sensor + Accelerometer Data + Gyroscope Data + Magnetometer Data + Orientation Data (Quaternions) + Orientation Data (Euler Angles) + GPS Location Data + Network Location Data + Video Data + Video Roll Data + Audio Data + + Device Name + Device IP Address + Server Profiles + Manage Servers + Services + Enable SOS Service + Enable Connected Systems Service + Enable Discovery Service + + Run Name + + Enable streaming of accelerometer data + Enable streaming of gyroscope data + Enable streaming of magnetometer data + Enable streaming of orientation data + Enable streaming of GPS location data + Enable streaming of network location data + Enable streaming of video data + Include video roll data in video frame header + Enable streaming of audio data + Enable streaming of Meshtastic data (sensor must be connected via Bluetooth on startup) + Enable streaming of Wardriving Sensor + Enable streaming of Polar Heart Sensor (sensor must be connected via Bluetooth LE) + Enable streaming of Kestrel Weather Meter + Enable streaming of USB Controller (controller must be connected to device via USB on startup) + Enable streaming of TruPulse range finder data (sensor must be connected via Bluetooth on startup) + Use simulated TruPulse data instead of the actual sensor data + Enable streaming of Angel Sensor health data (sensor must be connected via Bluetooth LE on startup) + Enable streaming of FLIR One thermal camera data when connected on USB port + Enable streaming of STE RadPager data (sensor must be connected via Bluetooth LE on startup) + Enable streaming of template driver + Options for pushing data + Tap to select or enter device address + Sensors UID Extension + Add, edit, or remove servers + JPEG H264 @@ -25,6 +78,20 @@ VP8 + + 24 + 30 + 60 + 120 + + + + 24 + 30 + 60 + 120 + + Quaternion Euler @@ -236,6 +303,10 @@ + + Sensors + Home + Settings Enter a message! Enter a message! Enter the destination Node ID (integer) diff --git a/sensorhub-android-app/res/values/strings_app_status.xml b/sensorhub-android-app/res/values/strings_app_status.xml index 3bdf465a..dd4a190e 100644 --- a/sensorhub-android-app/res/values/strings_app_status.xml +++ b/sensorhub-android-app/res/values/strings_app_status.xml @@ -17,6 +17,7 @@ SOS Service Status ConSys Service Status + Discovery Service Status HTTP Server Status Android Sensor Status Android Sensor Storage Status diff --git a/sensorhub-android-app/res/values/styles.xml b/sensorhub-android-app/res/values/styles.xml index 6ce89c7b..80a26efd 100644 --- a/sensorhub-android-app/res/values/styles.xml +++ b/sensorhub-android-app/res/values/styles.xml @@ -1,20 +1,244 @@ - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_audio.xml b/sensorhub-android-app/res/xml/pref_audio.xml deleted file mode 100644 index f23302f1..00000000 --- a/sensorhub-android-app/res/xml/pref_audio.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - diff --git a/sensorhub-android-app/res/xml/pref_general.xml b/sensorhub-android-app/res/xml/pref_general.xml deleted file mode 100644 index 9249b2e3..00000000 --- a/sensorhub-android-app/res/xml/pref_general.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_headers.xml b/sensorhub-android-app/res/xml/pref_headers.xml deleted file mode 100644 index c5f5f977..00000000 --- a/sensorhub-android-app/res/xml/pref_headers.xml +++ /dev/null @@ -1,20 +0,0 @@ - - -
-
-
-
- - - - - - diff --git a/sensorhub-android-app/res/xml/pref_kestrel.xml b/sensorhub-android-app/res/xml/pref_kestrel.xml deleted file mode 100644 index 54b96166..00000000 --- a/sensorhub-android-app/res/xml/pref_kestrel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/sensorhub-android-app/res/xml/pref_sensors.xml b/sensorhub-android-app/res/xml/pref_sensors.xml index 552ae3ed..14fa1e44 100644 --- a/sensorhub-android-app/res/xml/pref_sensors.xml +++ b/sensorhub-android-app/res/xml/pref_sensors.xml @@ -1,337 +1,480 @@ - + - - + - + android:summary="@string/summary_accel" + android:title="@string/sensor_accelerometer" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_gyro" + android:title="@string/sensor_gyroscope" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_mag" + android:title="@string/sensor_magnetometer" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_orientation" + android:title="@string/sensor_orientation_quat" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_orientation" + android:title="@string/sensor_orientation_euler" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_gps" + android:title="@string/sensor_gps" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_netloc" + android:title="@string/sensor_network_location" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + + android:summary="@string/summary_video" + android:title="@string/sensor_video" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + + + + + + + android:summary="@string/summary_video_roll" + android:title="@string/sensor_video_roll" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_audio" + android:title="@string/sensor_audio" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> + + + - + + + + + android:summary="@string/summary_meshtastic" + android:title="@string/sensor_meshtastic" + android:layout="@layout/preference_switch_item" /> - + android:summary="@string/summary_select_device" + android:layout="@layout/preference_list_item" /> + + + + + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item" /> - + android:summary="@string/summary_polar" + android:title="@string/sensor_polar" + android:layout="@layout/preference_switch_item" /> - + android:summary="@string/summary_select_device" + android:layout="@layout/preference_list_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item" /> - + android:summary="@string/summary_kestrel" + android:title="@string/sensor_kestrel" + android:layout="@layout/preference_switch_item" /> - - + android:summary="@string/summary_select_device" + android:layout="@layout/preference_list_item" /> + + + + + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_trupulse" + android:title="@string/sensor_trupulse" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@string/trupulse_datasource_default" + android:layout="@layout/preference_list_item"/> - + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_select_device" + android:layout="@layout/preference_list_item" /> - + android:summary="@string/summary_trupulse_simu" + android:title="Use Simulated TruPulse Data" + android:layout="@layout/preference_switch_item" /> - + android:summary="@string/summary_angel" + android:title="@string/sensor_angel" + android:layout="@layout/preference_switch_item" /> + android:title="Angel Sensor Bluetooth Address" + android:layout="@layout/preference_item"/> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item" /> - + android:summary="@string/summary_flirone" + android:title="@string/sensor_flirone" + android:layout="@layout/preference_switch_item" /> + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + android:summary="@string/summary_ste" + android:title="@string/sensor_ste" + android:layout="@layout/preference_switch_item" /> - + android:defaultValue="@array/sos_option_defaults" + android:layout="@layout/preference_list_item"/> - + + + + diff --git a/sensorhub-android-app/res/xml/pref_settings.xml b/sensorhub-android-app/res/xml/pref_settings.xml new file mode 100644 index 00000000..fa496da8 --- /dev/null +++ b/sensorhub-android-app/res/xml/pref_settings.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sensorhub-android-app/res/xml/pref_video.xml b/sensorhub-android-app/res/xml/pref_video.xml deleted file mode 100644 index 96080f4b..00000000 --- a/sensorhub-android-app/res/xml/pref_video.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - diff --git a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java index 993ba96b..4e02d47c 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/AppStatusActivity.java @@ -2,11 +2,16 @@ import android.content.Context; import android.content.Intent; -import androidx.appcompat.app.AppCompatActivity; +import android.graphics.drawable.GradientDrawable; import android.os.Bundle; -import android.util.Log; +import android.view.View; import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.google.android.material.appbar.MaterialToolbar; + public class AppStatusActivity extends AppCompatActivity { @Override @@ -14,25 +19,59 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_app_status); + MaterialToolbar toolbar = findViewById(R.id.status_toolbar); + setSupportActionBar(toolbar); + toolbar.setNavigationOnClickListener(v -> onBackPressed()); + Intent intent = getIntent(); - Context appContext = getApplicationContext(); String sosStatus = intent.getStringExtra("sosService"); String consSysStatus = intent.getStringExtra("conSysService"); + String discoveryStatus = intent.getStringExtra("discoveryService"); String httpStatus = intent.getStringExtra("httpStatus"); String sensorStatus = intent.getStringExtra("androidSensorStatus"); String sensorStorageStatus = intent.getStringExtra("sensorStorageStatus"); - TextView sosStatusView = (TextView) findViewById(R.id.sos_service_state); - TextView conSysStatusView = (TextView) findViewById(R.id.consys_service_state); - TextView httpStatusView = (TextView) findViewById(R.id.http_service_state); - TextView sensorStatusView = (TextView) findViewById(R.id.sensor_service_state); - TextView storageStatusView = (TextView) findViewById(R.id.storage_service_state); + TextView sosStatusView = findViewById(R.id.sos_service_state); + TextView conSysStatusView = findViewById(R.id.consys_service_state); + TextView discoveryStatusView = findViewById(R.id.discovery_service_state); + TextView httpStatusView = findViewById(R.id.http_service_state); + TextView sensorStatusView = findViewById(R.id.sensor_service_state); + TextView storageStatusView = findViewById(R.id.storage_service_state); sosStatusView.setText(sosStatus); conSysStatusView.setText(consSysStatus); + discoveryStatusView.setText(discoveryStatus); httpStatusView.setText(httpStatus); sensorStatusView.setText(sensorStatus); storageStatusView.setText(sensorStorageStatus); + + setStatusDotColor(findViewById(R.id.sos_status_dot), sosStatus); + setStatusDotColor(findViewById(R.id.consys_status_dot), consSysStatus); + setStatusDotColor(findViewById(R.id.discovery_status_dot), discoveryStatus); + setStatusDotColor(findViewById(R.id.http_status_dot), httpStatus); + setStatusDotColor(findViewById(R.id.sensor_status_dot), sensorStatus); + setStatusDotColor(findViewById(R.id.storage_status_dot), sensorStorageStatus); + } + + private void setStatusDotColor(View dot, String status) { + int colorRes; + if (status == null) { + colorRes = R.color.status_unknown; + } else { + String lower = status.toLowerCase(); + if (lower.contains("started")) { + colorRes = R.color.status_started; + } else if (lower.contains("stopped")) { + colorRes = R.color.status_stopped; + } else if (lower.contains("starting") || lower.contains("initializ")) { + colorRes = R.color.status_initializing; + } else { + colorRes = R.color.status_unknown; + } + } + + GradientDrawable background = (GradientDrawable) dot.getBackground(); + background.setColor(ContextCompat.getColor(this, colorRes)); } -} \ No newline at end of file +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java new file mode 100644 index 00000000..72b5d27b --- /dev/null +++ b/sensorhub-android-app/src/org/sensorhub/android/DashboardFragment.java @@ -0,0 +1,646 @@ +package org.sensorhub.android; + +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.graphics.SurfaceTexture; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.ImageButton; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.textfield.TextInputEditText; +import com.google.android.material.textfield.TextInputLayout; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.drawable.GradientDrawable; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; + +import org.sensorhub.api.event.Event; +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.ModuleEvent; +import org.sensorhub.impl.client.sost.SOSTClient; +import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; +import org.sensorhub.impl.event.EventBus; +import org.sensorhub.impl.module.ModuleRegistry; +import org.sensorhub.impl.sensor.android.AndroidSensorsConfig; +import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; +import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; +import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig.VideoPreset; +import org.sensorhub.impl.service.consys.client.ConSysApiClientModule; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.Flow; + +import android.widget.LinearLayout; + + +public class DashboardFragment extends Fragment implements TextureView.SurfaceTextureListener, Flow.Subscriber +{ + private TextView videoInfoArea; + private TextureView textureView; + private MaterialCardView videoStatusCard; + private MaterialButton btnToggleVideo; + private View videoStatusDot; + private FloatingActionButton fab; + private LinearLayout serverStatusContainer; + private Handler displayHandler; + private Runnable displayCallback; + private StringBuffer videoInfoText = new StringBuffer(); + private Flow.Subscription subscription; + private SensorHubServiceProvider provider; + private boolean videoPreviewVisible = false; + + private final Map serverCardViews = new HashMap<>(); + private final Set expandedServers = new HashSet<>(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + provider = (SensorHubServiceProvider) requireActivity(); + displayHandler = new Handler(Looper.getMainLooper()); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_dashboard, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + videoInfoArea = view.findViewById(R.id.video_info); + + textureView = view.findViewById(R.id.video); + textureView.setSurfaceTextureListener(this); + + videoStatusCard = view.findViewById(R.id.video_status_card); + btnToggleVideo = view.findViewById(R.id.btn_toggle_video); + videoStatusDot = view.findViewById(R.id.video_status_dot); + + btnToggleVideo.setOnClickListener(v -> toggleVideoPreview()); + + serverStatusContainer = view.findViewById(R.id.server_status_container); + + fab = view.findViewById(R.id.fab_toggle); + fab.setOnClickListener(v -> { + if (!provider.isOshStarted()) { + if (provider.getBoundService() != null) + showRunNamePopup(); + } else { + stopHub(); + } + }); + + updateFabIcon(); + } + + @Override + public void onResume() { + super.onResume(); + if (provider.isOshStarted()) { + startRefreshingStatus(); + updateVideoStatusCard(); + } + } + + @Override + public void onPause() { + stopRefreshingStatus(); + super.onPause(); + } + + @Override + public void onDestroyView() { + stopRefreshingStatus(); + displayHandler.removeCallbacksAndMessages(null); + super.onDestroyView(); + } + + private void updateFabIcon() { + if (fab == null) return; + if (provider.isOshStarted()) { + fab.setImageResource(R.drawable.ic_stop); + } else { + fab.setImageResource(R.drawable.ic_play); + } + } + + private void stopHub() { + Toast.makeText(requireContext(), "Stopping SensorHub", Toast.LENGTH_SHORT).show(); + stopRefreshingStatus(); + provider.stopSensorHub(); + updateFabIcon(); + hideVideoPreview(); + clearTextureView(); + videoStatusCard.setVisibility(View.GONE); + newStatusMessage("SensorHub Stopped"); + requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + private void clearTextureView() { + if (textureView == null || textureView.getSurfaceTexture() == null) return; + Canvas canvas = textureView.lockCanvas(); + if (canvas != null) { + canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); + textureView.unlockCanvasAndPost(canvas); + } + } + + + protected synchronized void showRunNamePopup() { + MaterialAlertDialogBuilder alert = new MaterialAlertDialogBuilder(requireContext()); + alert.setTitle("Run Name"); + alert.setMessage("Please enter the name for this run"); + + TextInputLayout inputLayout = new TextInputLayout(requireContext()); + inputLayout.setBoxBackgroundMode(TextInputLayout.BOX_BACKGROUND_OUTLINE); + inputLayout.setHint("Run Name"); + + TextInputEditText input = new TextInputEditText(inputLayout.getContext()); + input.getText().append("Run-"); + SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); + input.getText().append(formatter.format(new Date())); + inputLayout.addView(input); + + int padding = (int) (24 * getResources().getDisplayMetrics().density); + FrameLayout container = new FrameLayout(requireContext()); + container.setPadding(padding, 0, padding, 0); + container.addView(inputLayout); + alert.setView(container); + + alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int whichButton) { + requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + String runName = input.getText().toString(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + provider.updateConfig(prefs, runName); + + AndroidSensorsConfig androidSensorConfig = (AndroidSensorsConfig) provider.getSensorhubConfig().get("ANDROID_SENSORS"); + VideoEncoderConfig videoConfig = androidSensorConfig.videoConfig; + + boolean cameraInUse = (androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera); + boolean improperVideoSettings = (videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length); + + if (cameraInUse && improperVideoSettings) { + showVideoConfigErrorPopup(); + newStatusMessage("Video Config Error: Check Settings"); + } else { + Toast.makeText(requireContext(), "Starting SensorHub...", Toast.LENGTH_SHORT).show(); + newStatusMessage("Starting SensorHub..."); + provider.getSostClients().clear(); + provider.getConSysClients().clear(); + provider.startSensorHub(); + + waitForHubReady(); + } + } + }); + + alert.setNegativeButton("Cancel", (dialog, whichButton) -> {}); + alert.show(); + } + + private static final int HUB_POLL_INTERVAL_MS = 200; + private static final int HUB_POLL_MAX_ATTEMPTS = 150; + private int hubPollAttempts = 0; + + private void waitForHubReady() { + hubPollAttempts = 0; + displayHandler.post(this::pollHubReady); + } + + private void pollHubReady() { + if (!isAdded()) return; + + SensorHubService service = provider.getBoundService(); + hubPollAttempts++; + + if (service != null && service.getSensorHub() != null && service.getSensorHub().getEventBus() != null) { + EventBus shEvtBus = (EventBus) service.getSensorHub().getEventBus(); + shEvtBus.newSubscription() + .withTopicID(ModuleRegistry.EVENT_GROUP_ID) + .subscribe(DashboardFragment.this); + + ModuleRegistry registry = (ModuleRegistry) service.getSensorHub().getModuleRegistry(); + for (IModule module : registry.getLoadedModules()) { + if (module instanceof SOSTClient) { + provider.getSostClients().add((SOSTClient) module); + } else if (module instanceof ConSysApiClientModule) { + provider.getConSysClients().add((ConSysApiClientModule) module); + } else if (module instanceof AndroidSensorsDriver) { + provider.setAndroidSensors((AndroidSensorsDriver) module); + } + } + + if (!provider.isOshStarted()) { + provider.setOshStarted(true); + updateFabIcon(); + serverStatusContainer.removeAllViews(); + serverCardViews.clear(); + startRefreshingStatus(); + updateVideoStatusCard(); + if (videoPreviewVisible) + showVideo(); + } + } else if (hubPollAttempts < HUB_POLL_MAX_ATTEMPTS) { + displayHandler.postDelayed(this::pollHubReady, HUB_POLL_INTERVAL_MS); + } else { + newStatusMessage("SensorHub failed to start"); + updateFabIcon(); + } + } + + protected void showVideoConfigErrorPopup() { + String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; + new MaterialAlertDialogBuilder(requireContext()) + .setTitle("OpenSensorHub") + .setMessage(message) + .setPositiveButton("OK", (dialog, id) -> {}) + .show(); + } + + protected void startRefreshingStatus() { + if (displayCallback != null) return; + + displayCallback = new Runnable() { + public void run() { + displayStatus(); + videoInfoArea.setText(Html.fromHtml(videoInfoText.toString())); + displayHandler.postDelayed(this, 1000); + } + }; + displayHandler.post(displayCallback); + } + + protected void stopRefreshingStatus() { + if (displayCallback != null) { + displayHandler.removeCallbacks(displayCallback); + displayCallback = null; + } + } + + protected synchronized void displayStatus() { + Set activeClientIds = new HashSet<>(); + + for (SOSTClient client : provider.getSostClients()) { + String clientId = client.getLocalID(); + activeClientIds.add(clientId); + String serverName = extractServerName(client.getName(), "SOS-T"); + String clientMode = "SOS-T"; + + Map dataStreams = client.getDataStreams(); + StringBuffer detailHtml = new StringBuffer(); + boolean hasError = false; + + if (client.getCurrentError() != null) { + hasError = true; + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage() != null ? errorObj.getMessage().trim() : "Unknown error"; + if (!errorMsg.endsWith(".")) errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + detailHtml.append("" + errorMsg + "
"); + } + if (dataStreams.isEmpty() && client.getStatusMessage() != null) { + detailHtml.append(client.getStatusMessage() + "
"); + } + + long now = System.currentTimeMillis(); + boolean allOk = !hasError && !dataStreams.isEmpty(); + for (Entry stream : dataStreams.entrySet()) { + detailHtml.append("" + stream.getKey() + " : "); + long lastEventTime = stream.getValue().lastEventTime; + long dt = now - lastEventTime; + if (lastEventTime == Long.MIN_VALUE) { + detailHtml.append("NO OBS"); + allOk = false; + } else if (dt > stream.getValue().measPeriodMs) { + detailHtml.append("NOK (" + dt + "ms ago)"); + allOk = false; + } else { + detailHtml.append("OK (" + dt + "ms ago)"); + } + if (stream.getValue().errorCount > 0) { + detailHtml.append(" (" + stream.getValue().errorCount + ")"); + allOk = false; + } + detailHtml.append("
"); + } + + updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); + } + + for (ConSysApiClientModule client : provider.getConSysClients()) { + String clientId = client.getLocalID(); + activeClientIds.add(clientId); + String serverName = extractServerName(client.getName(), "Connected Systems"); + String clientMode = "Connected Systems"; + + Map dataStreams = client.getDataStreams(); + StringBuffer detailHtml = new StringBuffer(); + boolean hasError = false; + + if (client.getCurrentError() != null) { + hasError = true; + Throwable errorObj = client.getCurrentError(); + String errorMsg = errorObj.getMessage() != null ? errorObj.getMessage().trim() : "Unknown error"; + if (!errorMsg.endsWith(".")) errorMsg += ". "; + if (errorObj.getCause() != null && errorObj.getCause().getMessage() != null) + errorMsg += errorObj.getCause().getMessage(); + detailHtml.append("" + errorMsg + "
"); + } + if (dataStreams.isEmpty() && client.getStatusMessage() != null) { + detailHtml.append(client.getStatusMessage() + "
"); + } + + long now = System.currentTimeMillis(); + boolean allOk = !hasError && !dataStreams.isEmpty(); + for (Entry stream : dataStreams.entrySet()) { + detailHtml.append("" + stream.getKey() + " : "); + long lastEventTime = stream.getValue().lastEventTime; + long dt = now - lastEventTime; + if (lastEventTime == Long.MIN_VALUE) { + detailHtml.append("NO OBS"); + allOk = false; + } else if (dt > stream.getValue().measPeriodMs) { + detailHtml.append("NOK (" + dt + "ms ago)"); + allOk = false; + } else { + detailHtml.append("OK (" + dt + "ms ago)"); + } + if (stream.getValue().errorCount > 0) { + detailHtml.append(" (" + stream.getValue().errorCount + ")"); + allOk = false; + } + detailHtml.append("
"); + } + + updateServerCard(clientId, serverName, clientMode, allOk, hasError, detailHtml.toString()); + } + + Set staleIds = new HashSet<>(serverCardViews.keySet()); + staleIds.removeAll(activeClientIds); + for (String id : staleIds) { + View card = serverCardViews.remove(id); + if (card != null) serverStatusContainer.removeView(card); + expandedServers.remove(id); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); + MainActivity activity = (MainActivity) requireActivity(); + boolean serveOrStore = activity.shouldServe(prefs) || activity.shouldStore(prefs); + boolean noClients = provider.getSostClients().isEmpty() && provider.getConSysClients().isEmpty(); + + View emptyView = serverStatusContainer.findViewWithTag("empty_status"); + if (noClients && serveOrStore) { + if (emptyView == null) { + TextView tv = new TextView(requireContext()); + tv.setTag("empty_status"); + tv.setText("No Sensors Set to Push Remotely"); + tv.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurfaceVariant)); + tv.setTextSize(14); + tv.setGravity(android.view.Gravity.CENTER); + int pad = (int) (16 * getResources().getDisplayMetrics().density); + tv.setPadding(pad, pad, pad, pad); + serverStatusContainer.addView(tv); + } + } else if (emptyView != null) { + serverStatusContainer.removeView(emptyView); + } + + AndroidSensorsDriver sensors = provider.getAndroidSensors(); + SensorHubService service = provider.getBoundService(); + if (sensors != null && service != null && service.hasVideo()) { + try { + VideoEncoderConfig config = sensors.getConfiguration().videoConfig; + VideoPreset preset = config.presets[config.selectedPreset]; + videoInfoText.setLength(0); + videoInfoText.append(config.codec).append(", ") + .append(preset.width).append("x").append(preset.height).append(", ") + .append(config.frameRate).append(" fps, ") + .append(preset.selectedBitrate).append(" kbits/s"); + } catch (Exception e) { + // ignore display errors + } + updateVideoStatusCard(); + if (videoPreviewVisible) + showVideo(); + } + } + + protected synchronized void newStatusMessage(String msg) { + displayHandler.post(() -> { + serverStatusContainer.removeAllViews(); + serverCardViews.clear(); + TextView tv = new TextView(requireContext()); + tv.setText(msg); + tv.setTextColor(ContextCompat.getColor(requireContext(), R.color.md_theme_onSurface)); + tv.setTextSize(14); + int pad = (int) (16 * getResources().getDisplayMetrics().density); + tv.setPadding(pad, pad, pad, pad); + serverStatusContainer.addView(tv); + }); + } + + private String extractServerName(String clientName, String fallback) { + if (clientName != null && clientName.contains(" -> ")) { + return clientName.substring(clientName.lastIndexOf(" -> ") + 4); + } + return fallback; + } + + private void updateServerCard(String clientId, String serverName, String clientMode, + boolean allOk, boolean hasError, String detailHtml) { + View card = serverCardViews.get(clientId); + + if (card == null) { + card = LayoutInflater.from(requireContext()) + .inflate(R.layout.item_server_status, serverStatusContainer, false); + serverCardViews.put(clientId, card); + serverStatusContainer.addView(card); + + final View cardRef = card; + final String idRef = clientId; + View header = card.findViewById(R.id.server_status_header); + header.setOnClickListener(v -> { + boolean expanded = expandedServers.contains(idRef); + TextView details = cardRef.findViewById(R.id.server_status_details); + ImageButton toggle = cardRef.findViewById(R.id.btn_toggle_server_details); + if (expanded) { + expandedServers.remove(idRef); + details.setVisibility(View.GONE); + toggle.setImageResource(R.drawable.ic_expand_more); + } else { + expandedServers.add(idRef); + details.setVisibility(View.VISIBLE); + toggle.setImageResource(R.drawable.ic_expand_less); + } + }); + } + + TextView nameView = card.findViewById(R.id.server_status_name); + TextView modeView = card.findViewById(R.id.server_status_mode); + nameView.setText(serverName); + modeView.setText(clientMode); + + View dot = card.findViewById(R.id.server_status_dot); + if (dot.getBackground() instanceof GradientDrawable) { + GradientDrawable bg = (GradientDrawable) dot.getBackground(); + int colorRes; + if (hasError) colorRes = R.color.status_stopped; + else if (allOk) colorRes = R.color.status_started; + else colorRes = R.color.status_initializing; + bg.setColor(ContextCompat.getColor(requireContext(), colorRes)); + } + + if (card instanceof MaterialCardView) { + int strokeColorRes; + if (hasError) strokeColorRes = R.color.status_stopped; + else if (allOk) strokeColorRes = R.color.status_started; + else strokeColorRes = R.color.md_theme_outline; + ((MaterialCardView) card).setStrokeColor( + ContextCompat.getColor(requireContext(), strokeColorRes)); + } + + TextView details = card.findViewById(R.id.server_status_details); + details.setText(Html.fromHtml(detailHtml)); + boolean expanded = expandedServers.contains(clientId); + details.setVisibility(expanded ? View.VISIBLE : View.GONE); + + ImageButton toggle = card.findViewById(R.id.btn_toggle_server_details); + toggle.setImageResource(expanded ? R.drawable.ic_expand_less : R.drawable.ic_expand_more); + } + + private void updateVideoStatusCard() { + SensorHubService service = provider.getBoundService(); + boolean hasVideo = service != null && service.hasVideo(); + + videoStatusCard.setVisibility(hasVideo ? View.VISIBLE : View.GONE); + + if (hasVideo && videoInfoText.length() > 0) { + videoInfoArea.setText(videoInfoText.toString()); + } + + if (videoStatusDot != null && videoStatusDot.getBackground() instanceof GradientDrawable) { + GradientDrawable dot = (GradientDrawable) videoStatusDot.getBackground(); + int color = ContextCompat.getColor(requireContext(), + hasVideo ? R.color.status_started : R.color.status_unknown); + dot.setColor(color); + } + } + + private void toggleVideoPreview() { + videoPreviewVisible = !videoPreviewVisible; + if (videoPreviewVisible) { + textureView.setVisibility(View.VISIBLE); + btnToggleVideo.setText("Hide"); + serverStatusContainer.setBackgroundColor(getResources().getColor(R.color.overlay_light, requireActivity().getTheme())); + showVideo(); + } else { + hideVideoPreview(); + } + } + + private void hideVideoPreview() { + videoPreviewVisible = false; + textureView.setVisibility(View.GONE); + if (btnToggleVideo != null) btnToggleVideo.setText("Show"); + serverStatusContainer.setBackgroundColor(0x00000000); + } + + protected void showVideo() { + SensorHubService service = provider.getBoundService(); + if (service != null && service.getVideoTexture() != null && !service.getVideoTexture().isReleased()) { + if (textureView.getSurfaceTexture() != service.getVideoTexture()) + textureView.setSurfaceTexture(service.getVideoTexture()); + } + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) { + if (videoPreviewVisible) showVideo(); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {} + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {} + + + @Override + public void onSubscribe(Flow.Subscription subscription) { + this.subscription = subscription; + subscription.request(10); + } + + @Override + public void onNext(Event e) { + if (e instanceof ModuleEvent) { + if (!provider.isOshStarted() && ((ModuleEvent) e).getType() == ModuleEvent.Type.LOADED) { + provider.setOshStarted(true); + requireActivity().runOnUiThread(this::updateFabIcon); + startRefreshingStatus(); + subscription.request(10); + return; + } + else if (e.getSource() instanceof AndroidSensorsDriver) { + provider.setAndroidSensors((AndroidSensorsDriver) e.getSource()); + } + else if (e.getSource() instanceof SOSTClient && ((ModuleEvent) e).getType() == ModuleEvent.Type.STATE_CHANGED) { + if (((ModuleEvent) e).getNewState() == org.sensorhub.api.module.ModuleEvent.ModuleState.INITIALIZING) { + provider.getSostClients().add((SOSTClient) e.getSource()); + } + } + else if (e.getSource() instanceof ConSysApiClientModule && ((ModuleEvent) e).getType() == ModuleEvent.Type.STATE_CHANGED) { + if (((ModuleEvent) e).getNewState() == org.sensorhub.api.module.ModuleEvent.ModuleState.INITIALIZING) { + provider.getConSysClients().add((ConSysApiClientModule) e.getSource()); + } + } + } + subscription.request(10); + } + + @Override + public void onError(Throwable throwable) {} + + @Override + public void onComplete() {} +} diff --git a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java index 8966a540..90739e0b 100644 --- a/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java +++ b/sensorhub-android-app/src/org/sensorhub/android/MainActivity.java @@ -14,11 +14,7 @@ package org.sensorhub.android; -import static android.content.ContentValues.TAG; - import android.Manifest; -import android.annotation.SuppressLint; -import android.app.Activity; import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -30,29 +26,31 @@ import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.graphics.SurfaceTexture; import android.location.LocationManager; import android.location.LocationProvider; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; -import android.os.PowerManager; import android.preference.PreferenceManager; import android.provider.Settings.Secure; -import android.text.Html; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; -import android.view.TextureView; import android.view.View; import android.view.WindowManager; import android.widget.EditText; -import android.widget.TextView; +import android.os.PowerManager; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import com.botts.impl.service.discovery.DiscoveryService; +import com.botts.impl.service.discovery.DiscoveryServiceConfig; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import net.opengis.swe.v20.DataBlock; @@ -63,35 +61,38 @@ import org.sensorhub.api.command.CommandData; import org.sensorhub.api.command.IStreamingControlInterface; import org.sensorhub.api.common.BigId; -import org.sensorhub.api.event.Event; import org.sensorhub.api.module.IModule; import org.sensorhub.api.module.IModuleConfigRepository; import org.sensorhub.api.module.ModuleConfig; -import org.sensorhub.api.module.ModuleEvent; import org.sensorhub.api.sensor.SensorConfig; import org.sensorhub.impl.client.sost.SOSTClient; -import org.sensorhub.impl.client.sost.SOSTClient.StreamInfo; import org.sensorhub.impl.client.sost.SOSTClientConfig; +import org.sensorhub.impl.module.ModuleRegistry; import org.sensorhub.impl.datastore.h2.MVObsSystemDatabaseConfig; import org.sensorhub.impl.datastore.view.ObsSystemDatabaseViewConfig; -import org.sensorhub.impl.event.EventBus; import org.sensorhub.impl.module.InMemoryConfigDb; import org.sensorhub.impl.module.ModuleClassFinder; -import org.sensorhub.impl.module.ModuleRegistry; import org.sensorhub.impl.sensor.android.AndroidSensorsConfig; import org.sensorhub.impl.sensor.android.AndroidSensorsDriver; import org.sensorhub.impl.sensor.android.audio.AudioEncoderConfig; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig; import org.sensorhub.impl.sensor.android.video.VideoEncoderConfig.VideoPreset; +import org.sensorhub.impl.sensor.controller.ControllerConfig; +import org.sensorhub.impl.sensor.controller.ControllerDriver; + +import android.view.KeyEvent; +import android.view.MotionEvent; import org.sensorhub.impl.sensor.kestrel.KestrelConfig; import org.sensorhub.impl.sensor.meshtastic.MeshtasticConfig; import org.sensorhub.impl.sensor.meshtastic.MeshtasticSensor; import org.sensorhub.impl.sensor.meshtastic.control.TextMessageControl; import org.sensorhub.impl.sensor.polar.PolarConfig; import org.sensorhub.impl.sensor.ste.STERadPagerConfig; +import org.sensorhub.impl.sensor.template.TemplateConfig; import org.sensorhub.impl.sensor.trupulse.SimulatedDataStream; import org.sensorhub.impl.sensor.trupulse.TruPulseConfig; import org.sensorhub.impl.sensor.trupulse.TruPulseWithGeolocConfig; +import org.sensorhub.impl.sensor.wardriving.WardrivingConfig; import org.sensorhub.impl.service.HttpServerConfig; import org.sensorhub.impl.service.consys.ConSysApiService; import org.sensorhub.impl.service.consys.ConSysApiServiceConfig; @@ -104,13 +105,16 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.security.cert.X509Certificate; -import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; import java.util.Collection; @@ -118,10 +122,9 @@ import java.util.Date; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.Flow; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.FutureTask; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; @@ -131,7 +134,7 @@ import javax.net.ssl.X509TrustManager; -public class MainActivity extends Activity implements TextureView.SurfaceTextureListener, Flow.Subscriber +public class MainActivity extends AppCompatActivity implements SensorHubServiceProvider { public static final String ACTION_BROADCAST_RECEIVER = "org.sensorhub.android.BROADCAST_RECEIVER"; public static final String ANDROID_SENSORS_MODULE_ID = "ANDROID_SENSORS"; @@ -139,14 +142,8 @@ public class MainActivity extends Activity implements TextureView.SurfaceTexture public static final Date TRUPULSE_SENSOR_LAST_UPDATED = ANDROID_SENSORS_LAST_UPDATED; private static final Logger log = LoggerFactory.getLogger(MainActivity.class); - TextView mainInfoArea; - TextView videoInfoArea; SensorHubService boundService; IModuleConfigRepository sensorhubConfig; - Handler displayHandler; - Runnable displayCallback; - StringBuffer mainInfoText = new StringBuffer(); - StringBuffer videoInfoText = new StringBuffer(); boolean oshStarted = false; ArrayList sostClients = new ArrayList<>(); ArrayList conSysClients = new ArrayList<>(); @@ -154,21 +151,13 @@ public class MainActivity extends Activity implements TextureView.SurfaceTexture URL url; AndroidSensorsDriver androidSensors; boolean showVideo; - URI clientUri = null; - URL clientURL = null; String deviceID; String runName; - private Flow.Subscription subscription; - Flow.Subscriber mainActivity = this; + private Fragment activeFragment; private BroadcastReceiver broadcastReceiver; - // Request codes for permissions - final int FINE_LOC_RC = 101; - final int CAMERA_RC = 102; - final int AUDIO_RC = 103; - enum Sensors { Android, TruPulse, @@ -180,16 +169,17 @@ enum Sensors { BLELocation, Meshtastic, PolarHRMonitor, - Kestrel + Kestrel, + Wardriving, + Controller, + Template } - private final ServiceConnection sConn = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { boundService = ((SensorHubService.LocalBinder) service).getService(); -// boundService.initSensorhub(); } public void onServiceDisconnected(ComponentName className) @@ -198,51 +188,76 @@ public void onServiceDisconnected(ComponentName className) } }; + // ==================== SensorHubServiceProvider ==================== - protected void updateConfig(SharedPreferences prefs, String runName) - { - deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); - sensorhubConfig = new InMemoryConfigDb(new ModuleClassFinder()); + @Override + public SensorHubService getBoundService() { return boundService; } - //get ip, port, user, password - String host = prefs.getString("ip_address", "").trim(); - String port = prefs.getString("port", "").trim(); - String user = prefs.getString("username", null); - String password = prefs.getString("password", null); - String endpointPath = prefs.getString("endpoint_path", null); + @Override + public boolean isOshStarted() { return oshStarted; } - Boolean isApiServiceEnabled = prefs.getBoolean("api_service", true); - Boolean isSosServiceEnabled = prefs.getBoolean("sos_service", true); - Boolean isClientEnabled = prefs.getBoolean("enable_client", true); - Boolean isTLSEnabled = prefs.getBoolean("enable_tls", false); + @Override + public void setOshStarted(boolean started) { this.oshStarted = started; } - if (host.isEmpty()) - host = "127.0.0.1"; + @Override + public IModuleConfigRepository getSensorhubConfig() { return sensorhubConfig; } - if (port.isEmpty()) - port = "8585"; + @Override + public ArrayList getSostClients() { return sostClients; } -// String sensorhubEndpoint = "/sensorhub"; + @Override + public ArrayList getConSysClients() { return conSysClients; } - String newUrl = (isTLSEnabled ? "https://" : "http://") + host + ":" + port + endpointPath; + @Override + public AndroidSensorsDriver getAndroidSensors() { return androidSensors; } - try { - clientUri = new URI(newUrl); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } + @Override + public void setAndroidSensors(AndroidSensorsDriver driver) { this.androidSensors = driver; } - try { - clientURL = clientUri.toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + @Override + public boolean getShowVideo() { return showVideo; } + + @Override + public void startSensorHub() { + if (boundService != null && sensorhubConfig != null) { + boundService.startSensorHub(sensorhubConfig, showVideo); } + } + + @Override + public void stopSensorHub() { + sostClients.clear(); + conSysClients.clear(); + if (boundService != null) + boundService.stopSensorHub(); + oshStarted = false; + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + // ==================== Config ==================== - // disable SSL check if requested - boolean disableSslCheck = prefs.getBoolean("sos_disable_ssl_check", false); + @Override + public void updateConfig(SharedPreferences prefs, String runName) + { + deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); + sensorhubConfig = new InMemoryConfigDb(new ModuleClassFinder()); + + Boolean isApiServiceEnabled = prefs.getBoolean("csapi_service", true); + Boolean isSosServiceEnabled = prefs.getBoolean("sos_service", true); + Boolean isDiscoveryServiceEnabled = prefs.getBoolean("discovery_service", false); + + ServerProfileRepository serverRepo = new ServerProfileRepository(this); + List enabledServers = serverRepo.getEnabled(); + + boolean disableSslCheck = false; + for (ServerProfile sp : enabledServers) { + if (sp.disableSslCheck) { + disableSslCheck = true; + break; + } + } if (disableSslCheck) { - // Create a trust manager that does not validate certificate chains TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { public java.security.cert.X509Certificate[] getAcceptedIssuers() { @@ -258,7 +273,6 @@ public void checkServerTrusted( } }; - // Install the all-trusting trust manager try { SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, new java.security.SecureRandom()); @@ -274,13 +288,7 @@ public boolean verify(String arg0, SSLSession arg1) { } } - // OAuth - Boolean isOAuthEnabled = prefs.getBoolean("o_auth_enabled", false); - String clientId = prefs.getString("client_id", "").trim(); - String tokenEndpoint = prefs.getString("token_endpoint", "").trim(); - String clientSecret = prefs.getString("client_secret", "").trim(); - // get device name String deviceID = Secure.getString(getContentResolver(), Secure.ANDROID_ID); String deviceName = prefs.getString("device_name", null); if (deviceName == null || deviceName.length() < 2) @@ -302,8 +310,6 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.activateNetworkLocation = prefs.getBoolean("netloc_enabled", false); sensorsConfig.enableCamera = prefs.getBoolean("cam_enabled", false); sensorsConfig.selectedCameraId = Integer.parseInt(prefs.getString("camera_select", "0")); - /*if (sensorsConfig.activateBackCamera || sensorsConfig.activateFrontCamera) - showVideo = true;*/ if (sensorsConfig.enableCamera) showVideo = true; @@ -311,34 +317,13 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.videoConfig.codec = prefs.getString("video_codec", VideoEncoderConfig.JPEG_CODEC); sensorsConfig.videoConfig.frameRate = Integer.parseInt(prefs.getString("video_framerate", "30")); - // selected preset or AUTO mode - String selectedPreset = prefs.getString("video_preset", "0"); - if ("AUTO".equals(selectedPreset)) { - sensorsConfig.videoConfig.autoPreset = true; - sensorsConfig.videoConfig.selectedPreset = 0; - } - else { - sensorsConfig.videoConfig.autoPreset = false; - sensorsConfig.videoConfig.selectedPreset = Integer.parseInt(selectedPreset); - } - - // video preset list - int resIdx = 1; - ArrayList presetList = new ArrayList<>(); - while (prefs.contains("video_size" + resIdx)) - { - String resString = prefs.getString("video_size" + resIdx, "Disabled"); - String[] tokens = resString.split("x"); - VideoPreset preset = new VideoPreset(); - preset.width = Integer.parseInt(tokens[0]); - preset.height = Integer.parseInt(tokens[1]); - preset.minBitrate = Integer.parseInt(prefs.getString("video_min_bitrate" + resIdx, "3000")); - preset.maxBitrate = Integer.parseInt(prefs.getString("video_max_bitrate" + resIdx, "3000")); - preset.selectedBitrate = preset.maxBitrate; - presetList.add(preset); - resIdx++; - } - sensorsConfig.videoConfig.presets = presetList.toArray(new VideoPreset[0]); + String resolutionStr = prefs.getString("video_resolution", "640x480"); + String[] resParts = resolutionStr.split("x"); + VideoPreset videoPreset = new VideoPreset(); + videoPreset.width = Integer.parseInt(resParts[0]); + videoPreset.height = Integer.parseInt(resParts[1]); + sensorsConfig.videoConfig.presets = new VideoPreset[]{videoPreset}; + sensorsConfig.videoConfig.selectedPreset = 0; sensorsConfig.outputVideoRoll = prefs.getBoolean("video_roll_enabled", false); @@ -351,16 +336,14 @@ public boolean verify(String arg0, SSLSession arg1) { sensorsConfig.runName = runName; sensorsConfig.uidExtension = prefs.getString("uid_extension", "0"); - - // START SOS Config ************************************************************************ - // Setup HTTPServerConfig for enabling more complete node functionality + // HTTP Server HttpServerConfig serverConfig = new HttpServerConfig(); serverConfig.proxyBaseUrl = ""; serverConfig.httpPort = 8585; serverConfig.autoStart = true; sensorhubConfig.add(serverConfig); - // We don't need android context unless we're doing IPC things + // SOS Service SOSServiceConfig sosConfig = new SOSServiceConfig(); sosConfig.moduleClass = SOSService.class.getCanonicalName(); sosConfig.id = "SOS_SERVICE"; @@ -369,69 +352,88 @@ public boolean verify(String arg0, SSLSession arg1) { sosConfig.enableTransactional = true; sosConfig.exposedResources = new ObsSystemDatabaseViewConfig(); - //Connected systems service + // Connected Systems Service ConSysApiServiceConfig conSysApiService = new ConSysApiServiceConfig(); conSysApiService.moduleClass = ConSysApiService.class.getCanonicalName(); conSysApiService.id = "CON_SYS_SERVICE"; - conSysApiService.name= "Connected Systems API Service"; + conSysApiService.name = "Connected Systems API Service"; conSysApiService.autoStart = true; conSysApiService.enableTransactional = true; conSysApiService.exposedResources = new ObsSystemDatabaseViewConfig(); - ConSysOAuthConfig conSysOAuthConfig = new ConSysOAuthConfig(); - conSysOAuthConfig.oAuthEnabled = isOAuthEnabled; - conSysOAuthConfig.tokenEndpoint = tokenEndpoint; - conSysOAuthConfig.clientID = clientId; - conSysOAuthConfig.clientSecret = clientSecret; - + // Discovery Service + DiscoveryServiceConfig discoveryServiceConfig = new DiscoveryServiceConfig(); + discoveryServiceConfig.moduleClass = DiscoveryService.class.getCanonicalName(); + discoveryServiceConfig.id = "DISCOVERY_SERVICE"; + discoveryServiceConfig.name= "Discovery Service"; + discoveryServiceConfig.autoStart = true; + + File outFile = new File(getApplicationContext().getFilesDir(), "rules.txt"); + String rulesLink = prefs.getString("rules_link", ""); + FutureTask downloadTask = new java.util.concurrent.FutureTask<>(() -> { + URL rulesUrl = new URL(rulesLink); + HttpURLConnection conn = (HttpURLConnection) rulesUrl.openConnection(); + conn.setInstanceFollowRedirects(true); + try (InputStream in = conn.getInputStream(); + OutputStream out = new FileOutputStream(outFile)) { + byte[] buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) > 0) { + out.write(buffer, 0, len); + } + } finally { + conn.disconnect(); + } + return null; + }); + new Thread(downloadTask).start(); + try { + downloadTask.get(); + } catch (Exception e) { + Log.e("OSH - Discovery", "Failed to download rules file", e); + } + discoveryServiceConfig.rulesFilePath = outFile.getAbsolutePath(); - // Push Sensors Config sensorhubConfig.add(sensorsConfig); - if (isPushingSensor(Sensors.Android)) { - if (isClientEnabled) { - System.out.println("Connected Systems Client enabled"); - addCSApiConfig(sensorsConfig, user, password, conSysOAuthConfig); + for (ServerProfile sp : enabledServers) { + URL profileUrl = sp.buildClientUrl(); + if (profileUrl == null) { + log.error("Skipping server profile '{}': invalid URL", sp.name); + continue; + } - } else { - System.out.println("SOST Client enabled"); - addSosTConfig(sensorsConfig, user, password); - } + String pwd = serverRepo.getPassword(sp.id); + if (sp.useConSysClient) { + ConSysOAuthConfig oAuthConfig = new ConSysOAuthConfig(); + oAuthConfig.oAuthEnabled = sp.oAuthEnabled; + oAuthConfig.tokenEndpoint = serverRepo.getOAuthTokenEndpoint(sp.id); + oAuthConfig.clientID = serverRepo.getOAuthClientId(sp.id); + oAuthConfig.clientSecret = serverRepo.getOAuthClientSecret(sp.id); + addCSApiConfig(sensorsConfig, sp, profileUrl, sp.username, pwd, oAuthConfig); + } else { + addSosTConfig(sensorsConfig, sp, profileUrl, sp.username, pwd); + } + } } - if(shouldStore(prefs)) { + if (shouldStore(prefs)) { File dbFile = new File(getApplicationContext().getFilesDir() + "/db/"); dbFile.mkdirs(); MVObsSystemDatabaseConfig basicStorageConfig = new MVObsSystemDatabaseConfig(); basicStorageConfig.moduleClass = "org.sensorhub.impl.persistence.h2.MVObsStorageImpl"; basicStorageConfig.storagePath = dbFile.getAbsolutePath() + "/${STORAGE_ID}.dat"; basicStorageConfig.autoStart = true; - -// sosConfig.newStorageConfig = basicStorageConfig; - -// StreamStorageConfig androidStreamStorageConfig = createStreamStorageConfig(androidSensorsConfig); -// addStorageConfig(androidSensorsConfig, androidStreamStorageConfig); - - /* File dbFile = new File(getApplicationContext().getFilesDir() + "/db/"); - dbFile.mkdirs(); - - MVStorageConfig storageConfig = new MVStorageConfig(); - storageConfig.setStorageIdentifier("OSH_CONNECT_OBS");*/ } -// SensorDataProviderConfig androidDataProviderConfig = createDataProviderConfig(androidSensorsConfig); -// addSosServerConfig(sosConfig, androidDataProviderConfig); - // END SOS CONFIG ************************************************************************** - // TruPulse sensor boolean enabled = prefs.getBoolean("trupulse_enabled", false); if (enabled) { TruPulseConfig trupulseConfig = new TruPulseConfig(); - // add target geolocation processing if GPS is enabled if (sensorsConfig.activateGpsLocation) { String gpsOutputName = null; @@ -452,7 +454,6 @@ public boolean verify(String arg0, SSLSession arg1) { ((TruPulseWithGeolocConfig)trupulseConfig).locationOutputName = gpsOutputName; } - trupulseConfig.id = "TRUPULSE_SENSOR"; trupulseConfig.name = "TruPulse Range Finder [" + deviceName + "]"; trupulseConfig.autoStart = true; @@ -462,30 +463,26 @@ public boolean verify(String arg0, SSLSession arg1) { btConf.protocol.deviceName = prefs.getString("trupulse_device_address", ""); if (prefs.getBoolean("trupulse_simu", false)) btConf.moduleClass = SimulatedDataStream.class.getCanonicalName(); - else{ + else { btConf.moduleClass = BluetoothCommProvider.class.getCanonicalName(); trupulseConfig.connection.connectTimeout = 100000; trupulseConfig.connection.reconnectAttempts = 10; } trupulseConfig.commSettings = btConf; - - sensorhubConfig.add(trupulseConfig); } // STE Rad Pager sensor enabled = prefs.getBoolean("ste_radpager_enabled", false); - if(enabled){ + if (enabled) { STERadPagerConfig steRadPagerConfig = new STERadPagerConfig(); steRadPagerConfig.id = "STE_RADPAGER_SENSOR"; steRadPagerConfig.name = "STE Rad Pager [" + deviceName + "]"; steRadPagerConfig.autoStart = true; steRadPagerConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; - sensorhubConfig.add(steRadPagerConfig); } - // Meshtastic Sensor enabled = prefs.getBoolean("meshtastic_enabled", false); if (enabled) @@ -497,12 +494,10 @@ public boolean verify(String arg0, SSLSession arg1) { meshtasticConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; meshtasticConfig.device_name = prefs.getString("meshtastic_device_address", ""); meshtasticConfig.uid_extension = prefs.getString("uid_extension", ""); - - sensorhubConfig.add(meshtasticConfig); } - // polar heart Sensor + // Polar heart Sensor enabled = prefs.getBoolean("polar_enabled", false); if (enabled) { PolarConfig polarConfig = new PolarConfig(); @@ -512,13 +507,10 @@ public boolean verify(String arg0, SSLSession arg1) { polarConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; polarConfig.device_name = prefs.getString("polar_device_address", ""); polarConfig.uid_extension = prefs.getString("uid_extension", ""); - - sensorhubConfig.add(polarConfig); } -// // Kestrel Weather - + // Kestrel Weather enabled = prefs.getBoolean("kestrel_enabled", false); if (enabled) { BleConfig bleConf = new BleConfig(); @@ -535,102 +527,68 @@ public boolean verify(String arg0, SSLSession arg1) { kestrelConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; kestrelConfig.networkID = bleConf.id; kestrelConfig.deviceAddress = prefs.getString("kestrel_device_address", ""); - sensorhubConfig.add(kestrelConfig); } - // FLIR One Edge sensor -// enabled = prefs.getBoolean("flirone_enabled", false); -// if (enabled) -// { -// -// // perhaps do a wireless comm module -// FlirOneConfig flironeConfig = new FlirOneConfig(); -// flironeConfig.id = "FLIRONE_EDGE_SENSOR"; -// flironeConfig.name = "FLIR One Thermal Edge Camera [" + deviceName + "]"; -// flironeConfig.autoStart = true; -//// flironeConfig.androidContext = this.getApplicationContext(); -//// flironeConfig.camPreviewTexture = boundService.getVideoTexture(); -// showVideo = true; -// sensorhubConfig.add(flironeConfig); -// } - - // AngelSensor -// enabled = prefs.getBoolean("angel_enabled", false); -// if (enabled) -// { -// BleConfig bleConf = new BleConfig(); -// bleConf.id = "BLE"; -// bleConf.moduleClass = BleNetwork.class.getCanonicalName(); -// bleConf.androidContext = this.getApplicationContext(); -// bleConf.autoStart = true; -// sensorhubConfig.add(bleConf); -// -// AngelSensorConfig angelConfig = new AngelSensorConfig(); -// angelConfig.id = "ANGEL_SENSOR"; -// angelConfig.name = "Angel Sensor [" + deviceName + "]"; -// angelConfig.autoStart = true; -// angelConfig.networkID = bleConf.id; -// //angelConfig.btAddress = "00:07:80:79:04:AF"; // mike -// //angelConfig.btAddress = "00:07:80:03:0E:0A"; // alex -// angelConfig.btAddress = prefs.getString("angel_address", null); -// sensorhubConfig.add(angelConfig); - -/** - // FLIR One sensor - enabled = prefs.getBoolean("flirone_enabled", false); - if (enabled) - { - FlirOneCameraConfig flironeConfig = new FlirOneCameraConfig(); - flironeConfig.id = "FLIRONE_SENSOR"; - flironeConfig.name = "FLIR One Camera [" + deviceName + "]"; - flironeConfig.autoStart = true; - flironeConfig.androidContext = this.getApplicationContext(); - flironeConfig.camPreviewTexture = boundService.getVideoTexture(); - showVideo = true; - sensorhubConfig.add(flironeConfig); - addSosTConfig(flironeConfig, sosUser, sosPwd); + // Wardriving + enabled = prefs.getBoolean("wardriving_enabled", false); + if (enabled) { + WardrivingConfig wardrivingConfig = new WardrivingConfig(); + wardrivingConfig.id = "WARDRIVING_"; + wardrivingConfig.name = "Wardriving [" + deviceName + "]"; + wardrivingConfig.autoStart = true; + wardrivingConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; + wardrivingConfig.uid_extension = prefs.getString("uid_extension", ""); + sensorhubConfig.add(wardrivingConfig); } - // DJI Drone - /*enabled = prefs.getBoolean("dji_enabled", false); - if (enabled) - { - DjiConfig djiConfig = new DjiConfig(); - djiConfig.id = "DJI_DRONE"; - djiConfig.name = "DJI Aircraft [" + deviceName + "]"; - djiConfig.autoStart = true; - djiConfig.androidContext = this.getApplicationContext(); - djiConfig.camPreviewTexture = boundService.getVideoTexture(); - showVideo = true; - sensorhubConfig.add(djiConfig); - addSosTConfig(djiConfig, sosUser, sosPwd); - }*/ + // USB Controller + enabled = prefs.getBoolean("controller_enabled", false); + if (enabled) { + ControllerConfig controllerConfig = new ControllerConfig(); + controllerConfig.id = "CONTROLLER"; + controllerConfig.name = "Controller [" + deviceName + "]"; + controllerConfig.autoStart = true; + controllerConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; + controllerConfig.uid_extension = prefs.getString("uid_extension", ""); + sensorhubConfig.add(controllerConfig); + } + + // Template Driver + enabled = prefs.getBoolean("template_enabled", false); + if (enabled) { + TemplateConfig templateConfig = new TemplateConfig(); + templateConfig.id = "TEMPLATE_DRIVER_"; + templateConfig.name = "Template [" + deviceName + "]"; + templateConfig.autoStart = true; + templateConfig.lastUpdated = ANDROID_SENSORS_LAST_UPDATED; + templateConfig.uid_extension = prefs.getString("uid_extension", ""); + sensorhubConfig.add(templateConfig); + } - if(isApiServiceEnabled){ - // add connected sys service - System.out.println("Connected Systems Service enabled"); + //---------- SERVICES --------------------- + if (isApiServiceEnabled) { sensorhubConfig.add(conSysApiService); } - if(isSosServiceEnabled){ - // add sos service - System.out.println("SOS Service enabled"); + if (isSosServiceEnabled) { sensorhubConfig.add(sosConfig); } + if (isDiscoveryServiceEnabled) { + sensorhubConfig.add(discoveryServiceConfig); + } + } - protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) + protected void addSosTConfig(SensorConfig sensorConf, ServerProfile profile, URL serverUrl, String user, String pwd) { - if (clientURL == null) - return; SOSTClientConfig sosConfig = new SOSTClientConfig(); - sosConfig.id = sensorConf.id + "_SOST"; - sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "");// + "SOS-T Client"; + sosConfig.id = sensorConf.id + "_SOST_" + profile.id; + sosConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; sosConfig.autoStart = true; - sosConfig.sos.remoteHost = clientURL.getHost(); - sosConfig.sos.remotePort = clientURL.getPort() < 0 ? clientURL.getDefaultPort() : clientURL.getPort(); - sosConfig.sos.resourcePath = clientURL.getPath(); - sosConfig.sos.enableTLS = clientURL.getProtocol().equals("https"); + sosConfig.sos.remoteHost = serverUrl.getHost(); + sosConfig.sos.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); + sosConfig.sos.resourcePath = serverUrl.getPath(); + sosConfig.sos.enableTLS = serverUrl.getProtocol().equals("https"); sosConfig.sos.user = user; sosConfig.sos.password = pwd; sosConfig.connection.connectTimeout = 10000; @@ -641,168 +599,153 @@ protected void addSosTConfig(SensorConfig sensorConf, String user, String pwd) sensorhubConfig.add(sosConfig); } - protected void addCSApiConfig(SensorConfig sensorConf, String apiUser, String apiPwd, ConSysOAuthConfig oAuthConfig) + protected void addCSApiConfig(SensorConfig sensorConf, ServerProfile profile, URL serverUrl, String apiUser, String apiPwd, ConSysOAuthConfig oAuthConfig) { - if (clientURL == null) - return; - ConSysApiClientConfig consysConfig = new ConSysApiClientConfig(); - consysConfig.id = sensorConf.id + "_CONSYS"; - consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", ""); + consysConfig.id = sensorConf.id + "_CONSYS_" + profile.id; + consysConfig.name = sensorConf.name.replaceAll("\\[.*\\]", "") + " -> " + profile.name; consysConfig.autoStart = true; - consysConfig.conSys.remoteHost = clientURL.getHost(); - consysConfig.conSys.remotePort = clientURL.getPort() < 0 ? clientURL.getDefaultPort() : clientURL.getPort(); - consysConfig.conSys.resourcePath = clientURL.getPath(); - consysConfig.conSys.enableTLS = clientURL.getProtocol().equals("https"); + consysConfig.conSys.remoteHost = serverUrl.getHost(); + consysConfig.conSys.remotePort = serverUrl.getPort() < 0 ? serverUrl.getDefaultPort() : serverUrl.getPort(); + consysConfig.conSys.resourcePath = serverUrl.getPath(); + consysConfig.conSys.enableTLS = serverUrl.getProtocol().equals("https"); consysConfig.conSys.user = apiUser; consysConfig.conSys.password = apiPwd; consysConfig.connection.connectTimeout = 10000; consysConfig.connection.reconnectAttempts = 9; consysConfig.httpClientImplClass = OkHttpClientWrapper.class.getCanonicalName(); - consysConfig.dataSourceSelector = new ObsSystemDatabaseViewConfig(); - consysConfig.conSysOAuth = oAuthConfig; - sensorhubConfig.add(consysConfig); } - private void requestBatteryOptimizationExemption() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (!pm.isIgnoringBatteryOptimizations(getPackageName())) { - Intent intent = new Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); - intent.setData(Uri.parse("package:" + getPackageName())); - startActivity(intent); - } - } - - } - @SuppressLint("HandlerLeak") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); - mainInfoArea = findViewById(R.id.main_info); - videoInfoArea = findViewById(R.id.video_info); - // listen to texture view lifecycle - TextureView textureView = findViewById(R.id.video); - textureView.setSurfaceTextureListener(this); + MaterialToolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); + } + Fragment homeFragment = new DashboardFragment(); + Fragment sensorsFragment = new SensorsFragment(); + Fragment settingsFragment = new SettingsFragment(); - hasBluetoothPermissions(); + getSupportFragmentManager().beginTransaction() + .add(R.id.flFragment, homeFragment, "dashboard") + .add(R.id.flFragment, sensorsFragment, "sensors") + .add(R.id.flFragment, settingsFragment, "settings") + .hide(sensorsFragment) + .hide(settingsFragment) + .commit(); + activeFragment = homeFragment; + + BottomNavigationView bottomNav = findViewById(R.id.bottom_nav); + + bottomNav.setOnNavigationItemSelectedListener(item -> { + switch (item.getItemId()) { + case R.id.dashboard: + switchFragment(homeFragment); + break; + case R.id.sensors: + switchFragment(sensorsFragment); + break; + case R.id.settings: + switchFragment(settingsFragment); + break; + } + return true; + }); + + bottomNav.setSelectedItemId(R.id.dashboard); + + hasBluetoothPermissions(); checkForPermissions(); + // bind to SensorHub service Intent intent = new Intent(this, SensorHubService.class); - startService(intent); // ADD THIS LINE + startService(intent); bindService(intent, sConn, Context.BIND_AUTO_CREATE); - // handler to refresh sensor status in UI - displayHandler = new Handler(Looper.getMainLooper()); - setupBroadcastReceivers(); requestBatteryOptimizationExemption(); - - // Due to changes with OSH, it may be best to create and start the hub immediately - // This allows us access to the module registry created by default -// boundService.initSensorhub(); } - private boolean hasBluetoothPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return checkSelfPermission(Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED - && checkSelfPermission(Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED; - } - return true; // Older versions handled by existing checks + private void switchFragment(Fragment fragment) { + if (fragment == activeFragment) return; + getSupportFragmentManager() + .beginTransaction() + .hide(activeFragment) + .show(fragment) + .commit(); + activeFragment = fragment; } - Menu optionsMenu; - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); - optionsMenu = menu; - return true; } - @Override public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); - if (id == R.id.action_settings) - { - startActivity(new Intent(this, UserSettingsActivity.class)); - return true; - } - else if (id == R.id.action_start) - { - if (boundService != null && boundService.getSensorHub() == null) - showRunNamePopup(); - return true; - } - else if (id == R.id.action_stop) + if (id == R.id.action_about) { - stopListeningForEvents(); - stopRefreshingStatus(); - sostClients.clear(); - conSysClients.clear(); - if (boundService != null) - boundService.stopSensorHub(); - mainInfoArea.setBackgroundColor(0xFFFFFFFF); - oshStarted = false; - newStatusMessage("SensorHub Stopped"); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + showAboutPopup(); return true; } - else if (id == R.id.action_about) + else if (id == R.id.action_meshtastic) { - showAboutPopup(); - } - else if (id == R.id.action_meshtastic) { - showMeshtasticDialog(); + return true; } - else if(id == R.id.action_status) - { + else if(id == R.id.action_status) { Intent statusIntent = new Intent(this, AppStatusActivity.class); - if(boundService.sensorhub != null) { + + if (boundService != null && boundService.sensorhub != null) { ModuleRegistry moduleRegistry = boundService.sensorhub.getModuleRegistry(); Collection> modules = moduleRegistry.getLoadedModules(); for (IModule module : modules) { var moduleConf = module.getConfiguration(); - String status = module.getCurrentState().name(); - - switch (((ModuleConfig) moduleConf).id) { - case "HTTP_SERVER_0": - statusIntent.putExtra("httpStatus", status); - break; - case "SOS_SERVICE": - statusIntent.putExtra("sosService", status); - break; - case "CON_SYS_SERVICE": - statusIntent.putExtra("conSysService", status); - break; - case "ANDROID_SENSORS": - statusIntent.putExtra("androidSensorStatus", status); - break; - case "ANDROID_SENSORS#storage": - statusIntent.putExtra("sensorStorageStatus", status); - break; + + if (moduleConf instanceof ModuleConfig) { + String status = module.getCurrentState().name(); + String moduleId = ((ModuleConfig) moduleConf).id; + + switch (moduleId) { + case "HTTP_SERVER_0": + statusIntent.putExtra("httpStatus", status); + break; + case "SOS_SERVICE": + statusIntent.putExtra("sosService", status); + break; + case "CON_SYS_SERVICE": + statusIntent.putExtra("conSysService", status); + break; + case "DISCOVERY_SERVICE": + statusIntent.putExtra("discoveryService", status); + break; + case "ANDROID_SENSORS": + statusIntent.putExtra("androidSensorStatus", status); + break; + case "ANDROID_SENSORS#storage": + statusIntent.putExtra("sensorStorageStatus", status); + break; + } } } - } - else { + } else { statusIntent.putExtra("sosService", "N/A"); statusIntent.putExtra("conSysService", "N/A"); statusIntent.putExtra("httpStatus", "N/A"); @@ -810,31 +753,65 @@ else if(id == R.id.action_status) statusIntent.putExtra("sensorStorageStatus", "N/A"); } -// statusIntent.putExtra("boundService", boundService); - - startActivity(statusIntent); return true; } - return super.onOptionsItemSelected(item); } - protected void showMeshtasticDialog() { + @Override + protected void onDestroy() + { + if (broadcastReceiver != null) { + unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + if (boundService != null) { + unbindService(sConn); + boundService = null; + } + super.onDestroy(); + } + + private ControllerDriver getControllerDriver() { + if (boundService == null || boundService.sensorhub == null) + return null; + try { + return boundService.sensorhub.getModuleRegistry().getModuleByType(ControllerDriver.class); + } catch (Exception e) { + return null; + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + ControllerDriver controller = getControllerDriver(); + if (controller != null && controller.onKeyEvent(event)) + return true; + return super.dispatchKeyEvent(event); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + ControllerDriver controller = getControllerDriver(); + if (controller != null && controller.onMotionEvent(event)) + return true; + return super.dispatchGenericMotionEvent(event); + } + + protected void showMeshtasticDialog() { LayoutInflater inflater = getLayoutInflater(); View dialogView = inflater.inflate(R.layout.dialog_meshtastic, null); EditText messageInput = dialogView.findViewById(R.id.msg_input); + EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle("Send Meshtastic Message"); builder.setView(dialogView); - EditText destinationIdText = dialogView.findViewById(R.id.destination_nodeId); - - builder.setPositiveButton("Send", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { String msg = messageInput.getText().toString(); @@ -847,7 +824,6 @@ public void onClick(DialogInterface dialog, int id) { } }); - builder.setNegativeButton("Cancel", null); builder.show(); } @@ -869,84 +845,16 @@ private void sendMeshtasticMessage(String message, String nodeId) throws IOExcep .build(); textMessageControl.submitCommand(cmd); - } - - protected synchronized void showRunNamePopup() { - AlertDialog.Builder alert = new AlertDialog.Builder(this); - - alert.setTitle("Run Name"); - alert.setMessage("Please enter the name for this run"); - - // Set an EditText view to get user input - final EditText input = new EditText(this); - input.getText().append("Run-"); - SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd-HHmmss", Locale.US); - input.getText().append(formatter.format(new Date())); - alert.setView(input); - - alert.setPositiveButton("Ok", new DialogInterface.OnClickListener() - { - public void onClick(DialogInterface dialog, int whichButton) - { - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - String runName = input.getText().toString(); - - - updateConfig(PreferenceManager.getDefaultSharedPreferences(MainActivity.this), runName); - - AndroidSensorsConfig androidSensorConfig = (AndroidSensorsConfig) sensorhubConfig.get("ANDROID_SENSORS"); - VideoEncoderConfig videoConfig = androidSensorConfig.videoConfig; - - boolean cameraInUse = (androidSensorConfig.activateBackCamera || androidSensorConfig.activateFrontCamera); - boolean improperVideoSettings = (videoConfig.selectedPreset < 0 || videoConfig.selectedPreset >= videoConfig.presets.length); - - if (cameraInUse && improperVideoSettings) { - showVideoConfigErrorPopup(); - newStatusMessage("Video Config Error: Check Settings"); - } else { - newStatusMessage("Starting SensorHub..."); - sostClients.clear(); - conSysClients.clear(); - boundService.startSensorHub(sensorhubConfig, showVideo); - - if (boundService.hasVideo()) - mainInfoArea.setBackgroundColor(0x80FFFFFF); - - while(boundService.getSensorHub() == null){ - System.out.println("Waiting for BoundService Hub to start..."); - } - System.out.println("BoundService SensorHub Started..."); - while(boundService.getSensorHub().getEventBus() == null){ - System.out.println("Waiting for BoundService Hub EventBus to start..."); - } - System.out.println("BoundService SensorHub EventBus Started..."); - EventBus shEvtBus = (EventBus) boundService.getSensorHub().getEventBus(); - - shEvtBus.newSubscription() - .withTopicID(ModuleRegistry.EVENT_GROUP_ID) - .subscribe(mainActivity); - } - - } - }); - - alert.setNegativeButton("Cancel", (dialog, whichButton) -> { - }); - - alert.show(); } - protected void showAboutPopup() { String version = "?"; - try - { + try { PackageInfo pInfo = this.getPackageManager().getPackageInfo(getPackageName(), 0); version = pInfo.versionName; - } - catch (PackageManager.NameNotFoundException e) - { + } catch (PackageManager.NameNotFoundException e) { + log.warn("Could not retrieve package version", e); } String message = "A software platform for building smart sensor networks and the Internet of Things\n\n"; @@ -959,274 +867,8 @@ protected void showAboutPopup() { alert.show(); } - protected void showVideoConfigErrorPopup() { - String message = "Check Video Settings and ensure the resolution for the selected preset has been set."; - - AlertDialog.Builder alert = new AlertDialog.Builder(this); - alert.setTitle("OpenSensorHub"); - alert.setMessage(message); - alert.setPositiveButton("OK", (dialog, id) -> { - // user accepted - }); - alert.show(); - } - - - protected void startRefreshingStatus() { - if (displayCallback != null) - return; - - // handler to display async messages in UI - displayCallback = new Runnable() - { - public void run() - { - displayStatus(); - mainInfoArea.setText(Html.fromHtml(mainInfoText.toString())); - videoInfoArea.setText(Html.fromHtml(videoInfoText.toString())); - displayHandler.postDelayed(this, 1000); - } - }; - - displayHandler.post(displayCallback); - } - - - protected void stopRefreshingStatus() - { - if (displayCallback != null) - { - displayHandler.removeCallbacks(displayCallback); - displayCallback = null; - } - } - - - protected synchronized void displayStatus() { - - boolean needsRestart = false; - - mainInfoText.setLength(0); - - // first display error messages if any - for (SOSTClient client: sostClients) - { - Map dataStreams = client.getDataStreams(); - boolean showError = (client.getCurrentError() != null); - boolean showMsg = (dataStreams.isEmpty()) && (client.getStatusMessage() != null); - - if (showError || showMsg) - { - mainInfoText.append("

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

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

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

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

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

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

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

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

"); - - // Notify we are running when no data is being pushed - boolean serveOrStore = shouldServe(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)) || shouldStore(PreferenceManager.getDefaultSharedPreferences(MainActivity.this)); - if(sostClients.isEmpty() && serveOrStore){ - mainInfoText.append("No Sensors Set to Push Remotely"); - } - - if(conSysClients.isEmpty() && serveOrStore){ - mainInfoText.append("No Sensors Set to Push Remotely"); - } - - // show video info - if (androidSensors != null && boundService.hasVideo()) - { -// TODO: Fix crash resulting from this (620) - try { - VideoEncoderConfig config = androidSensors.getConfiguration().videoConfig; - VideoPreset preset = config.presets[config.selectedPreset]; - videoInfoText.setLength(0); - videoInfoText.append("") - .append(config.codec).append(", ") - .append(preset.width).append("x").append(preset.height).append(", ") - .append(config.frameRate).append(" fps, ") - .append(preset.selectedBitrate).append(" kbits/s") - .append(""); - }catch (Exception e){ - log.error("Exception thrown trying to disaply video", e.getMessage()); - } - } - - } - - protected synchronized void newStatusMessage(String msg) - { - mainInfoText.setLength(0); - appendStatusMessage(msg); - } - - - protected synchronized void appendStatusMessage(String msg) - { - mainInfoText.append(msg); - - displayHandler.post(new Runnable() - { - public void run() - { - mainInfoArea.setText(mainInfoText.toString()); - } - }); - } - - - protected void startListeningForEvents() { - if (boundService == null || boundService.getSensorHub() == null){ - - } - - // TODO: Implement a listener that can sub to the status of the hub -// boundService.getSensorHub().getModuleRegistry().registerListener(this); - - } - - - protected void stopListeningForEvents() - { - if (boundService == null || boundService.getSensorHub() == null){ - - } - - // TODO: Unsub the listener here -// boundService.getSensorHub().getModuleRegistry().unregisterListener(this); - } - - - - protected void showVideo() - { - if (boundService.getVideoTexture() != null) - { - TextureView textureView = (TextureView) findViewById(R.id.video); - if (textureView.getSurfaceTexture() != boundService.getVideoTexture()) - textureView.setSurfaceTexture(boundService.getVideoTexture()); - } - } - - - protected void hideVideo() - { - } - - private boolean isPushingSensor(Sensors sensor) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); + boolean isPushingSensor(Sensors sensor) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (Sensors.Android.equals(sensor)) { if (prefs.getBoolean("accel_enabled", false) @@ -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> getModuleClass() + { + return ControllerDriver.class; + } + + + @Override + public Class getModuleConfigClass() + { + return ControllerConfig.class; + } + +} diff --git a/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..3bd1f5f8 --- /dev/null +++ b/sensorhub-android-controller/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.controller.Descriptor \ No newline at end of file diff --git a/sensorhub-android-controller/src/test/java/empty b/sensorhub-android-controller/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-controller/src/test/resources/empty b/sensorhub-android-controller/src/test/resources/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-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> getModuleClass() + { + return Sensor.class; + } + + + @Override + public Class getModuleConfigClass() + { + return TemplateConfig.class; + } + +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java new file mode 100644 index 00000000..3aa99130 --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Output.java @@ -0,0 +1,101 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for template WiFi access point scan results + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Output extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "wifiScan"; + private static final String SENSOR_OUTPUT_LABEL = "WiFi Access Point Scan"; + private static final Logger logger = LoggerFactory.getLogger(Output.class); + + protected Output(Sensor parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("WifiScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("text", fac.createText() + .label("Text") + .definition(SWEHelper.getPropertyUri("Text")) + .description("Example text field") + .build()) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + + public void setData(String text) { + + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, text); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java new file mode 100644 index 00000000..c094d75a --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/Sensor.java @@ -0,0 +1,118 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; + +import net.opengis.sensorml.v20.PhysicalComponent; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.SensorMLBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Sensor extends AbstractSensorModule { + static final String UID_PREFIX = "urn:osh:sensor:template:driver:"; + + private final ArrayList smlComponents; + private final SensorMLBuilder smlBuilder; + static final Logger logger = LoggerFactory.getLogger(Sensor.class.getSimpleName()); + private Context context; + Output output; + private HandlerThread eventThread; + private Handler eventHandler; + Thread processingThread; + volatile boolean doProcessing = true; + + + public Sensor() { + this.smlComponents = new ArrayList(); + this.smlBuilder = new SensorMLBuilder(); + } + + @Override + public void doInit() { + logger.info("Initializing Sensor"); + this.xmlID = "TEMPLATE_DRIVER_" + Build.SERIAL; + this.uniqueID = UID_PREFIX + config.getUidWithExt(); + + context = SensorHubService.getContext(); + + output = new Output(this); + output.doInit(); + addOutput(output, false); + + } + + @Override + public void doStart() throws SensorException { + eventThread = new HandlerThread("TemplateThread"); + eventThread.start(); + eventHandler = new Handler(eventThread.getLooper()); + + startProcessing(); + } + + public void startProcessing() { + doProcessing = true; + + processingThread = new Thread(() -> { + while (doProcessing) { + output.setData( "Sample Data"); + + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + }); + processingThread.start(); + } + + public void stopProcessing() { + doProcessing = false; + } + + @Override + public void doStop() { + + if (eventThread != null) { + eventThread.quitSafely(); + eventThread = null; + } + + eventHandler = null; + logger.info("Sensor stopped"); + } + + @Override + public boolean isConnected() { + return processingThread != null && processingThread.isAlive(); + } +} diff --git a/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java new file mode 100644 index 00000000..ed6d5126 --- /dev/null +++ b/sensorhub-android-template/src/main/java/org/sensorhub/impl/sensor/template/TemplateConfig.java @@ -0,0 +1,51 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.template; + +import org.sensorhub.android.SensorHubService; + +import android.content.Context; +import android.provider.Settings; + + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class TemplateConfig extends org.sensorhub.api.sensor.SensorConfig +{ + + public TemplateConfig() + { + this.moduleClass = Sensor.class.getCanonicalName(); + } + public String uid_extension; + + public long scanIntervalMs = 10000; + + public static String getUid() { + Context context = SensorHubService.getContext(); + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public String getUidWithExt() + { + String baseUid = getUid(); + if (uid_extension != null && !uid_extension.isEmpty()) + return baseUid + ":" + uid_extension; + return baseUid; + } +} diff --git a/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..924ba4ab --- /dev/null +++ b/sensorhub-android-template/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.template.Descriptor \ No newline at end of file diff --git a/sensorhub-android-template/src/test/java/empty b/sensorhub-android-template/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-template/src/test/resources/empty b/sensorhub-android-template/src/test/resources/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-wardriving/AndroidManifest.xml b/sensorhub-android-wardriving/AndroidManifest.xml new file mode 100644 index 00000000..d2a3abe1 --- /dev/null +++ b/sensorhub-android-wardriving/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/sensorhub-android-wardriving/README.md b/sensorhub-android-wardriving/README.md new file mode 100644 index 00000000..0bb1e3a4 --- /dev/null +++ b/sensorhub-android-wardriving/README.md @@ -0,0 +1,27 @@ +# Wardriving WiFi and BLE Scan Driver + +OpenSensorHub driver that performs wardriving by scanning for nearby WiFi access points and Bluetooth Low Energy (BLE) devices, and device's GPS location at the time of scan. + +## Outputs + +### WiFi Scan +Each observation captures a single access point: +- **BSSID** - MAC address of the access point +- **SSID** - Network name (empty for hidden networks) +- **RSSI** - Signal strength in dBm +- **Frequency** - Channel frequency in MHz +- **Capabilities** - Security/encryption schemes (e.g. WPA2, WPA3) +- **Location** - GPS lat/lon/alt of the device at scan time + +### BLE Scan +Each observation captures a single BLE device: +- **Device Address** - MAC address of the BLE device +- **Device Name** - Advertised name (if available) +- **RSSI** - Signal strength in dBm +- **Location** - GPS lat/lon/alt of the device at scan time + +## Setup +1. Enable the wardriving sensor in the osh-android app sensors tab +2. Ensure WiFi and Bluetooth are enabled on the device +3. Grant location and nearby device permissions if prompted +4. The driver begins periodic WiFi scans and continuous BLE scanning automatically \ No newline at end of file diff --git a/sensorhub-android-wardriving/build.gradle b/sensorhub-android-wardriving/build.gradle new file mode 100644 index 00000000..ef5842a0 --- /dev/null +++ b/sensorhub-android-wardriving/build.gradle @@ -0,0 +1,47 @@ +apply plugin: 'com.android.library' + +description = 'Wardriving' +ext.details = 'Driver for scanning and logging wireless networks' +version = '1.0.0' + +dependencies { + //api 'org.sensorhub:sensorhub-core:' + oshCoreVersion + api project(':sensorhub-core') + api project(':sensorhub-android-service') + + implementation 'io.reactivex.rxjava3:rxjava:3.1.6' + implementation 'io.reactivex.rxjava3:rxandroid:3.0.2' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation project(path: ':sensorhub-driver-android') + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.core:core:1.5.0' +} +configurations.configureEach { + exclude group: "ch.qos.logback" +} + +android { + namespace 'org.sensorhub.impl.sensor.wardriving' + compileSdkVersion rootProject.compileSdkVersion + buildToolsVersion rootProject.buildToolsVersion + + defaultConfig { + minSdkVersion rootProject.minSdkVersion + targetSdkVersion rootProject.targetSdkVersion + } + + lintOptions { + abortOnError false + } + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = ['src/main/java'] + resources.srcDirs = ['src/main/resources'] + res.srcDirs = ['res'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java new file mode 100644 index 00000000..dd458d8b --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/BLEOutput.java @@ -0,0 +1,116 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for BLE device scan results. + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class BLEOutput extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "bleScan"; + private static final String SENSOR_OUTPUT_LABEL = "BLE Device Scan"; + private static final Logger logger = LoggerFactory.getLogger(BLEOutput.class); + + protected BLEOutput(Wardriving parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("BLEScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("deviceAddress", fac.createText() + .label("Device Address") + .definition(SWEHelper.getPropertyUri("NetworkAddress")) + .description("MAC address of the BLE device") + .build()) + .addField("deviceName", fac.createText() + .label("Device Name") + .definition(SWEHelper.getPropertyUri("DeviceName")) + .description("Advertised name of the BLE device") + .build()) + .addField("rssi", fac.createQuantity() + .label("Signal Strength") + .definition(SWEHelper.getPropertyUri("SignalStrength")) + .description("Received signal strength indicator") + .build()) + .addField("location", fac.newLocationVectorLLA( + SWEHelper.getPropertyUri("SensorLocation"))) + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + public void setData(String deviceAddress, String deviceName, int rssi, double lat, double lon, double alt) { + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, deviceAddress != null ? deviceAddress : ""); + dataBlock.setStringValue(idx++, deviceName != null ? deviceName : ""); + dataBlock.setIntValue(idx++, rssi); + dataBlock.setDoubleValue(idx++, lat); + dataBlock.setDoubleValue(idx++, lon); + dataBlock.setDoubleValue(idx++, alt); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java new file mode 100644 index 00000000..f666c9ee --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Descriptor.java @@ -0,0 +1,76 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import org.sensorhub.api.module.IModule; +import org.sensorhub.api.module.IModuleProvider; +import org.sensorhub.api.module.ModuleConfig; + + +/** + *

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

+ * + * @author Alex Robin + * @since Sep 7, 2013 + */ +public class Descriptor implements IModuleProvider +{ + + @Override + public String getModuleName() + { + return "Wardriving"; + } + + + @Override + public String getModuleDescription() + { + return "Driver for collecting wireless networks"; + } + + + @Override + public String getModuleVersion() + { + return "0.1"; + } + + + @Override + public String getProviderName() + { + return "GeoRobotix LLC"; + } + + + @Override + public Class> getModuleClass() + { + return Wardriving.class; + } + + + @Override + public Class getModuleConfigClass() + { + return WardrivingConfig.class; + } + +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java new file mode 100644 index 00000000..f806e839 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/Wardriving.java @@ -0,0 +1,351 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import android.Manifest; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.bluetooth.le.BluetoothLeScanner; +import android.bluetooth.le.ScanCallback; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanSettings; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; + +import androidx.core.content.ContextCompat; + +import net.opengis.sensorml.v20.PhysicalComponent; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorException; +import org.sensorhub.impl.sensor.AbstractSensorModule; +import org.sensorhub.impl.sensor.android.SensorMLBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Wardriving sensor driver that scans for WiFi access points and + * records their details along with the device's GPS location. + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class Wardriving extends AbstractSensorModule { + static final String UID_PREFIX = "urn:osh:sensor:wardriving:"; + + private final ArrayList smlComponents; + private final SensorMLBuilder smlBuilder; + static final Logger logger = LoggerFactory.getLogger(Wardriving.class.getSimpleName()); + + private Context context; + WifiOutput wifiOutput; + BLEOutput bleOutput; + private HandlerThread eventThread; + private Handler eventHandler; + private BluetoothLeScanner bluetoothLeScanner; + private BluetoothManager bluetoothManager; + private WifiManager wifiManager; + private LocationManager locationManager; + private BroadcastReceiver wifiReceiver; + private LocationListener locationListener; + + private volatile double currentLat = 0.0; + private volatile double currentLon = 0.0; + private volatile double currentAlt = 0.0; + private volatile boolean scanning = false; + private Runnable scanRunnable; + private ScanCallback bleScanCallback; + + public Wardriving() { + this.smlComponents = new ArrayList(); + this.smlBuilder = new SensorMLBuilder(); + } + + @Override + public void doInit() { + logger.info("Initializing Wardriving Sensor"); + this.xmlID = "WARDRIVING_" + Build.SERIAL; + this.uniqueID = UID_PREFIX + config.getUidWithExt(); + + context = SensorHubService.getContext(); + + bleOutput = new BLEOutput(this); + bleOutput.doInit(); + addOutput(bleOutput, false); + + wifiOutput = new WifiOutput(this); + wifiOutput.doInit(); + addOutput(wifiOutput, false); + } + + @Override + public void doStart() throws SensorException { + eventThread = new HandlerThread("WardrivingThread"); + eventThread.start(); + eventHandler = new Handler(eventThread.getLooper()); + + wifiManager = (WifiManager) context.getApplicationContext() + .getSystemService(Context.WIFI_SERVICE); + locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + if (wifiManager == null) + throw new SensorException("WiFi service not available"); + + if (!wifiManager.isWifiEnabled()) { + logger.warn("WiFi is disabled"); + } + + wifiReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context ctx, Intent intent) { + logger.info("WiFi scan broadcast received"); + handleScanResults(); + } + }; + + IntentFilter filter = new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + context.registerReceiver(wifiReceiver, filter); + + // start GPS location updates + startLocationUpdates(); + + scanning = true; + scanRunnable = new Runnable() { + @Override + public void run() { + if (scanning) { + logger.info("Triggering WiFi scan"); + boolean started = wifiManager.startScan(); + logger.info("WiFi scan started: {}", started); + eventHandler.postDelayed(this, config.scanIntervalMs); + } + } + }; + eventHandler.post(scanRunnable); + + // start BLE scanning + startBleScan(); + + logger.info("Wardriving sensor started"); + } + + private void startBleScan() { + bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + if (bluetoothManager == null) { + logger.warn("BluetoothManager not available, skipping BLE scan"); + return; + } + + BluetoothAdapter adapter = bluetoothManager.getAdapter(); + if (adapter == null || !adapter.isEnabled()) { + logger.warn("Bluetooth adapter not available or disabled, skipping BLE scan"); + return; + } + + try { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { + logger.error("BLUETOOTH_SCAN permission not granted"); + return; + } + + bluetoothLeScanner = adapter.getBluetoothLeScanner(); + if (bluetoothLeScanner == null) { + logger.warn("BLE scanner not available"); + return; + } + + bleScanCallback = new ScanCallback() { + @Override + public void onScanResult(int callbackType, android.bluetooth.le.ScanResult result) { + if (!scanning) return; + + String address = result.getDevice().getAddress(); + String name = null; + try { + name = result.getDevice().getName(); + } catch (SecurityException e) { + } + int rssi = result.getRssi(); + + bleOutput.setData(address, name, rssi, currentLat, currentLon, currentAlt); + } + + @Override + public void onBatchScanResults(List results) { + for (android.bluetooth.le.ScanResult result : results) { + onScanResult(ScanSettings.CALLBACK_TYPE_ALL_MATCHES, result); + } + } + + @Override + public void onScanFailed(int errorCode) { + logger.error("BLE scan failed with error code: {}", errorCode); + } + }; + + ScanSettings settings = new ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) + .setReportDelay(0) + .build(); + + bluetoothLeScanner.startScan(Collections.emptyList(), settings, bleScanCallback); + logger.info("BLE scanning started"); + + } catch (SecurityException e) { + logger.error("Security exception starting BLE scan", e); + } + } + + private void startLocationUpdates() { + locationListener = new LocationListener() { + @Override + public void onLocationChanged(Location location) { + currentLat = location.getLatitude(); + currentLon = location.getLongitude(); + currentAlt = location.hasAltitude() ? location.getAltitude() : 0.0; + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) {} + @Override + public void onProviderEnabled(String provider) {} + @Override + public void onProviderDisabled(String provider) {} + }; + + try { + if (ContextCompat.checkSelfPermission(context, + Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000, 0, locationListener, eventThread.getLooper()); + + Location last = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); + if (last != null) { + currentLat = last.getLatitude(); + currentLon = last.getLongitude(); + currentAlt = last.hasAltitude() ? last.getAltitude() : 0.0; + } + } else { + logger.error("Location permission not granted"); + } + } catch (SecurityException e) { + logger.error("Security exception requesting location updates", e); + } + } + + private void handleScanResults() { + if (!scanning) + return; + + try { + List results = wifiManager.getScanResults(); + if (results == null || results.isEmpty()) { + logger.debug("No WiFi scan results"); + return; + } + + logger.info("Scan found {} WiFi access points at [{}, {}]", + results.size(), currentLat, currentLon); + + for (ScanResult ap : results) { + logger.info("AP: BSSID={} SSID=\"{}\" RSSI={}dBm Freq={}MHz Security={}", + ap.BSSID, + ap.SSID != null ? ap.SSID : "", + ap.level, + ap.frequency, + ap.capabilities); + + wifiOutput.setData( + ap.BSSID, + ap.SSID, + ap.level, + ap.frequency, + ap.capabilities, + currentLat, + currentLon, + currentAlt + ); + } + } catch (SecurityException e) { + logger.error("Security exception reading scan results", e); + } + } + + @Override + public void doStop() { + scanning = false; + + if (eventHandler != null && scanRunnable != null) { + eventHandler.removeCallbacks(scanRunnable); + } + + if (wifiReceiver != null) { + try { + context.unregisterReceiver(wifiReceiver); + } catch (IllegalArgumentException e) { + logger.warn("WiFi receiver already unregistered"); + } + wifiReceiver = null; + } + + if (bluetoothLeScanner != null && bleScanCallback != null) { + try { + bluetoothLeScanner.stopScan(bleScanCallback); + } catch (SecurityException e) { + logger.warn("Security exception stopping BLE scan", e); + } + bleScanCallback = null; + bluetoothLeScanner = null; + } + + if (locationManager != null && locationListener != null) { + locationManager.removeUpdates(locationListener); + locationListener = null; + } + + if (eventThread != null) { + eventThread.quitSafely(); + eventThread = null; + } + + eventHandler = null; + logger.info("Wardriving sensor stopped"); + } + + @Override + public boolean isConnected() { + return wifiManager != null && scanning; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java new file mode 100644 index 00000000..82daa6b9 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WardrivingConfig.java @@ -0,0 +1,52 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + +The contents of this file are subject to the Mozilla Public License, v. 2.0. +If a copy of the MPL was not distributed with this file, You can obtain one +at http://mozilla.org/MPL/2.0/. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +for the specific language governing rights and limitations under the License. + +Copyright (C) 2012-2015 Sensia Software LLC. All Rights Reserved. + +******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import org.sensorhub.android.SensorHubService; +import org.sensorhub.api.sensor.SensorConfig; + +import android.content.Context; +import android.provider.Settings; + + +/** + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class WardrivingConfig extends SensorConfig +{ + + public WardrivingConfig() + { + this.moduleClass = Wardriving.class.getCanonicalName(); + } + public String uid_extension; + + public long scanIntervalMs = 10000; + + public static String getUid() { + Context context = SensorHubService.getContext(); + return Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + } + + public String getUidWithExt() + { + String baseUid = getUid(); + if (uid_extension != null && !uid_extension.isEmpty()) + return baseUid + ":" + uid_extension; + return baseUid; + } +} diff --git a/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java new file mode 100644 index 00000000..438a8b59 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/java/org/sensorhub/impl/sensor/wardriving/WifiOutput.java @@ -0,0 +1,132 @@ +/***************************** BEGIN LICENSE BLOCK *************************** + + The contents of this file are subject to the Mozilla Public License, v. 2.0. + If a copy of the MPL was not distributed with this file, You can obtain one + at http://mozilla.org/MPL/2.0/. + + Software distributed under the License is distributed on an "AS IS" basis, + WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + for the specific language governing rights and limitations under the License. + + The Initial Developer is Botts Innovative Research Inc. Portions created by the Initial + Developer are Copyright (C) 2025 the Initial Developer. All Rights Reserved. + + ******************************* END LICENSE BLOCK ***************************/ + +package org.sensorhub.impl.sensor.wardriving; + +import net.opengis.swe.v20.DataBlock; +import net.opengis.swe.v20.DataComponent; +import net.opengis.swe.v20.DataEncoding; + +import org.sensorhub.api.data.DataEvent; +import org.sensorhub.impl.sensor.AbstractSensorOutput; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vast.swe.SWEHelper; +import org.vast.swe.helper.GeoPosHelper; + + +/** + * Output for wardriving WiFi access point scan results + * + * @author Kalyn Stricklin + * @since April 6, 2026 + */ +public class WifiOutput extends AbstractSensorOutput +{ + DataComponent dataStruct; + DataEncoding dataEncoding; + private static final String SENSOR_OUTPUT_NAME = "wifiScan"; + private static final String SENSOR_OUTPUT_LABEL = "WiFi Access Point Scan"; + private static final Logger logger = LoggerFactory.getLogger(WifiOutput.class); + + protected WifiOutput(Wardriving parent) { + super(SENSOR_OUTPUT_NAME, parent); + } + + public void doInit() { + GeoPosHelper fac = new GeoPosHelper(); + + dataStruct = fac.createRecord() + .name(SENSOR_OUTPUT_NAME) + .label(SENSOR_OUTPUT_LABEL) + .definition(SWEHelper.getPropertyUri("WifiScanResult")) + .addField("time", fac.createTime() + .asSamplingTimeIsoUTC() + .label("Sampling Time") + .build()) + .addField("bssid", fac.createText() + .label("BSSID") + .definition(SWEHelper.getPropertyUri("NetworkAddress")) + .description("MAC address of the access point") + .build()) + .addField("ssid", fac.createText() + .label("SSID") + .definition(SWEHelper.getPropertyUri("NetworkName")) + .description("Network name (may be empty for hidden networks)") + .build()) + .addField("rssi", fac.createQuantity() + .label("Signal Strength") + .definition(SWEHelper.getPropertyUri("SignalStrength")) + .description("Received signal strength indicator") + .build()) + .addField("frequency", fac.createQuantity() + .label("Channel Frequency") + .definition(SWEHelper.getPropertyUri("RadioFrequency")) + .description("Center frequency of the channel in MHz") + .build()) + .addField("capabilities", fac.createText() + .label("Security Capabilities") + .definition(SWEHelper.getPropertyUri("SecurityCapabilities")) + .description("Authentication and encryption schemes supported") + .build()) + .addField("location", fac.newLocationVectorLLA( + SWEHelper.getPropertyUri("SensorLocation"))) + + .build(); + + dataEncoding = fac.newTextEncoding(",", "\n"); + } + + + public void setData(String bssid, String ssid, int rssi, int frequency, + String capabilities, double lat, double lon, double alt) { + + DataBlock dataBlock; + if (latestRecord == null) + dataBlock = dataStruct.createDataBlock(); + else + dataBlock = latestRecord.renew(); + + int idx = 0; + dataBlock.setDoubleValue(idx++, System.currentTimeMillis() / 1000d); + dataBlock.setStringValue(idx++, bssid); + dataBlock.setStringValue(idx++, ssid != null ? ssid : ""); + dataBlock.setIntValue(idx++, rssi); + dataBlock.setIntValue(idx++, frequency); + dataBlock.setStringValue(idx++, capabilities != null ? capabilities : ""); + dataBlock.setDoubleValue(idx++, lat); + dataBlock.setDoubleValue(idx++, lon); + dataBlock.setDoubleValue(idx++, alt); + + latestRecord = dataBlock; + latestRecordTime = System.currentTimeMillis(); + eventHandler.publish(new DataEvent(latestRecordTime, this, dataBlock)); + } + + @Override + public double getAverageSamplingPeriod() { + return 10.0; + } + + @Override + public DataComponent getRecordDescription() { + return dataStruct; + } + + @Override + public DataEncoding getRecommendedEncoding() { + return dataEncoding; + } +} diff --git a/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider b/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider new file mode 100644 index 00000000..26092ad3 --- /dev/null +++ b/sensorhub-android-wardriving/src/main/resources/META-INF/services/org.sensorhub.api.module.IModuleProvider @@ -0,0 +1 @@ +org.sensorhub.impl.sensor.wardriving.Descriptor \ No newline at end of file diff --git a/sensorhub-android-wardriving/src/test/java/empty b/sensorhub-android-wardriving/src/test/java/empty new file mode 100644 index 00000000..e69de29b diff --git a/sensorhub-android-wardriving/src/test/resources/empty b/sensorhub-android-wardriving/src/test/resources/empty new file mode 100644 index 00000000..e69de29b 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