From b67abd60e6dc3dbfeac90c7300c60be0ab26dc1d Mon Sep 17 00:00:00 2001 From: Himali Aryal <82476670+himalia416@users.noreply.github.com> Date: Wed, 30 Jul 2025 14:51:02 +0200 Subject: [PATCH] Migration to new BLEK library (#143) * Changed view. * Clear messages. * Clear messages. * Add or delete configuration. * Fixed configuration picker. * Edit configuration. * Create new macro. * removed unnecessary resource files. * Fixed running macro command. * Delete macro * Edit macro * Changed to peripheral name. * Show peripheral name. * Fixed Eol tab design. * Removed icon resource * String changes * Removed any permission from home view. * Clear device after disconnection. * 1 line app bar * Changed missing services text. * Throughput service view changes. * Throughput service fixes. * Removed unused resources. * Fixed Health temperature profile. * Show heart rate. * Fixed hrs view. * Show heart rate data from left to right in the chart. * Changed chart color, solid, and scroll to see history. * Horizontal grid hidden, in case needed. * HTS view update * Changed padding. * Removed circular icon background. * Updated Battery level view. * Updated hrs body sensor location. * Moved ui mappers into view. * Updated gls view. * Changed focus color. * Fixed issue with job. * Fixed bps. * Added Blood pressure feature uuid. * Added blood pressure feature data. * Added rscs feature data. * Fixed cscs view. * Show supported features. * Fixed ui * Suspend the service discovery for GLS and CGMS until bonding is completed. * Added suspend on the function level. * Bonding state check only to cgms service * Removed stacktrace print. * Make cgms record available within a scrollable box * Changed to gray color. * removed padding * Fix height for output section. * onExpand click event. * Added todo for 9th item. * Removed unused code block. * When in focus, reduce the hint text alpha value. * Show empty text error. * Clear focus on tap outside. * Add border when focused. * Propagate focus changes. * CGM graph * Added sample of one to many uart configuration database. * Added device and configuration entities. * Fixed issue with only showing last item from the list. * Changed configuration database irrespective of device address. * File rename. * Added last configuration datastore. * Check if configuration name is unique * Removed Macro text. * Included x and y axis data points. * Added channel sounding service uuid. * Upgraded agp version to 2.7. * Added channel sounding manager. * Downgraded datastore preference to 1.1.4. * Changed to nordic colors. * Added ranging permission. * channel sounding repository * channel sounding service data * channel sounding profile * channel sounding profile in viewmodel * channel sounding manager class * channel sounding testing * CS service characteristics * Create bonding before channel sounding connection. * Clean up. * Added LBS profile * Read/write data to LBS * LBS ui events * LBS service * LBS profile * LBS ui * Agp upgrade * Fixed LBS profile * Removed focus * Changed macro size to 9 * Changed macro color * Show macro in bottom sheet * View refactoring * Added Blek dependency * Added utils dependency * rename * Removed unused event * reorganization * uart macro view update * background color update * different color for input and output message type * Changed to uart event * removed duplicate * rename * auto scroll to new record * removed unused dependency * Fixed crash with ChannelSoundingManager injection. * Require bonding only if it has bonding information * Changed disconnection * CGMS graph * changes in the home view * Home view fixes * changed color * Show MacroEol character in the input message. * Home view icon fixes. * Cadence data parser fixes * Fixed CSC settings view. * Fixed rscs view * hiding graphs until its finished * Removed duplicate * Fixed RSCS view * Fixed notification icon * fixed csc module name * Fixed icon cutoff * Fixed CSCDataParser * Fixed CGMS profile * Fixed GLS view * Fixed GLS strings * Fixed HTS view * Fixed HTS view * title change * Added hts timestamp * Deleted verbose text * UART: changed macro/configuration to preset * UART: fixed input text field * UART: removed expandable/collapsable preset * UART: added extra warning to delete action * UART: don't trim message end. * UART: message section * UART: configuration fixed * UART: configuration fixed * Fix crash when disconnecting before MTU change completes * Disabled incomplete PRX profile * Moved non-composable lambdas to parameters * refactoring display text * Fixed channel sounding screen * Disconnect on missing services before navigation * Fixed label name * Tailored disconnection message. * Tailored disconnection message. * Moved profile file to utils * App analytics events and modes * Integrated analytics with the profile actions. * Show only first non-battery service if multiple services are present. * Fixed window insets for camera notch. * Fixed glucose measurement context. * Fixed glucose concentration unit. * Fixed duplicate analytics update. * rename * refactoring text * Handled disconnecting event. * Replaced with LazyColumn * Fixed window insets * Replaced TitleAppBar with NordicAppBar * Show device address * Show multiple service names if available. * Fixed padding * BPS: Fixed waiting for measurement view. * BPS: view * GLS: Fixed padding * Ui: Fixed dialog * RSCS: fixed distance formatting error * CGMS: ui consistency * DFS: ui fixes * Replaced local scanner with common library scanner. * Fixed padding * reorganization * Removed previous uart module * Text with animated three dots * HTS: text fixes * formatting texts * changed text style * fixed string * Fixed HRS, not completed * DFS: fixed ui * HRS: graph fixes * UART: scroll up when keyboard is visible * Uart input: Add focus * Uart fix: input text field * UART: created rememberImeState * HRS: heart rate ui fixes * profile view scrollable fix * DFS: ui fixes * Fixed logger * Check if the battery characteristics supports NOTIFY or INDICATE property * Dependency update * Changed background color * cleanup * Fixed distance measurement data update. * Filtered devices with testing address * Added preview data * Fixed section view * Fixed elevation view * Removed duplicate views * Fixes control points * String fixes * Elevation view fixes * Range slider view update * Fixed DFS views * Fixed DFS ui * Fixed DFS views * Separated views * Separated profile viewmodel into individual profile view models. * AGP upgrade * Job canceled and make jobs null on clear * Profile name update * Request maximum MTU size only if it is not already set. * Fixed null pointer exception * Battery characteristics read property check * Fixed early mtu request * Removed garbage states * Removed logs * Removed multiple vertical scroll * Fixed padding * Ui fixes * File reorganization * Fixed previous configuration not loading on reconnection * Removed unused files * Dependency update * Renamed module name * Removed unused dependencies * Added param * Removed unused code block * Code optimization * Removed unused file * Readme update * Hide Channel sounding until implementation is complete * Handled initial state closed * revert changes * Added library as module placeholder * Fixed multiple flows for the same peripheral * Request mtu size only when needed * Readme update --- README.md | 8 +- app/build.gradle.kts | 40 +- .../nrftoolbox/NrfToolboxApplication.kt | 11 - app/src/main/AndroidManifest.xml | 7 + .../android/nrftoolbox/AppDestination.kt | 39 +- .../android/nrftoolbox/MainActivity.kt | 14 +- .../android/nrftoolbox/ScannerDestination.kt | 46 ++ .../{ => di}/ApplicationScopeModule.kt | 5 +- .../nrftoolbox/di/CentralManagerModule.kt | 24 + .../nrftoolbox/repository/ActivitySignals.kt | 48 -- .../android/nrftoolbox/view/FeatureButton.kt | 169 +++--- .../android/nrftoolbox/view/HomeView.kt | 527 ++++++++++-------- .../android/nrftoolbox/view/HomeViewState.kt | 48 -- .../android/nrftoolbox/view/Links.kt | 103 ++++ .../nrftoolbox/view/NoConnectedDeviceView.kt | 83 +++ .../android/nrftoolbox/view/TitleAppBar.kt | 60 -- .../nrftoolbox/viewmodel/HomeViewModel.kt | 149 ++--- .../android/nrftoolbox/viewmodel/UiEvent.kt | 23 + app/src/main/res/drawable/ic_gls.xml | 43 -- app/src/main/res/values/strings.xml | 35 ++ gradle/wrapper/gradle-wrapper.properties | 7 +- lib_analytics/build.gradle.kts | 1 + .../{Profiles.kt => AnalyticsMode.kt} | 26 +- .../no/nordicsemi/android/analytics/Events.kt | 64 ++- lib_scanner/build.gradle.kts | 50 -- lib_scanner/module-rules.pro | 17 - .../toolbox/scanner/ScannerDestination.kt | 29 - lib_service/build.gradle.kts | 12 +- lib_service/src/main/AndroidManifest.xml | 11 +- .../android/service/DisconnectAndStopEvent.kt | 3 - .../android/service/NotificationService.kt | 15 +- .../android/service/OpenLoggerEvent.kt | 3 - .../android/service/ServiceManager.kt | 41 -- .../service/ServiceManagerHiltModule.kt | 21 - .../android/service/ServiceManagerImpl.kt | 20 - .../di/ProfileServiceManagerImpModule.kt | 21 + .../android/service/profile/ProfileService.kt | 321 +++++++++++ .../service/profile/ProfileServiceManager.kt | 56 ++ .../android/service/profile/ServiceApi.kt | 69 +++ lib_storage/build.gradle.kts | 14 + lib_storage/module-rules.pro | 21 + .../src/main/AndroidManifest.xml | 4 +- .../toolbox/lib/storage/Configuration.kt | 14 + .../toolbox/lib/storage/ConfigurationDao.kt | 20 + .../lib/storage/ConfigurationDatabase.kt | 13 + .../toolbox/lib/storage/InitMigration.kt | 10 + .../toolbox/lib/storage/di/DaoHiltModule.kt | 20 + .../toolbox/lib/storage/di}/DbHiltModule.kt | 14 +- lib_ui/build.gradle.kts | 8 +- .../android/ui/view/AnimatedThreeDots.kt | 91 +++ .../ui/view/AnimationTransitionState.kt | 130 +++++ .../android/ui/view/BatteryLevelView.kt | 46 -- .../android/ui/view/DropdownView.kt | 113 ++++ .../android/ui/view/FeatureSupportedRow.kt | 53 ++ .../android/ui/view/KeyValueColumn.kt | 152 +++++ .../android/ui/view/ScreenSection.kt | 11 +- .../nordicsemi/android/ui/view/SectionRow.kt | 22 + .../android/ui/view/SectionTitle.kt | 125 ++++- .../android/ui/view/TextInputField.kt | 104 ++++ .../nordicsemi/android/ui/view/TopAppBar.kt | 87 +-- .../android/ui/view/animate/AnimatedHeart.kt | 49 ++ .../nordicsemi/android/ui/view/dialog/Ext.kt | 5 +- .../ui/view/dialog/StringListDialog.kt | 101 ---- .../ui/view/dialog/StringListDialogConfig.kt | 43 -- .../ui/view/dialog/StringListDialogResult.kt | 38 -- .../ui/view/internal/DeviceConnectingView.kt | 80 +++ .../view/internal/DeviceDisconnectedView.kt | 116 ++++ .../android/ui/view/internal/EmptyView.kt | 40 ++ .../android/ui/view/internal/LoadingView.kt | 32 ++ .../ui/view/internal/ServiceDiscoveryView.kt | 81 +++ lib_ui/src/main/res/drawable/ic_battery.xml | 22 + .../src/main/res/drawable/ic_bps.xml | 0 .../src/main/res/drawable/ic_cgm.xml | 4 +- .../src/main/res/drawable/ic_csc.xml | 0 .../src/main/res/drawable/ic_dfu.xml | 0 .../src/main/res/drawable/ic_gls.xml | 10 +- .../src/main/res/drawable/ic_hrs.xml | 0 .../src/main/res/drawable/ic_hts.xml | 0 .../src/main/res/drawable/ic_prx.xml | 0 .../src/main/res/drawable/ic_rscs.xml | 0 .../res/drawable/ic_running_indicator.xml | 0 .../src/main/res/drawable/ic_uart.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 3 +- .../main/res/mipmap-hdpi/ic_shortcut_dfu.png | Bin 2723 -> 0 bytes .../main/res/mipmap-hdpi/ic_shortcut_uart.png | Bin 2196 -> 0 bytes lib_ui/src/main/res/values/strings.xml | 7 +- lib_utils/build.gradle.kts | 42 +- lib_utils/module-rules.pro | 16 +- lib_utils/src/main/AndroidManifest.xml | 35 +- .../android/toolbox/lib/utils/Ext.kt | 7 + .../android/toolbox/lib/utils/Profile.kt | 37 ++ .../android/toolbox/lib/utils/spec/Spec.kt | 18 + .../lib/utils/tryOrLog.kt} | 2 +- .../java/no/nordicsemi/android/utils/Ext.kt | 58 -- permissions-ranging/build.gradle.kts | 11 + permissions-ranging/module-rules.pro | 21 + .../src/main/AndroidManifest.xml | 6 + .../RequestRangingPermission.kt | 42 ++ .../repository/RangingStateManager.kt | 85 +++ .../utils/LocalDataProvider.kt | 35 ++ .../utils/RangingPermissionState.kt | 23 + .../utils/RangingPermissionUtils.kt | 53 ++ .../view/RangingPermissionRequestView.kt | 49 ++ .../viewmodel/RangingPermissionViewModel.kt | 37 ++ profile-parsers/build.gradle.kts | 16 + profile-parsers/module-rules.pro | 21 + profile-parsers/src/main/AndroidManifest.xml | 2 + .../parser/battery/BatteryLevelParser.kt | 10 +- .../toolbox/profile/parser/bps/BPMStatus.kt | 21 + .../toolbox/profile/parser/bps/BPSData.kt | 30 + .../parser/bps/BloodPressureFeatureData.kt | 17 + .../parser/bps/BloodPressureFeatureParser.kt | 27 + .../bps/BloodPressureMeasurementParser.kt | 89 +++ .../bps/IntermediateCuffPressureParser.kt | 76 +++ .../profile/parser/cgms/CGMFeatureParser.kt | 43 ++ .../parser/cgms/CGMMeasurementParser.kt | 118 ++++ .../cgms/CGMSpecificOpsControlPointParser.kt | 200 +++++++ .../profile/parser/cgms/CGMStatusParser.kt | 35 ++ .../parser/cgms/data/CGMCalibrationStatus.kt | 7 + .../profile/parser/cgms/data/CGMErrorCode.kt | 15 + .../profile/parser/cgms/data/CGMFeatures.kt | 50 ++ .../profile/parser/cgms/data/CGMOpCode.kt | 22 + .../profile/parser/cgms/data/CGMRecord.kt | 10 + .../data/CGMSpecificOpsControlPointData.kt | 18 + .../profile/parser/cgms/data/CGMStatus.kt | 55 ++ .../toolbox/profile/parser/common/CRC16.kt | 175 ++++++ .../profile/parser/common/WorkingMode.kt | 5 + .../toolbox/profile/parser/csc/CSCData.kt | 11 + .../profile/parser/csc/CSCDataParser.kt | 198 +++++++ .../profile/parser/csc/CSCDataSnapshot.kt | 8 + .../toolbox/profile/parser/csc/SpeedUnit.kt | 7 + .../toolbox/profile/parser/csc/WheelSize.kt | 64 +++ .../profile/parser/date/DateTimeParser.kt | 73 ++- .../profile/parser/directionFinder/Mapper.kt | 11 + .../PeripheralBluetoothAddress.kt | 25 + .../azimuthal/AzimuthalMeasurementData.kt | 22 + .../AzimuthalMeasurementDataParser.kt | 43 ++ .../controlPoint/ControlPointDataParser.kt | 68 +++ .../controlPoint/ControlPointMode.kt | 36 ++ .../controlPoint/ControlPointResult.kt | 15 + .../parser/directionFinder/ddf/DDFData.kt | 6 + .../directionFinder/ddf/DDFDataParser.kt | 18 + .../directionFinder/distance/DirectionMode.kt | 5 + .../distance/DistanceMeasurementData.kt | 52 ++ .../distance/DistanceMeasurementDataParser.kt | 66 +++ .../distance/QualityIndicator.kt | 15 + .../elevation/ElevationMeasurementData.kt | 11 + .../ElevationMeasurementDataParser.kt | 39 ++ .../parser/gls/CGMSpecificOpsControlPoint.kt | 67 +++ .../gls/GlucoseMeasurementContextParser.kt | 123 ++++ .../parser/gls/GlucoseMeasurementParser.kt | 80 +++ .../RecordAccessControlPointInputParser.kt | 257 +++++++++ .../gls/RecordAccessControlPointParser.kt | 59 ++ .../profile/parser/gls/data/Carbohydrate.kt | 19 + .../profile/parser/gls/data/GLSRecord.kt | 91 +++ .../parser/gls/data/GlucoseMeasurementUnit.kt | 6 + .../profile/parser/gls/data/GlucoseStatus.kt | 41 ++ .../toolbox/profile/parser/gls/data/Health.kt | 18 + .../toolbox/profile/parser/gls/data/Meal.kt | 17 + .../profile/parser/gls/data/Medication.kt | 17 + .../gls/data/RecordAccessControlPointData.kt | 33 ++ .../profile/parser/gls/data/RequestStatus.kt | 5 + .../toolbox/profile/parser/gls/data/Tester.kt | 16 + .../parser/hrs/BodySensorLocationParser.kt | 14 + .../toolbox/profile/parser/hrs/HRSData.kt | 15 + .../profile/parser/hrs/HRSDataParser.kt | 53 ++ .../toolbox/profile/parser/hts/HTSData.kt | 104 ++++ .../profile/parser/hts/HTSDataParser.kt | 74 ++- .../toolbox/profile/parser/prx/AlarmLevel.kt | 14 + .../profile/parser/prx/AlarmLevelParser.kt | 15 + .../parser/prx/AlertLevelInputParser.kt | 8 + .../toolbox/profile/parser/prx/PRXData.kt | 7 + .../toolbox/profile/parser/racp/RACPOpCode.kt | 15 + .../profile/parser/racp/RACPResponseCode.kt | 20 + .../toolbox/profile/parser/rscs/RSCSData.kt | 9 + .../profile/parser/rscs/RSCSDataParser.kt | 54 ++ .../profile/parser/rscs/RSCSFeatureData.kt | 17 + .../profile/parser/rscs/RSCSFeatureParser.kt | 23 + .../profile/parser/rscs/RSCSSettingsUnit.kt | 18 + .../parser/throughput/ThroughputDataParser.kt | 26 + .../parser/throughput/ThroughputMetrics.kt | 7 + .../parser/battery/BatteryLevelParserTest.kt | 43 ++ .../bps/BloodPressureMeasurementParserTest.kt | 105 ++++ .../bps/IntermediateCuffPressureParserTest.kt | 121 ++++ .../parser/cgm/CGMFeatureParserTest.kt | 46 ++ .../parser/cgm/CGMMeasurementParserTest.kt | 103 ++++ .../profile/parser/csc/CSCDataParserTest.kt | 127 +++++ .../gls/GlucoseMeasurementParserTest.kt | 142 +++++ .../profile/parser/hrs/HRSDataParserTest.kt | 68 +++ .../profile/parser/hts/DateTimeParserTest.kt | 76 +++ .../profile/parser/hts/HTSDataParserTest.kt | 95 ++++ .../profile/parser/rscs/RSCSDataParserTest.kt | 81 +++ profile/build.gradle.kts | 47 ++ profile/module-rules.pro | 21 + profile/src/main/AndroidManifest.xml | 12 + .../toolbox/profile/ProfileDestination.kt | 11 + .../android/toolbox/profile/ProfileScreen.kt | 258 +++++++++ .../android/toolbox/profile/data/UiMapper.kt | 15 + .../profile/repository/DeviceRepository.kt | 60 ++ .../channelSounding/ChannelSoundingManager.kt | 144 +++++ .../RangingSessionCloseReason.kt | 28 + .../RangingSessionFailedReason.kt | 27 + .../RangingSessionStartTechnology.kt | 24 + .../repository/uartXml/CommentVisitor.kt | 28 + .../uartXml/UartConfigurationDataStore.kt | 36 ++ .../uartXml/UartConfigurationRepository.kt | 114 ++++ .../repository/uartXml/XmlConfiguration.java | 53 ++ .../profile/repository/uartXml/XmlMacro.java | 95 ++++ .../profile/view/battery/BatteryLevelView.kt | 136 +++++ .../toolbox/profile/view/bps/BPSScreen.kt | 243 ++++++++ .../toolbox/profile/view/cgms/CGMMapper.kt | 19 + .../toolbox/profile/view/cgms/CGMScreen.kt | 350 ++++++++++++ .../channelSounding/ChannelSoundingScreen.kt | 62 +++ .../toolbox/profile/view/cscs/CSCScreen.kt | 327 +++++++++++ .../toolbox/profile/view/cscs/CSCUiMapper.kt | 58 ++ .../AzimuthAndElevationSection.kt | 106 ++++ .../view/directionFinder/AzimuthSection.kt | 84 +++ .../view/directionFinder/AzimuthView.kt | 266 +++++++++ .../view/directionFinder/ControlView.kt | 171 ++++++ .../profile/view/directionFinder/DFSScreen.kt | 77 +++ .../view/directionFinder/DistanceSection.kt | 58 ++ .../view/directionFinder/DistanceView.kt | 46 ++ .../view/directionFinder/DistanceViewChart.kt | 79 +++ .../view/directionFinder/ElevationSection.kt | 52 ++ .../view/directionFinder/ElevationView.kt | 175 ++++++ .../view/directionFinder/LinearDataView.kt | 147 +++++ .../directionFinder/MeasurementDetailsView.kt | 33 ++ .../view/directionFinder/RangeSlider.kt | 81 +++ .../SectionBluetoothDeviceComponent.kt | 276 +++++++++ .../toolbox/profile/view/gls/GLSScreen.kt | 379 +++++++++++++ .../toolbox/profile/view/gls/GLSUiMapper.kt | 49 ++ .../profile/view/gls/details/GLSDetails.kt | 310 +++++++++++ .../view/gls/details/GLSDetailsUiMapper.kt | 81 +-- .../toolbox/profile/view/hrs/HRSScreen.kt | 104 ++++ .../toolbox/profile/view/hrs/HRSUiMapper.kt | 20 + .../toolbox/profile/view/hrs/LineChart.kt | 150 +++-- .../toolbox/profile/view/hts/HTSScreen.kt | 213 +++++++ .../toolbox/profile/view/hts/HTSUiMapper.kt | 28 +- .../profile/view/internal/ProfileAppBar.kt | 35 ++ .../toolbox/profile/view/lbs/BlinkyScreen.kt | 168 ++++++ .../toolbox/profile/view/rscs/RSCSScreen.kt | 226 ++++++++ .../toolbox/profile/view/rscs/RSCSUiMapper.kt | 128 +++++ .../view/throughput/ThroughputScreen.kt | 274 +++++++++ .../view/throughput/ThroughputUiMapper.kt | 44 ++ .../toolbox/profile/view/uart/InputSection.kt | 115 ++++ .../toolbox/profile/view/uart/MacroSection.kt | 260 +++++++++ .../profile/view/uart/OutputSection.kt | 227 ++++++++ .../view/uart/UARTAddConfigurationDialog.kt | 92 +++ .../profile/view/uart/UARTAddMacroDialog.kt | 259 +++++++++ .../view/uart/UARTConfigurationPicker.kt | 96 ++++ .../profile/view/uart}/UARTMacroView.kt | 136 ++--- .../toolbox/profile/view/uart/UARTScreen.kt | 34 ++ .../toolbox/profile/view/uart/UartUiMapper.kt | 31 ++ .../profile/view/uart/rememberImeState.kt | 33 ++ .../toolbox/profile/viewmodel/BPSViewModel.kt | 66 +++ .../profile/viewmodel/BatteryViewModel.kt | 65 +++ .../profile/viewmodel/CGMSViewModel.kt | 90 +++ .../toolbox/profile/viewmodel/CSCViewModel.kt | 76 +++ .../viewmodel/ChannelSoundingViewModel.kt | 83 +++ .../viewmodel/DeviceConnectionState.kt | 23 + .../viewmodel/DirectionFinderViewModel.kt | 110 ++++ .../toolbox/profile/viewmodel/GLSViewModel.kt | 81 +++ .../toolbox/profile/viewmodel/HRSViewModel.kt | 78 +++ .../toolbox/profile/viewmodel/HTSViewModel.kt | 93 ++++ .../toolbox/profile/viewmodel/LBSViewModel.kt | 97 ++++ .../profile/viewmodel/ProfileViewModel.kt | 308 ++++++++++ .../profile/viewmodel/RSCSViewModel.kt | 78 +++ .../profile/viewmodel/ThroughputViewModel.kt | 92 +++ .../profile/viewmodel/UartViewModel.kt | 276 +++++++++ profile/src/main/res/drawable/ic_arrow.xml | 9 + profile/src/main/res/drawable/ic_azimuth.xml | 9 + profile/src/main/res/drawable/ic_control.xml | 9 + profile/src/main/res/drawable/ic_distance.xml | 9 + .../src/main/res/drawable/ic_elevation.xml | 9 + .../src/main/res/drawable/ic_input.xml | 0 .../src/main/res/drawable/ic_macro.xml | 0 .../src/main/res/drawable/ic_output.xml | 0 profile/src/main/res/drawable/ic_rscs.xml | 49 ++ .../src/main/res/drawable/ic_sync_down.xml | 0 .../main/res/drawable/ic_sync_down_off.xml | 0 .../src/main/res/drawable/ic_uart_1.xml | 0 .../src/main/res/drawable/ic_uart_2.xml | 0 .../src/main/res/drawable/ic_uart_3.xml | 0 .../src/main/res/drawable/ic_uart_4.xml | 0 .../src/main/res/drawable/ic_uart_5.xml | 0 .../src/main/res/drawable/ic_uart_6.xml | 0 .../src/main/res/drawable/ic_uart_7.xml | 0 .../src/main/res/drawable/ic_uart_8.xml | 0 .../src/main/res/drawable/ic_uart_9.xml | 0 .../src/main/res/drawable/ic_uart_about.xml | 0 .../src/main/res/drawable/ic_uart_down.xml | 0 .../src/main/res/drawable/ic_uart_forward.xml | 0 .../src/main/res/drawable/ic_uart_left.xml | 0 .../src/main/res/drawable/ic_uart_pause.xml | 0 .../src/main/res/drawable/ic_uart_play.xml | 0 .../src/main/res/drawable/ic_uart_rewind.xml | 0 .../src/main/res/drawable/ic_uart_right.xml | 0 .../main/res/drawable/ic_uart_settings.xml | 0 .../src/main/res/drawable/ic_uart_stop.xml | 0 .../src/main/res/drawable/ic_uart_up.xml | 0 .../src/main/res/drawable/ic_zoom_in.xml | 0 .../src/main/res/drawable/ic_zoom_out.xml | 0 .../src/main/res/drawable/uart_button.xml | 0 profile/src/main/res/values/blinkyStrings.xml | 9 + profile/src/main/res/values/bpsStrings.xml | 24 + profile/src/main/res/values/cgmStrings.xml | 7 + profile/src/main/res/values/cscStrings.xml | 17 + profile/src/main/res/values/dfsStrings.xml | 53 ++ .../src/main/res/values/glsStrings.xml | 81 ++- profile/src/main/res/values/hrsStrings.xml | 6 + profile/src/main/res/values/htsStrings.xml | 11 + profile/src/main/res/values/rscsStrings.xml | 25 + profile/src/main/res/values/strings.xml | 4 + .../src/main/res/values/throughputStings.xml | 21 + profile/src/main/res/values/uartStrings.xml | 66 +++ profile_bps/build.gradle.kts | 67 --- profile_bps/src/main/AndroidManifest.xml | 35 -- .../android/bps/data/BPSServiceData.kt | 12 - .../android/bps/view/BPSContentView.kt | 62 --- .../nordicsemi/android/bps/view/BPSScreen.kt | 89 --- .../android/bps/view/BPSSensorsReadingView.kt | 119 ---- .../android/bps/view/BPSViewEvent.kt | 38 -- .../android/bps/view/BPSViewState.kt | 48 -- .../android/bps/viewmodel/BPSViewModel.kt | 216 ------- profile_bps/src/main/res/values/strings.xml | 47 -- profile_cgms/build.gradle.kts | 68 --- profile_cgms/module-rules.pro | 17 - profile_cgms/src/main/AndroidManifest.xml | 39 -- .../android/cgms/data/CGMServiceCommand.kt | 39 -- .../android/cgms/data/CGMServiceData.kt | 28 - .../android/cgms/repository/CGMRepository.kt | 154 ----- .../android/cgms/repository/CGMService.kt | 343 ------------ .../android/cgms/view/CGMContentView.kt | 193 ------- .../nordicsemi/android/cgms/view/CGMScreen.kt | 91 --- .../android/cgms/view/CGMViewEvent.kt | 44 -- .../android/cgms/viewmodel/CGMViewModel.kt | 126 ----- profile_cgms/src/main/res/values/strings.xml | 43 -- profile_csc/build.gradle.kts | 69 --- profile_csc/module-rules.pro | 17 - profile_csc/src/main/AndroidManifest.xml | 45 -- .../android/csc/data/CSCServiceData.kt | 21 - .../nordicsemi/android/csc/data/SpeedUnit.kt | 38 -- .../android/csc/repository/CSCRepository.kt | 149 ----- .../android/csc/repository/CSCService.kt | 146 ----- .../android/csc/view/CSCContentView.kt | 132 ----- .../nordicsemi/android/csc/view/CSCMappers.kt | 123 ---- .../nordicsemi/android/csc/view/CSCScreen.kt | 89 --- .../android/csc/view/CSCViewEvent.kt | 47 -- .../android/csc/view/SelectWheelSizeDialog.kt | 71 --- .../android/csc/view/SensorsReadingView.kt | 85 --- .../android/csc/view/WheelSizeView.kt | 77 --- .../android/csc/viewmodel/CSCViewModel.kt | 128 ----- profile_csc/src/main/res/values/strings.xml | 153 ----- profile_data/build.gradle.kts | 12 + .../module-rules.pro | 2 +- profile_data/src/main/AndroidManifest.xml | 2 + .../toolbox/profile/data/BPSServiceData.kt | 20 + .../profile/data/BatteryServiceData.kt | 14 + .../toolbox/profile/data/CGMServiceData.kt | 19 + .../toolbox/profile/data/CSCServiceData.kt | 11 + .../data/ChannelSoundingServiceData.kt | 7 + .../toolbox/profile/data/DFSServiceData.kt | 58 ++ .../toolbox/profile/data/GLSServiceData.kt | 14 + .../toolbox/profile/data/HRSServiceData.kt | 22 + .../toolbox/profile/data/HTSServiceData.kt | 18 + .../toolbox/profile/data/LBSServiceData.kt | 16 + .../profile/data/ProfileServiceData.kt | 10 + .../toolbox/profile/data/RSCSServiceData.kt | 13 + .../profile/data/ThroughputServiceData.kt | 37 ++ .../toolbox/profile/data/UARTServiceData.kt | 42 ++ .../data/directionFinder/SensorDataExt.kt | 85 +++ .../toolbox/profile/data/uart/MacroEol.kt | 31 ++ .../toolbox/profile/data/uart/MacroIcon.kt | 31 ++ .../profile/data/uart/UARTConfiguration.kt | 16 + .../toolbox/profile/data/uart/UARTMacro.kt | 7 + .../profile/data/uiMapper}/TemperatureUnit.kt | 14 +- profile_gls/build.gradle.kts | 84 --- profile_gls/module-rules.pro | 17 - profile_gls/src/main/AndroidManifest.xml | 35 -- .../nordicsemi/android/gls/GLSDestination.kt | 42 -- .../no/nordicsemi/android/gls/GLSServer.kt | 288 ---------- .../android/gls/data/GLSServiceData.kt | 44 -- .../android/gls/details/view/Field.kt | 91 --- .../gls/details/view/GLSDetailsContentView.kt | 260 --------- .../gls/details/view/GLSDetailsScreen.kt | 56 -- .../details/viewmodel/GLSDetailsViewModel.kt | 55 -- .../android/gls/main/view/GLSContentView.kt | 213 ------- .../android/gls/main/view/GLSMapper.kt | 83 --- .../android/gls/main/view/GLSScreen.kt | 89 --- .../gls/main/view/GLSScreenViewEvent.kt | 45 -- .../android/gls/main/view/GLSViewState.kt | 84 --- .../gls/main/viewmodel/GLSViewModel.kt | 341 ------------ .../android/gls/GLSViewModelTest.kt | 188 ------- profile_hrs/build.gradle.kts | 70 --- profile_hrs/module-rules.pro | 17 - profile_hrs/src/main/AndroidManifest.xml | 41 -- .../android/hrs/data/HRSServiceData.kt | 55 -- .../android/hrs/service/HRSRepository.kt | 143 ----- .../android/hrs/service/HRSService.kt | 152 ----- .../android/hrs/view/HRSContentView.kt | 106 ---- .../nordicsemi/android/hrs/view/HRSScreen.kt | 89 --- .../android/hrs/view/HRSScreenViewEvent.kt | 42 -- .../android/hrs/viewmodel/HRSViewModel.kt | 125 ----- profile_hrs/src/main/res/values/strings.xml | 37 -- profile_hts/build.gradle.kts | 69 --- profile_hts/module-rules.pro | 17 - profile_hts/src/main/AndroidManifest.xml | 41 -- .../android/hts/data/HTSServiceData.kt | 53 -- .../android/hts/repository/HTSRepository.kt | 140 ----- .../android/hts/repository/HTSService.kt | 144 ----- .../android/hts/view/HTSContentView.kt | 103 ---- .../nordicsemi/android/hts/view/HTSMapper.kt | 78 --- .../nordicsemi/android/hts/view/HTSScreen.kt | 88 --- .../android/hts/view/HTSScreenViewEvent.kt | 42 -- .../android/hts/viewmodel/HTSViewModel.kt | 125 ----- profile_hts/src/main/res/values/strings.xml | 42 -- profile_manager/build.gradle.kts | 18 + profile_manager/module-rules.pro | 21 + profile_manager/src/main/AndroidManifest.xml | 2 + .../toolbox/profile/manager/BPSManager.kt | 64 +++ .../toolbox/profile/manager/BatteryManager.kt | 70 +++ .../toolbox/profile/manager/CGMManager.kt | 264 +++++++++ .../toolbox/profile/manager/CSCManager.kt | 42 ++ .../profile/manager/ChannelSoundingManager.kt | 66 +++ .../toolbox/profile/manager/DFSManager.kt | 157 ++++++ .../toolbox/profile/manager/GLSManager.kt | 184 ++++++ .../toolbox/profile/manager/HRSManager.kt | 56 ++ .../toolbox/profile/manager/HTSManager.kt | 45 ++ .../toolbox/profile/manager/LBSManager.kt | 100 ++++ .../toolbox/profile/manager/RSCSManager.kt | 55 ++ .../toolbox/profile/manager/ServiceManager.kt | 14 + .../profile/manager/ServiceManagerFactory.kt | 42 ++ .../profile/manager/ThroughputManager.kt | 120 ++++ .../toolbox/profile/manager/UARTManager.kt | 89 +++ .../manager/repository/BPSRepository.kt | 34 ++ .../manager/repository/BatteryRepository.kt | 23 + .../manager/repository/CGMRepository.kt | 50 ++ .../manager/repository/CSCRepository.kt | 40 ++ .../repository/ChannelSoundingRepository.kt | 11 + .../manager/repository/DFSRepository.kt | 216 +++++++ .../manager/repository/GLSRepository.kt | 72 +++ .../manager/repository/HRSRepository.kt | 36 ++ .../manager/repository/HTSRepository.kt | 29 + .../manager/repository/LBSRepository.kt | 55 ++ .../manager/repository/PRXRepository.kt | 27 + .../manager/repository/RSCSRepository.kt | 34 ++ .../repository/ThroughputRepository.kt | 48 ++ .../manager/repository/UartRepository.kt | 180 ++++++ profile_prx/build.gradle.kts | 69 --- profile_prx/module-rules.pro | 17 - profile_prx/src/main/AndroidManifest.xml | 40 -- .../android/prx/data/PRXServiceData.kt | 25 - .../android/prx/repository/PRXRepository.kt | 151 ----- .../android/prx/repository/PRXService.kt | 243 -------- .../android/prx/view/PRXContentView.kt | 132 ----- .../android/prx/view/PRXLinkLossView.kt | 99 ---- .../nordicsemi/android/prx/view/PRXMapper.kt | 54 -- .../nordicsemi/android/prx/view/PRXScreen.kt | 89 --- .../android/prx/view/PRXScreenViewEvent.kt | 44 -- .../android/prx/viewmodel/PRXViewModel.kt | 136 ----- profile_prx/src/main/res/values/strings.xml | 54 -- profile_rscs/build.gradle.kts | 68 --- profile_rscs/module-rules.pro | 17 - profile_rscs/src/main/AndroidManifest.xml | 40 -- .../android/rscs/data/RSCSServiceData.kt | 82 --- .../android/rscs/repository/RSCSRepository.kt | 135 ----- .../android/rscs/repository/RSCSService.kt | 144 ----- .../android/rscs/view/RSCSScreen.kt | 88 --- .../android/rscs/view/RSCScreenViewEvent.kt | 40 -- .../android/rscs/view/SensorsReadingView.kt | 70 --- .../android/rscs/viewmodel/RSCSViewModel.kt | 119 ---- profile_rscs/src/main/res/values/strings.xml | 46 -- profile_uart/build.gradle.kts | 100 ---- profile_uart/module-rules.pro | 20 - profile_uart/src/main/AndroidManifest.xml | 41 -- .../nordicsemi/android/uart/DaoHiltModule.kt | 20 - .../no/nordicsemi/android/uart/UartServer.kt | 143 ----- .../uart/data/ConfigurationDataSource.kt | 67 --- .../nordicsemi/android/uart/data/MacroEol.kt | 38 -- .../nordicsemi/android/uart/data/MacroIcon.kt | 62 --- .../android/uart/data/UARTConfiguration.kt | 47 -- .../nordicsemi/android/uart/data/UARTMacro.kt | 34 -- .../android/uart/data/UARTParser.kt | 40 -- .../uart/data/UARTPersistentDataSource.kt | 119 ---- .../android/uart/data/UARTServiceData.kt | 62 --- .../android/uart/db/CommentVisitor.kt | 59 -- .../android/uart/db/Configuration.kt | 45 -- .../android/uart/db/ConfigurationsDao.kt | 51 -- .../android/uart/db/ConfigurationsDatabase.kt | 40 -- .../android/uart/db/InitMigration.kt | 41 -- .../android/uart/db/XmlConfiguration.java | 84 --- .../nordicsemi/android/uart/db/XmlMacro.java | 126 ----- .../android/uart/repository/UARTRepository.kt | 169 ------ .../android/uart/repository/UARTService.kt | 175 ------ .../android/uart/view/InputSection.kt | 140 ----- .../android/uart/view/MacroSection.kt | 177 ------ .../android/uart/view/OutputSection.kt | 196 ------- .../uart/view/UARTAddConfigurationDialog.kt | 106 ---- .../android/uart/view/UARTAddMacroDialog.kt | 177 ------ .../uart/view/UARTConfigurationPicker.kt | 119 ---- .../android/uart/view/UARTContentView.kt | 72 --- .../android/uart/view/UARTMapper.kt | 74 --- .../android/uart/view/UARTScreen.kt | 133 ----- .../nordicsemi/android/uart/view/UARTState.kt | 55 -- .../android/uart/view/UARTViewEvent.kt | 58 -- .../android/uart/viewmodel/UARTViewModel.kt | 250 --------- profile_uart/src/main/res/values/strings.xml | 95 ---- .../gls/NordicLoggerFactoryTestModule.kt | 40 -- .../android/gls/ServiceManagerTestModule.kt | 57 -- .../android/gls/TestDbHiltModule.kt | 29 - .../nordicsemi/android/gls/TestHiltModule.kt | 13 - .../android/gls/UARTViewModelTest.kt | 207 ------- settings.gradle.kts | 22 +- 513 files changed, 19164 insertions(+), 14446 deletions(-) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/ScannerDestination.kt rename app/src/main/java/no/nordicsemi/android/nrftoolbox/{ => di}/ApplicationScopeModule.kt (65%) create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/repository/ActivitySignals.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/view/NoConnectedDeviceView.kt delete mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt create mode 100644 app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/UiEvent.kt delete mode 100644 app/src/main/res/drawable/ic_gls.xml rename lib_analytics/src/main/java/no/nordicsemi/android/analytics/{Profiles.kt => AnalyticsMode.kt} (85%) delete mode 100644 lib_scanner/build.gradle.kts delete mode 100644 lib_scanner/module-rules.pro delete mode 100644 lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/OpenLoggerEvent.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt delete mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/di/ProfileServiceManagerImpModule.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileService.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileServiceManager.kt create mode 100644 lib_service/src/main/java/no/nordicsemi/android/service/profile/ServiceApi.kt create mode 100644 lib_storage/build.gradle.kts create mode 100644 lib_storage/module-rules.pro rename {lib_scanner => lib_storage}/src/main/AndroidManifest.xml (62%) create mode 100644 lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/Configuration.kt create mode 100644 lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDao.kt create mode 100644 lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDatabase.kt create mode 100644 lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/InitMigration.kt create mode 100644 lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DaoHiltModule.kt rename {profile_uart/src/main/java/no/nordicsemi/android/uart => lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di}/DbHiltModule.kt (54%) create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimatedThreeDots.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimationTransitionState.kt delete mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/BatteryLevelView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/DropdownView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/FeatureSupportedRow.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/KeyValueColumn.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionRow.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/TextInputField.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/animate/AnimatedHeart.kt delete mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialog.kt delete mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogConfig.kt delete mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogResult.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceConnectingView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceDisconnectedView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/EmptyView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/LoadingView.kt create mode 100644 lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/ServiceDiscoveryView.kt create mode 100644 lib_ui/src/main/res/drawable/ic_battery.xml rename {app => lib_ui}/src/main/res/drawable/ic_bps.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_cgm.xml (98%) rename {app => lib_ui}/src/main/res/drawable/ic_csc.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_dfu.xml (100%) rename profile_uart/src/main/res/drawable/ic_pencil_off.xml => lib_ui/src/main/res/drawable/ic_gls.xml (53%) rename {app => lib_ui}/src/main/res/drawable/ic_hrs.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_hts.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_prx.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_rscs.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_running_indicator.xml (100%) rename {app => lib_ui}/src/main/res/drawable/ic_uart.xml (100%) delete mode 100644 lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_dfu.png delete mode 100644 lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_uart.png create mode 100644 lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Ext.kt create mode 100644 lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt create mode 100644 lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt rename lib_utils/src/main/java/no/nordicsemi/android/{utils/TryOrLog.kt => toolbox/lib/utils/tryOrLog.kt} (74%) delete mode 100644 lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt create mode 100644 permissions-ranging/build.gradle.kts create mode 100644 permissions-ranging/module-rules.pro create mode 100644 permissions-ranging/src/main/AndroidManifest.xml create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt create mode 100644 permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt create mode 100644 profile-parsers/build.gradle.kts create mode 100644 profile-parsers/module-rules.pro create mode 100644 profile-parsers/src/main/AndroidManifest.xml rename profile_gls/src/main/java/no/nordicsemi/android/gls/data/WorkingMode.kt => profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParser.kt (89%) create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPMStatus.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPSData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMFeatureParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMMeasurementParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMSpecificOpsControlPointParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMStatusParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMCalibrationStatus.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMErrorCode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMFeatures.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMOpCode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMRecord.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMSpecificOpsControlPointData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMStatus.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/CRC16.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/WorkingMode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataSnapshot.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/SpeedUnit.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/WheelSize.kt rename profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt => profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/date/DateTimeParser.kt (51%) create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/Mapper.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/PeripheralBluetoothAddress.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointMode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointResult.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DirectionMode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/QualityIndicator.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/CGMSpecificOpsControlPoint.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementContextParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointInputParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Carbohydrate.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GLSRecord.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseMeasurementUnit.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseStatus.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Health.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Meal.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Medication.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RecordAccessControlPointData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RequestStatus.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Tester.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/BodySensorLocationParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSData.kt rename profile_prx/src/main/java/no/nordicsemi/android/prx/repository/AlarmHandler.kt => profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParser.kt (52%) create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevel.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevelParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlertLevelInputParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/PRXData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPOpCode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPResponseCode.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureData.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSSettingsUnit.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputDataParser.kt create mode 100644 profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputMetrics.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMFeatureParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMMeasurementParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/DateTimeParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParserTest.kt create mode 100644 profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParserTest.kt create mode 100644 profile/build.gradle.kts create mode 100644 profile/module-rules.pro create mode 100644 profile/src/main/AndroidManifest.xml create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileDestination.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/data/UiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/DeviceRepository.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/ChannelSoundingManager.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionCloseReason.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionFailedReason.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionStartTechnology.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/CommentVisitor.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationDataStore.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationRepository.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlConfiguration.java create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlMacro.java create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/battery/BatteryLevelView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/bps/BPSScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/channelSounding/ChannelSoundingScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCUiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthAndElevationSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ControlView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DFSScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceViewChart.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/LinearDataView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/MeasurementDetailsView.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/RangeSlider.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/SectionBluetoothDeviceComponent.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSUiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetails.kt rename profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt => profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetailsUiMapper.kt (60%) create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSUiMapper.kt rename profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt => profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/LineChart.kt (59%) create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSScreen.kt rename profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt => profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSUiMapper.kt (68%) create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/internal/ProfileAppBar.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/lbs/BlinkyScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSUiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputUiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/InputSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/MacroSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/OutputSection.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddConfigurationDialog.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddMacroDialog.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTConfigurationPicker.kt rename {profile_uart/src/main/java/no/nordicsemi/android/uart/view => profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart}/UARTMacroView.kt (51%) create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTScreen.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UartUiMapper.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/rememberImeState.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BPSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BatteryViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CGMSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CSCViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ChannelSoundingViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DeviceConnectionState.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DirectionFinderViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/GLSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HRSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HTSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/LBSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ProfileViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/RSCSViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ThroughputViewModel.kt create mode 100644 profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/UartViewModel.kt create mode 100644 profile/src/main/res/drawable/ic_arrow.xml create mode 100644 profile/src/main/res/drawable/ic_azimuth.xml create mode 100644 profile/src/main/res/drawable/ic_control.xml create mode 100644 profile/src/main/res/drawable/ic_distance.xml create mode 100644 profile/src/main/res/drawable/ic_elevation.xml rename {profile_uart => profile}/src/main/res/drawable/ic_input.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_macro.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_output.xml (100%) create mode 100644 profile/src/main/res/drawable/ic_rscs.xml rename {profile_uart => profile}/src/main/res/drawable/ic_sync_down.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_sync_down_off.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_1.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_2.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_3.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_4.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_5.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_6.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_7.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_8.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_9.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_about.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_down.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_forward.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_left.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_pause.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_play.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_rewind.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_right.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_settings.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_stop.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/ic_uart_up.xml (100%) rename {profile_hrs => profile}/src/main/res/drawable/ic_zoom_in.xml (100%) rename {profile_hrs => profile}/src/main/res/drawable/ic_zoom_out.xml (100%) rename {profile_uart => profile}/src/main/res/drawable/uart_button.xml (100%) create mode 100644 profile/src/main/res/values/blinkyStrings.xml create mode 100644 profile/src/main/res/values/bpsStrings.xml create mode 100644 profile/src/main/res/values/cgmStrings.xml create mode 100644 profile/src/main/res/values/cscStrings.xml create mode 100644 profile/src/main/res/values/dfsStrings.xml rename profile_gls/src/main/res/values/strings.xml => profile/src/main/res/values/glsStrings.xml (75%) create mode 100644 profile/src/main/res/values/hrsStrings.xml create mode 100644 profile/src/main/res/values/htsStrings.xml create mode 100644 profile/src/main/res/values/rscsStrings.xml create mode 100644 profile/src/main/res/values/strings.xml create mode 100644 profile/src/main/res/values/throughputStings.xml create mode 100644 profile/src/main/res/values/uartStrings.xml delete mode 100644 profile_bps/build.gradle.kts delete mode 100644 profile_bps/src/main/AndroidManifest.xml delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewEvent.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt delete mode 100644 profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt delete mode 100644 profile_bps/src/main/res/values/strings.xml delete mode 100644 profile_cgms/build.gradle.kts delete mode 100644 profile_cgms/module-rules.pro delete mode 100644 profile_cgms/src/main/AndroidManifest.xml delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt delete mode 100644 profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt delete mode 100644 profile_cgms/src/main/res/values/strings.xml delete mode 100644 profile_csc/build.gradle.kts delete mode 100644 profile_csc/module-rules.pro delete mode 100644 profile_csc/src/main/AndroidManifest.xml delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/data/SpeedUnit.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCMappers.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt delete mode 100644 profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt delete mode 100644 profile_csc/src/main/res/values/strings.xml create mode 100644 profile_data/build.gradle.kts rename {profile_bps => profile_data}/module-rules.pro (99%) create mode 100644 profile_data/src/main/AndroidManifest.xml create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BPSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BatteryServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CGMServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CSCServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ChannelSoundingServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/GLSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HRSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HTSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/LBSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ProfileServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/RSCSServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ThroughputServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/UARTServiceData.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/directionFinder/SensorDataExt.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroEol.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroIcon.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTConfiguration.kt create mode 100644 profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTMacro.kt rename {profile_hts/src/main/java/no/nordicsemi/android/hts/view => profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uiMapper}/TemperatureUnit.kt (84%) delete mode 100644 profile_gls/build.gradle.kts delete mode 100644 profile_gls/module-rules.pro delete mode 100644 profile_gls/src/main/AndroidManifest.xml delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/GLSServer.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/Field.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/details/viewmodel/GLSDetailsViewModel.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSViewState.kt delete mode 100644 profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt delete mode 100644 profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt delete mode 100644 profile_hrs/build.gradle.kts delete mode 100644 profile_hrs/module-rules.pro delete mode 100644 profile_hrs/src/main/AndroidManifest.xml delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt delete mode 100644 profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt delete mode 100644 profile_hrs/src/main/res/values/strings.xml delete mode 100644 profile_hts/build.gradle.kts delete mode 100644 profile_hts/module-rules.pro delete mode 100644 profile_hts/src/main/AndroidManifest.xml delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSMapper.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt delete mode 100644 profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt delete mode 100644 profile_hts/src/main/res/values/strings.xml create mode 100644 profile_manager/build.gradle.kts create mode 100644 profile_manager/module-rules.pro create mode 100644 profile_manager/src/main/AndroidManifest.xml create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BPSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BatteryManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CGMManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CSCManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ChannelSoundingManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/GLSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HRSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HTSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/LBSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/RSCSManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ThroughputManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/UARTManager.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BPSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BatteryRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CGMRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CSCRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ChannelSoundingRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/GLSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HRSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HTSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/LBSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/PRXRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/RSCSRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ThroughputRepository.kt create mode 100644 profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/UartRepository.kt delete mode 100644 profile_prx/build.gradle.kts delete mode 100644 profile_prx/module-rules.pro delete mode 100644 profile_prx/src/main/AndroidManifest.xml delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXLinkLossView.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXMapper.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt delete mode 100644 profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt delete mode 100644 profile_prx/src/main/res/values/strings.xml delete mode 100644 profile_rscs/build.gradle.kts delete mode 100644 profile_rscs/module-rules.pro delete mode 100644 profile_rscs/src/main/AndroidManifest.xml delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceData.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt delete mode 100644 profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt delete mode 100644 profile_rscs/src/main/res/values/strings.xml delete mode 100644 profile_uart/build.gradle.kts delete mode 100644 profile_uart/module-rules.pro delete mode 100644 profile_uart/src/main/AndroidManifest.xml delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/UartServer.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/ConfigurationDataSource.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroEol.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroIcon.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTConfiguration.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTMacro.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTParser.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTPersistentDataSource.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/CommentVisitor.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/Configuration.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDao.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDatabase.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/InitMigration.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlConfiguration.java delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlMacro.java delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/InputSection.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/MacroSection.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/OutputSection.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddConfigurationDialog.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTConfigurationPicker.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMapper.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViewEvent.kt delete mode 100644 profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt delete mode 100644 profile_uart/src/main/res/values/strings.xml delete mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt delete mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt delete mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt delete mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt delete mode 100644 profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt diff --git a/README.md b/README.md index 8053dc80..2e1c9026 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,12 @@ It contains applications demonstrating standard Bluetooth LE profiles: * **Health Thermometer Monitor**, * **Glucose Monitor**, * **Continuous Glucose Monitor**, -* **Proximity Monitor** +* **Universal Asynchronous Receiver/Transmitter (UART)**, +* **Throughput**, +* **Direction Finder**, +* **Blinky (LBS) Service** -Since version 1.10.0 the *nRF Toolbox* also supports the **Nordic UART Service** which may be used -for bidirectional text communication between devices. +**_NOTE:_** The Proximity profile is not included in this version of the app. If you need it, please download the previous version. ### How to import to Android Studio diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ab4e1bc3..c0d67a60 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,9 +31,10 @@ plugins { alias(libs.plugins.nordic.application.compose) alias(libs.plugins.nordic.hilt) + alias(libs.plugins.kotlin.parcelize) } -if (getGradle().getStartParameter().getTaskRequests().toString().contains("Release")) { +if (getGradle().startParameter.taskRequests.toString().contains("Release")) { apply(plugin = "com.google.gms.google-services") apply(plugin = "com.google.firebase.crashlytics") } @@ -43,44 +44,31 @@ android { } dependencies { - //Hilt requires to implement every module in the main app module - //https://github.com/google/dagger/issues/2123 - implementation(project(":profile_bps")) - implementation(project(":profile_csc")) - implementation(project(":profile_cgms")) - implementation(project(":profile_gls")) - implementation(project(":profile_hrs")) - implementation(project(":profile_hts")) - implementation(project(":profile_prx")) - implementation(project(":profile_rscs")) - - implementation(project(":profile_uart")) - implementation(project(":lib_analytics")) + implementation(project(":profile-parsers")) + implementation(project(":profile_manager")) + implementation(project(":profile")) + implementation(project(":profile_data")) implementation(project(":lib_ui")) implementation(project(":lib_utils")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(libs.nordic.core) - implementation(libs.nordic.theme) implementation(libs.nordic.navigation) - implementation(libs.nordic.blek.uiscanner) + implementation(libs.nordic.theme) implementation(libs.nordic.logger) - implementation(libs.nordic.permissions.ble) implementation(libs.nordic.analytics) - - implementation(libs.nordic.blek.client) + implementation(libs.nordic.ui) + implementation(libs.nordic.core) + implementation(libs.nordic.scanner.ble) implementation(libs.androidx.core.ktx) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.compose.material.iconsExtended) + implementation(libs.androidx.compose.runtime) implementation(libs.androidx.hilt.navigation.compose) // Timber & SLF4J implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) + + implementation(libs.nordic.blek.client.android) } diff --git a/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt index 9379a68b..1b4b5d4e 100644 --- a/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt +++ b/app/src/debug/java/no/nordicsemi/android/nrftoolbox/NrfToolboxApplication.kt @@ -35,8 +35,6 @@ import android.app.Application import dagger.hilt.android.HiltAndroidApp import no.nordicsemi.android.analytics.AppAnalytics import no.nordicsemi.android.analytics.AppOpenEvent -import no.nordicsemi.android.gls.GLSServer -import no.nordicsemi.android.uart.UartServer import timber.log.Timber import javax.inject.Inject @@ -46,20 +44,11 @@ class NrfToolboxApplication : Application() { @Inject lateinit var analytics: AppAnalytics - @Inject - lateinit var uartServer: UartServer - - @Inject - lateinit var glsServer: GLSServer - override fun onCreate() { super.onCreate() analytics.logEvent(AppOpenEvent) - uartServer.start(this) - glsServer.start(this) - Timber.plant(Timber.DebugTree()) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 05f78497..ad112a54 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,13 @@ + + + + + + + diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt index d76a3e6d..e1217da4 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/AppDestination.kt @@ -31,45 +31,10 @@ package no.nordicsemi.android.nrftoolbox -import no.nordicsemi.android.bps.view.BPSScreen -import no.nordicsemi.android.cgms.view.CGMScreen import no.nordicsemi.android.common.navigation.createSimpleDestination import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.csc.view.CSCScreen -import no.nordicsemi.android.gls.main.view.GLSScreen -import no.nordicsemi.android.hrs.view.HRSScreen -import no.nordicsemi.android.hts.view.HTSScreen -import no.nordicsemi.android.nrftoolbox.view.HomeScreen -import no.nordicsemi.android.prx.view.PRXScreen -import no.nordicsemi.android.rscs.view.RSCSScreen -import no.nordicsemi.android.toolbox.scanner.ScannerDestination -import no.nordicsemi.android.uart.view.UARTScreen +import no.nordicsemi.android.nrftoolbox.view.HomeView val HomeDestinationId = createSimpleDestination("home-destination") -val HomeDestinations = listOf( - defineDestination(HomeDestinationId) { HomeScreen() }, - ScannerDestination -) - -val CSCDestinationId = createSimpleDestination("csc-destination") -val HRSDestinationId = createSimpleDestination("hrs-destination") -val HTSDestinationId = createSimpleDestination("hts-destination") -val GLSDestinationId = createSimpleDestination("gls-destination") -val BPSDestinationId = createSimpleDestination("bps-destination") -val PRXDestinationId = createSimpleDestination("prx-destination") -val RSCSDestinationId = createSimpleDestination("rscs-destination") -val CGMSDestinationId = createSimpleDestination("cgms-destination") -val UARTDestinationId = createSimpleDestination("uart-destination") - -val ProfileDestinations = listOf( - defineDestination(CSCDestinationId) { CSCScreen() }, - defineDestination(HRSDestinationId) { HRSScreen() }, - defineDestination(HTSDestinationId) { HTSScreen() }, - defineDestination(GLSDestinationId) { GLSScreen() }, - defineDestination(BPSDestinationId) { BPSScreen() }, - defineDestination(PRXDestinationId) { PRXScreen() }, - defineDestination(RSCSDestinationId) { RSCSScreen() }, - defineDestination(CGMSDestinationId) { CGMScreen() }, - defineDestination(UARTDestinationId) { UARTScreen() }, -) +val HomeDestinations = defineDestination(HomeDestinationId) { HomeView() } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt index e5a8682b..e62e2778 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/MainActivity.kt @@ -42,17 +42,11 @@ import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionRequestDia import no.nordicsemi.android.common.navigation.NavigationView import no.nordicsemi.android.common.theme.NordicActivity import no.nordicsemi.android.common.theme.NordicTheme -import no.nordicsemi.android.gls.GLSDestination -import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals -import no.nordicsemi.android.toolbox.scanner.ScannerDestination -import javax.inject.Inject +import no.nordicsemi.android.toolbox.profile.ProfileDestination @AndroidEntryPoint class MainActivity : NordicActivity() { - @Inject - lateinit var activitySignals: ActivitySignals - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -62,7 +56,7 @@ class MainActivity : NordicActivity() { color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxSize() ) { - NavigationView(HomeDestinations + ProfileDestinations + ScannerDestination + GLSDestination) + NavigationView(HomeDestinations + ScannerDestination + ProfileDestination) } AnalyticsPermissionRequestDialog() @@ -70,8 +64,4 @@ class MainActivity : NordicActivity() { } } - override fun onResume() { - super.onResume() - activitySignals.onResume() - } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ScannerDestination.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ScannerDestination.kt new file mode 100644 index 00000000..84ee223e --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/ScannerDestination.kt @@ -0,0 +1,46 @@ +package no.nordicsemi.android.nrftoolbox + +import androidx.hilt.navigation.compose.hiltViewModel +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.defineDestination +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.common.scanner.DeviceSelected +import no.nordicsemi.android.common.scanner.ScannerScreen +import no.nordicsemi.android.common.scanner.ScanningCancelled +import no.nordicsemi.android.common.scanner.data.OnlyNearby +import no.nordicsemi.android.common.scanner.data.OnlyWithNames +import no.nordicsemi.android.common.scanner.rememberFilterState +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.kotlin.ble.client.android.ScanResult + +val ScannerDestinationId = createDestination("ble-scanner") + +val ScannerDestination = defineDestination(ScannerDestinationId) { + val navigationVM = hiltViewModel() + + ScannerScreen( + cancellable = true, + state = rememberFilterState( + dynamicFilters = listOf( + OnlyNearby(), + OnlyWithNames(), + ) + ), + onResultSelected = { + when (it) { + is DeviceSelected -> { + navigationVM.navigateTo(ProfileDestinationId, it.scanResult.peripheral.address) + { + popUpTo(ScannerDestinationId.toString()) { + inclusive = true + } + } + } + + ScanningCancelled -> { + navigationVM.navigateUp() + } + } + } + ) +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt similarity index 65% rename from app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt rename to app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt index 0e1e58c8..e5960d0b 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/ApplicationScopeModule.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/ApplicationScopeModule.kt @@ -1,10 +1,11 @@ -package no.nordicsemi.android.nrftoolbox +package no.nordicsemi.android.nrftoolbox.di import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @Module @@ -12,5 +13,5 @@ import kotlinx.coroutines.SupervisorJob class ApplicationScopeModule { @Provides - fun applicationScope() = CoroutineScope(SupervisorJob()) + fun applicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.IO) } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt new file mode 100644 index 00000000..e2201029 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/di/CentralManagerModule.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.nrftoolbox.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.kotlin.ble.client.android.CentralManager +import no.nordicsemi.kotlin.ble.client.android.native + +@Module +@InstallIn(SingletonComponent::class) +object CentralManagerModule { + + @Provides + fun provideCentralManager( + @ApplicationContext context: Context, + scope: CoroutineScope + ): CentralManager { + return CentralManager.Factory.native(context, scope) + } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/repository/ActivitySignals.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/repository/ActivitySignals.kt deleted file mode 100644 index 8cde211b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/repository/ActivitySignals.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.nrftoolbox.repository - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ActivitySignals @Inject constructor() { - - private val _onResumeTrigger = MutableStateFlow(false) - val state = _onResumeTrigger.asStateFlow() - - fun onResume() { - _onResumeTrigger.value = !_onResumeTrigger.value - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt index 27fcd0f0..6a74774a 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/FeatureButton.kt @@ -1,120 +1,76 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.nrftoolbox.view import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.colorResource +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import no.nordicsemi.android.nrftoolbox.R @Composable -fun FeatureButton( +internal fun FeatureButton( @DrawableRes iconId: Int, - @StringRes nameCode: Int, - @StringRes name: Int, - isRunning: Boolean? = null, - @StringRes description: Int? = null, + @StringRes description: Int, + profileNames: List = listOf(stringResource(description)), + deviceName: String?, + deviceAddress: String, onClick: () -> Unit ) { - OutlinedCard(onClick = onClick) { + OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) { Row( - modifier = Modifier.padding(16.dp).fillMaxWidth(), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - val color = if (isRunning == true) { - colorResource(id = R.color.nordicGrass) - } else { - MaterialTheme.colorScheme.secondary - } - Image( painter = painterResource(iconId), - contentDescription = stringResource(id = name), - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary), + contentDescription = stringResource(id = description), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), modifier = Modifier - .size(64.dp) - .clip(CircleShape) - .background(color) - .padding(16.dp) + .size(40.dp) ) - Spacer(modifier = Modifier.size(16.dp)) - - Column(modifier = Modifier.weight(1f)) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = name), - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center + text = deviceName ?: stringResource(R.string.unknown_device), + style = MaterialTheme.typography.titleMedium, + ) + Text( + modifier = Modifier.fillMaxWidth(), + text = profileNames.joinToString(", "), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - description?.let { - Spacer(modifier = Modifier.size(4.dp)) - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(id = it), - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Text( + modifier = Modifier.fillMaxWidth(), + text = deviceAddress, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) } } } @@ -123,5 +79,64 @@ fun FeatureButton( @Preview @Composable private fun FeatureButtonPreview() { - FeatureButton(R.drawable.ic_csc, R.string.csc_module, R.string.csc_module_full) { } + FeatureButton( + R.drawable.ic_csc, + R.string.csc_module_full, + listOf("Cycling Speed and Cadence", "Cycling Speed Sensor"), + "Testing peripheral", + deviceAddress = "AA:BB:CC:DD:EE:FF", + ) { } +} + +@Composable +internal fun FeatureButton( + iconId: ImageVector, + @StringRes description: Int, + profileNames: List = listOf(stringResource(description)), + deviceName: String?, + deviceAddress: String, + onClick: () -> Unit +) { + OutlinedCard(onClick = onClick, modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + imageVector = iconId, + contentDescription = deviceAddress, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + modifier = Modifier + .size(40.dp) + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = deviceName ?: stringResource(R.string.unknown_device), + style = MaterialTheme.typography.titleMedium, + ) + + Text( + modifier = Modifier.fillMaxWidth(), + text = profileNames.joinToString(", "), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + modifier = Modifier.fillMaxWidth(), + text = deviceAddress, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt index f3765ada..d5c3b0c6 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeView.kt @@ -1,263 +1,344 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.nrftoolbox.view -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Lightbulb +import androidx.compose.material.icons.filled.SocialDistance +import androidx.compose.material.icons.filled.SyncAlt +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.draw.alpha import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.analytics.Link -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileOpenEvent -import no.nordicsemi.android.nrftoolbox.BPSDestinationId -import no.nordicsemi.android.nrftoolbox.BuildConfig -import no.nordicsemi.android.nrftoolbox.CGMSDestinationId -import no.nordicsemi.android.nrftoolbox.CSCDestinationId -import no.nordicsemi.android.nrftoolbox.GLSDestinationId -import no.nordicsemi.android.nrftoolbox.HRSDestinationId -import no.nordicsemi.android.nrftoolbox.HTSDestinationId -import no.nordicsemi.android.nrftoolbox.PRXDestinationId +import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionButton +import no.nordicsemi.android.common.ui.view.NordicAppBar import no.nordicsemi.android.nrftoolbox.R -import no.nordicsemi.android.nrftoolbox.RSCSDestinationId -import no.nordicsemi.android.nrftoolbox.UARTDestinationId import no.nordicsemi.android.nrftoolbox.viewmodel.HomeViewModel +import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent +import no.nordicsemi.android.toolbox.lib.utils.Profile -private const val DFU_PACKAGE_NAME = "no.nordicsemi.android.dfu" -private const val DFU_LINK = "https://play.google.com/store/apps/details?id=no.nordicsemi.android.dfu" - -private const val LOGGER_PACKAGE_NAME = "no.nordicsemi.android.log" - +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen() { - val viewModel: HomeViewModel = hiltViewModel() +internal fun HomeView() { + val viewModel = hiltViewModel() val state by viewModel.state.collectAsStateWithLifecycle() + val onEvent: (UiEvent) -> Unit = { viewModel.onClickEvent(it) } Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), topBar = { - TitleAppBar(stringResource(id = R.string.app_name)) - } - ) { - Column( - modifier = Modifier - .padding(it) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) + NordicAppBar( + title = { Text(stringResource(id = R.string.app_name)) }, + showBackButton = false, ) { - Spacer(modifier = Modifier.height(16.dp)) + AnalyticsPermissionButton() + } + }, + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = { onEvent(UiEvent.OnConnectDeviceClick) }, + modifier = Modifier.padding(top = 8.dp, bottom = 16.dp, end = 8.dp, start = 8.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add device from scanner" + ) + Text(text = stringResource(R.string.connect_device)) + } + } + } + ) { paddingValues -> + // Get notch padding for devices with a display cutout (notch) + val notchPadding = WindowInsets.displayCutout + .union(WindowInsets(left = 8.dp, right = 8.dp, top = 8.dp, bottom = 8.dp)) + .only(WindowInsetsSides.Horizontal) + .asPaddingValues() + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .padding(paddingValues), + contentPadding = notchPadding, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + item { + // Show the title at the top Text( - text = stringResource(id = R.string.viewmodel_profiles), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.connected_devices), + modifier = Modifier + .alpha(0.5f) + .padding(start = 16.dp), ) + if (state.connectedDevices.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + state.connectedDevices.values.forEach { (peripheral, services) -> + // Skip if no services + if (services.isEmpty()) return@forEach + // Case 1: If only one service, show it directly like battery service + if (services.size == 1 && services.first().profile == Profile.BATTERY) { + FeatureButton( + iconId = R.drawable.ic_battery, + description = R.string.battery_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + services.first().profile + ) + ) + }, + ) + } + // Case 2: Show the first *non-Battery* profile. + // This ensures only one service is shown per peripheral when multiple services are available. + services.firstOrNull { it.profile != Profile.BATTERY } + ?.let { serviceManager -> + when (serviceManager.profile) { + Profile.HRS -> FeatureButton( + iconId = R.drawable.ic_hrs, + description = R.string.hrs_module_full, + deviceName = peripheral.name, + profileNames = services.map { it.profile.toString() }, + deviceAddress = peripheral.address, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - Spacer(modifier = Modifier.height(16.dp)) + Profile.HTS -> FeatureButton( + iconId = R.drawable.ic_hts, + description = R.string.hts_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - FeatureButton(R.drawable.ic_gls, R.string.gls_module, R.string.gls_module_full) { - viewModel.openProfile(GLSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.GLS)) - } + Profile.BPS -> FeatureButton( + iconId = R.drawable.ic_bps, + description = R.string.bps_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - Spacer(modifier = Modifier.height(16.dp)) + Profile.GLS -> FeatureButton( + iconId = R.drawable.ic_gls, + description = R.string.gls_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - FeatureButton(R.drawable.ic_bps, R.string.bps_module, R.string.bps_module_full) { - viewModel.openProfile(BPSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.BPS)) - } + Profile.CGM -> FeatureButton( + iconId = R.drawable.ic_cgm, + description = R.string.cgm_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - Spacer(modifier = Modifier.height(16.dp)) + Profile.RSCS -> FeatureButton( + iconId = R.drawable.ic_rscs, + description = R.string.rscs_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - Text( - text = stringResource(id = R.string.service_profiles), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), - ) + Profile.DFS -> FeatureButton( + iconId = R.drawable.ic_distance, + description = R.string.direction_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - Spacer(modifier = Modifier.height(16.dp)) + Profile.CSC -> FeatureButton( + iconId = R.drawable.ic_csc, + description = R.string.csc_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) - FeatureButton( - R.drawable.ic_csc, - R.string.csc_module, - R.string.csc_module_full, - state.isCSCModuleRunning - ) { - viewModel.openProfile(CSCDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.CSC)) - } + Profile.THROUGHPUT -> { + FeatureButton( + iconId = Icons.Default.SyncAlt, + description = R.string.throughput_module, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) + } - Spacer(modifier = Modifier.height(16.dp)) + Profile.UART -> { + FeatureButton( + iconId = R.drawable.ic_uart, + description = R.string.uart_module_full, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) + } - FeatureButton( - R.drawable.ic_hrs, - R.string.hrs_module, - R.string.hrs_module_full, - state.isHRSModuleRunning - ) { - viewModel.openProfile(HRSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.HRS)) - } + Profile.CHANNEL_SOUNDING -> { + FeatureButton( + iconId = Icons.Default.SocialDistance, + description = R.string.channel_sounding_module, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) + } - Spacer(modifier = Modifier.height(16.dp)) + Profile.LBS -> { + FeatureButton( + iconId = Icons.Default.Lightbulb, + description = R.string.lbs_blinky_module, + deviceName = peripheral.name, + deviceAddress = peripheral.address, + profileNames = services.map { it.profile.toString() }, + onClick = { + onEvent( + UiEvent.OnDeviceClick( + peripheral.address, + serviceManager.profile + ) + ) + }, + ) + } - FeatureButton( - R.drawable.ic_hts, - R.string.hts_module, - R.string.hts_module_full, - state.isHTSModuleRunning - ) { - viewModel.openProfile(HTSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.HTS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_rscs, - R.string.rscs_module, - R.string.rscs_module_full, - state.isRSCSModuleRunning - ) { - viewModel.openProfile(RSCSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.RSCS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_cgm, - R.string.cgm_module, - R.string.cgm_module_full, - state.isCGMModuleRunning - ) { - viewModel.openProfile(CGMSDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.CGMS)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_prx, - R.string.prx_module, - R.string.prx_module_full, - state.isPRXModuleRunning - ) { - viewModel.openProfile(PRXDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.PRX)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.utils_services), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer(modifier = Modifier.height(16.dp)) - - FeatureButton( - R.drawable.ic_uart, - R.string.uart_module, - R.string.uart_module_full, - state.isUARTModuleRunning - ) { - viewModel.openProfile(UARTDestinationId) - viewModel.logEvent(ProfileOpenEvent(Profile.UART)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - val uriHandler = LocalUriHandler.current - val context = LocalContext.current - val packageManger = context.packageManager - - val description = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME)?.let { - R.string.dfu_module_info - } ?: R.string.dfu_module_install - - FeatureButton(R.drawable.ic_dfu, R.string.dfu_module, R.string.dfu_module_full, null, description) { - val intent = packageManger.getLaunchIntentForPackage(DFU_PACKAGE_NAME) - if (intent != null) { - context.startActivity(intent) - } else { - uriHandler.openUri(DFU_LINK) + else -> { + // TODO: Add more profiles + } + } + } + } } - viewModel.logEvent(ProfileOpenEvent(Link.DFU)) + } else { + NoConnectedDeviceView() } - - Spacer(modifier = Modifier.height(16.dp)) - - val loggerDescription = packageManger.getLaunchIntentForPackage(LOGGER_PACKAGE_NAME)?.let { - R.string.logger_module_info - } ?: R.string.dfu_module_install - - FeatureButton( - R.drawable.ic_logger, - R.string.logger_module, - R.string.logger_module_full, - null, - loggerDescription - ) { - viewModel.openLogger() - viewModel.logEvent(ProfileOpenEvent(Link.LOGGER)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.fillMaxWidth(), - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(16.dp)) + } + item { + Links { onEvent(it) } } } } diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt deleted file mode 100644 index 5fc11ff4..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/HomeViewState.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.nrftoolbox.view - -data class HomeViewState( - val isCSCModuleRunning: Boolean = false, - val isHRSModuleRunning: Boolean = false, - val isHTSModuleRunning: Boolean = false, - val isRSCSModuleRunning: Boolean = false, - val isPRXModuleRunning: Boolean = false, - val isCGMModuleRunning: Boolean = false, - val isUARTModuleRunning: Boolean = false, - val refreshToggle: Boolean = false -) { - - fun copyWithRefresh(): HomeViewState { - return copy(refreshToggle = !refreshToggle) - } -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt new file mode 100644 index 00000000..5053d384 --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/Links.kt @@ -0,0 +1,103 @@ +package no.nordicsemi.android.nrftoolbox.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Code +import androidx.compose.material.icons.filled.Language +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.nrftoolbox.R +import no.nordicsemi.android.nrftoolbox.viewmodel.UiEvent + +@Composable +internal fun Links(onEvent: (UiEvent) -> Unit) { + Column { + Text( + text = stringResource(R.string.links), + modifier = Modifier + .alpha(0.5f) + .padding(start = 16.dp), + ) + OutlinedCard( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp)) + .clickable { onEvent(UiEvent.OnGitHubClick) } + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Code, + contentDescription = stringResource(R.string.github_repo), + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.github_repo), + style = MaterialTheme.typography.bodyLarge, + ) + } + } + + HorizontalDivider() + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)) + .clickable { onEvent(UiEvent.OnNordicDevZoneClick) } + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Language, + contentDescription = stringResource(R.string.nordic_dev_zone), + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.nordic_dev_zone), + style = MaterialTheme.typography.bodyLarge, + ) + } + } + } + } +} + +@Preview +@Composable +private fun LinksPreview() { + Links { } +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/NoConnectedDeviceView.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/NoConnectedDeviceView.kt new file mode 100644 index 00000000..dcfc808c --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/NoConnectedDeviceView.kt @@ -0,0 +1,83 @@ +package no.nordicsemi.android.nrftoolbox.view + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.nrftoolbox.R + +@Composable +internal fun NoConnectedDeviceView() { + val infiniteTransition = rememberInfiniteTransition() + val scale by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 1.1f, + animationSpec = infiniteRepeatable( + animation = tween(1000, easing = LinearEasing), + repeatMode = RepeatMode.Reverse + ) + ) + OutlinedCard(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.ic_notification_icon), + contentDescription = null, + modifier = Modifier + .size(80.dp) + .graphicsLayer { + scaleX = scale + scaleY = scale + }, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + ) + + Text( + text = stringResource(R.string.device_not_connected_title), + style = MaterialTheme.typography.titleMedium, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(R.string.device_not_connected_message), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun NoConnectedDeviceViewPreview() { + NordicTheme { + NoConnectedDeviceView() + } +} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt deleted file mode 100644 index ac65aa3b..00000000 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/view/TitleAppBar.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.nrftoolbox.view - -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.colorResource -import no.nordicsemi.android.common.analytics.view.AnalyticsPermissionButton -import no.nordicsemi.android.nrftoolbox.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TitleAppBar(text: String) { - TopAppBar( - title = { Text(text, maxLines = 2) }, - colors = TopAppBarDefaults.topAppBarColors( - scrolledContainerColor = MaterialTheme.colorScheme.primary, - containerColor = colorResource(id = R.color.appBarColor), - titleContentColor = MaterialTheme.colorScheme.onPrimary, - actionIconContentColor = MaterialTheme.colorScheme.onPrimary, - navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, - ), - actions = { - AnalyticsPermissionButton() - } - ) -} diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt index 94b1434a..74216336 100644 --- a/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/HomeViewModel.kt @@ -1,123 +1,74 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - package no.nordicsemi.android.nrftoolbox.viewmodel -import android.content.Context +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import no.nordicsemi.android.analytics.AppAnalytics +import no.nordicsemi.android.analytics.Link import no.nordicsemi.android.analytics.ProfileOpenEvent -import no.nordicsemi.android.cgms.repository.CGMRepository -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.common.navigation.DestinationId import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.csc.repository.CSCRepository -import no.nordicsemi.android.hrs.service.HRSRepository -import no.nordicsemi.android.hts.repository.HTSRepository -import no.nordicsemi.android.nrftoolbox.repository.ActivitySignals -import no.nordicsemi.android.nrftoolbox.view.HomeViewState -import no.nordicsemi.android.prx.repository.PRXRepository -import no.nordicsemi.android.rscs.repository.RSCSRepository -import no.nordicsemi.android.uart.repository.UARTRepository +import no.nordicsemi.android.nrftoolbox.ScannerDestinationId +import no.nordicsemi.android.toolbox.profile.manager.ServiceManager +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import no.nordicsemi.kotlin.ble.client.android.Peripheral import javax.inject.Inject -@HiltViewModel -class HomeViewModel @Inject constructor( - @ApplicationContext - private val context: Context, - private val navigationManager: Navigator, - private val activitySignals: ActivitySignals, - cgmRepository: CGMRepository, - cscRepository: CSCRepository, - hrsRepository: HRSRepository, - htsRepository: HTSRepository, - prxRepository: PRXRepository, - rscsRepository: RSCSRepository, - uartRepository: UARTRepository, - private val analytics: AppAnalytics -) : ViewModel() { +internal data class HomeViewState( + val connectedDevices: Map>> = emptyMap(), +) +private const val GITHUB_REPO_URL = "https://github.com/NordicSemiconductor/Android-nRF-Toolbox.git" +private const val NORDIC_DEV_ZONE_URL = "https://devzone.nordicsemi.com/" + +@HiltViewModel +internal class HomeViewModel @Inject constructor( + private val navigator: Navigator, + deviceRepository: DeviceRepository, + private val analytics: AppAnalytics, +) : ViewModel() { private val _state = MutableStateFlow(HomeViewState()) val state = _state.asStateFlow() init { - cgmRepository.isRunning.onEach { - _state.value = _state.value.copy(isCGMModuleRunning = it) - }.launchIn(viewModelScope) - - cscRepository.isRunning.onEach { - _state.value = _state.value.copy(isCSCModuleRunning = it) - }.launchIn(viewModelScope) - - hrsRepository.isRunning.onEach { - _state.value = _state.value.copy(isHRSModuleRunning = it) - }.launchIn(viewModelScope) - - htsRepository.isRunning.onEach { - _state.value = _state.value.copy(isHTSModuleRunning = it) - }.launchIn(viewModelScope) - - prxRepository.isRunning.onEach { - _state.value = _state.value.copy(isPRXModuleRunning = it) - }.launchIn(viewModelScope) - - rscsRepository.isRunning.onEach { - _state.value = _state.value.copy(isRSCSModuleRunning = it) - }.launchIn(viewModelScope) - - uartRepository.isRunning.onEach { - _state.value = _state.value.copy(isUARTModuleRunning = it) - }.launchIn(viewModelScope) - - activitySignals.state.onEach { - _state.value = _state.value.copyWithRefresh() + // Observe connected devices from the repository + deviceRepository.connectedDevices.onEach { devices -> + _state.update { currentState -> + currentState.copy(connectedDevices = devices) + } }.launchIn(viewModelScope) } - fun openProfile(destination: DestinationId) { - navigationManager.navigateTo(destination) + fun onClickEvent(event: UiEvent) { + when (event) { + UiEvent.OnConnectDeviceClick -> navigator.navigateTo(ScannerDestinationId) + is UiEvent.OnDeviceClick -> { + // Log the event for analytics. + analytics.logEvent(ProfileOpenEvent(event.profile)) + + navigator.navigateTo( + ProfileDestinationId, event.deviceAddress + ) + } + + UiEvent.OnGitHubClick -> { + // Log the event for analytics. + analytics.logEvent(ProfileOpenEvent(Link.GITHUB)) + navigator.open(GITHUB_REPO_URL.toUri()) + } + + UiEvent.OnNordicDevZoneClick -> { + // Log the event for analytics. + analytics.logEvent(ProfileOpenEvent(Link.DEV_ACADEMY)) + navigator.open(NORDIC_DEV_ZONE_URL.toUri()) + } + } } - fun openLogger() { - LoggerLauncher.launch(context, null) - } - - fun logEvent(event: ProfileOpenEvent) { - analytics.logEvent(event) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/UiEvent.kt b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/UiEvent.kt new file mode 100644 index 00000000..3c3a272c --- /dev/null +++ b/app/src/main/java/no/nordicsemi/android/nrftoolbox/viewmodel/UiEvent.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.nrftoolbox.viewmodel + +import no.nordicsemi.android.toolbox.lib.utils.Profile + +/** + * HomeViewEvent is a sealed interface that represents the events that can be emitted by the Home view. + */ +sealed interface UiEvent { + + /** OnConnectDeviceClick event that is emitted when the user clicks on the Connect Device button. */ + data object OnConnectDeviceClick : UiEvent + + /** OnDeviceClick event is emitted when the user clicks on a connected device. */ + data class OnDeviceClick(val deviceAddress: String, val profile: Profile) : UiEvent + + /** + * OnGitHubClick event is emitted when the user clicks on the GitHub repository option. + */ + data object OnGitHubClick : UiEvent + + /** OnNordicDevZoneClick event is emitted when the user clicks on the Nordic DevZone option. */ + data object OnNordicDevZoneClick : UiEvent +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gls.xml b/app/src/main/res/drawable/ic_gls.xml deleted file mode 100644 index f2317221..00000000 --- a/app/src/main/res/drawable/ic_gls.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 198dc811..701a8780 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,33 +32,68 @@ CSC Cyclic Speed and Cadence + HRS Heart Rate + GLS Glucose + HTS Health Thermometer + BPS Blood Pressure + RSCS Running Speed and Cadence + PRX Proximity + CGMS Continuous Glucose + UART Universal Asynchronous Receiver/Transmitter (UART) + DFU Device Firmware Update Open DFU application. Download from Google Play. + nRF Logger nRF Logger Open nRF Logger application. + DF + Direction Finder + ViewModel profiles Service profiles Utils services Icon indicating if the profile is running + + BATTERY + Battery + + CHANNEL SOUNDING + + Throughput + Unknown Device + + LBS/Blinky + + NO DEVICES CONNECTED + Tap Connect device button to begin. + + Connect Device + Connected devices + + Source code (GitHub) + Help (Nordic DevZone) + + Links + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e5fd6cc9..28a54c27 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -28,10 +28,9 @@ # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # - -#Mon Feb 14 14:46:55 CET 2022 +#Thu Jul 03 10:42:00 CEST 2025 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/lib_analytics/build.gradle.kts b/lib_analytics/build.gradle.kts index 7e2986c8..9f848c1e 100644 --- a/lib_analytics/build.gradle.kts +++ b/lib_analytics/build.gradle.kts @@ -38,6 +38,7 @@ android { } dependencies { + implementation(project(":lib_utils")) implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) diff --git a/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Profiles.kt b/lib_analytics/src/main/java/no/nordicsemi/android/analytics/AnalyticsMode.kt similarity index 85% rename from lib_analytics/src/main/java/no/nordicsemi/android/analytics/Profiles.kt rename to lib_analytics/src/main/java/no/nordicsemi/android/analytics/AnalyticsMode.kt index 273ce300..9be715ff 100644 --- a/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Profiles.kt +++ b/lib_analytics/src/main/java/no/nordicsemi/android/analytics/AnalyticsMode.kt @@ -31,24 +31,20 @@ package no.nordicsemi.android.analytics -enum class Profile(val displayName: String) { - BPS("BPS"), - CGMS("CGMS"), - CSC("CSC"), - GLS("GLS"), - HRS("HRS"), - HTS("HTS"), - PRX("PRX"), - RSCS("RSCS"), - UART("UART"); -} - +/** + * Represents the different links that can be used in the application. + */ enum class Link(val displayName: String) { - DFU("DFU"), - LOGGER("LOGGER"); + LOGGER("LOGGER"), + GITHUB("GITHUB"), + DEV_ACADEMY("DEV_ACADEMY"), } +/** + * Represents the mode of the UART service. + * Used to determine how the UART service should behave. + */ enum class UARTMode(val displayName: String) { - MACRO("MACRO"), + PRESET("PRESET"), TEXT("TEXT") } \ No newline at end of file diff --git a/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Events.kt b/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Events.kt index 16f78812..534018cb 100644 --- a/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Events.kt +++ b/lib_analytics/src/main/java/no/nordicsemi/android/analytics/Events.kt @@ -32,15 +32,39 @@ package no.nordicsemi.android.analytics import android.os.Bundle +import no.nordicsemi.android.toolbox.lib.utils.Profile +/** + * Base class for Firebase Analytics events. + */ sealed class FirebaseEvent(val eventName: String, val params: Bundle?) +/** + * Represents an event that is logged when the app is opened. + * This event does not carry any additional parameters. + */ data object AppOpenEvent : FirebaseEvent("APP_OPEN", null) +/** + * Represents an event that is logged when profile is opened. + * This event can be created with a [Profile] or a [Link]. + */ class ProfileOpenEvent : FirebaseEvent { - constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.displayName)) + /** + * Creates a new instance of [ProfileOpenEvent] with the given profile. + * The profile's string representation is used as a parameter. + * + * @param profile The profile that was opened. + */ + constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.toString())) + /** + * Creates a new instance of [ProfileOpenEvent] with the given link. + * The link's display name is used as a parameter. + * + * @param link The link that was opened. + */ constructor(link: Link) : super(EVENT_NAME, createBundle(link.displayName)) companion object { @@ -48,11 +72,12 @@ class ProfileOpenEvent : FirebaseEvent { } } -class ProfileConnectedEvent : FirebaseEvent { - - constructor(profile: Profile) : super(EVENT_NAME, createBundle(profile.displayName)) - - constructor(link: Link) : super(EVENT_NAME, createBundle(link.displayName)) +/** + * Represents an event that is logged when a profile is connected. + * This event can be created with a [Profile] or a [Link]. + */ +class ProfileConnectedEvent(profile: Profile) : + FirebaseEvent(EVENT_NAME, createBundle(profile.toString())) { companion object { private const val EVENT_NAME = "PROFILE_CONNECTED" @@ -61,13 +86,28 @@ class ProfileConnectedEvent : FirebaseEvent { const val PROFILE_PARAM_KEY = "PROFILE_NAME" +/** + * Creates a [Bundle] with the given profile name. + * + * @param name The name of the profile to be included in the bundle. + * @return A [Bundle] containing the profile name. + */ private fun createBundle(name: String): Bundle { return Bundle().apply { putString(PROFILE_PARAM_KEY, name) } } -sealed class UARTAnalyticsEvent(eventName: String, params: Bundle?) : FirebaseEvent(eventName, params) +/** + * Represents an event related to UART (Universal Asynchronous Receiver-Transmitter) analytics. + */ +sealed class UARTAnalyticsEvent(eventName: String, params: Bundle?) : + FirebaseEvent(eventName, params) -class UARTSendAnalyticsEvent(mode: UARTMode) : UARTAnalyticsEvent("UART_SEND_EVENT", createParams(mode)) { +/** + * Represents an event that is logged when a UART message is send or received. + * This event can be created with a [UARTMode]. + */ +class UARTSendAnalyticsEvent(mode: UARTMode) : + UARTAnalyticsEvent("UART_SEND_EVENT", createParams(mode)) { companion object { fun createParams(mode: UARTMode) = Bundle().apply { @@ -76,6 +116,14 @@ class UARTSendAnalyticsEvent(mode: UARTMode) : UARTAnalyticsEvent("UART_SEND_EVE } } +/** + * Represents an event that is logged when a UART preset configuration is created. + * This event can be created with a [UARTMode]. + */ class UARTCreateConfiguration : UARTAnalyticsEvent("UART_CREATE_CONF", null) +/** + * Represents an event that is logged when a UART preset configuration is changed. + * This event does not carry any additional parameters. + */ class UARTChangeConfiguration : UARTAnalyticsEvent("UART_CHANGE_CONF", null) diff --git a/lib_scanner/build.gradle.kts b/lib_scanner/build.gradle.kts deleted file mode 100644 index 9a3684ce..00000000 --- a/lib_scanner/build.gradle.kts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) -} - -android { - namespace = "no.nordicsemi.android.toolbox.scanner" -} - -dependencies { - implementation(libs.nordic.navigation) - - implementation(libs.nordic.blek.uiscanner) - implementation(libs.nordic.blek.scanner) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) -} diff --git a/lib_scanner/module-rules.pro b/lib_scanner/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/lib_scanner/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt b/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt deleted file mode 100644 index 9c44d07c..00000000 --- a/lib_scanner/src/main/java/no/nordicsemi/android/toolbox/scanner/ScannerDestination.kt +++ /dev/null @@ -1,29 +0,0 @@ -package no.nordicsemi.android.toolbox.scanner - -import android.os.ParcelUuid -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.common.navigation.createDestination -import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.ui.scanner.DeviceSelected -import no.nordicsemi.android.kotlin.ble.ui.scanner.ScannerScreen -import no.nordicsemi.android.kotlin.ble.ui.scanner.ScanningCancelled - -val ScannerDestinationId = createDestination("uiscanner-destination") - -val ScannerDestination = defineDestination(ScannerDestinationId) { - val navigationViewModel = hiltViewModel() - - val arg = navigationViewModel.parameterOf(ScannerDestinationId) - - ScannerScreen( - uuid = arg, - onResult = { - when (it) { - is DeviceSelected -> navigationViewModel.navigateUpWithResult(ScannerDestinationId, it.scanResults.device) - ScanningCancelled -> navigationViewModel.navigateUp() - } - } - ) -} diff --git a/lib_service/build.gradle.kts b/lib_service/build.gradle.kts index 566cef72..7976d9c1 100644 --- a/lib_service/build.gradle.kts +++ b/lib_service/build.gradle.kts @@ -31,7 +31,6 @@ plugins { alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) } android { @@ -40,11 +39,16 @@ android { dependencies { implementation(project(":lib_ui")) - - implementation(libs.nordic.blek.uiscanner) - implementation(libs.nordic.blek.core) + implementation(project(":lib_utils")) + implementation(project(":profile_manager")) + + implementation(libs.nordic.logger) + implementation(libs.nordic.log.timber) + implementation(libs.nordic.blek.client.android) implementation(libs.androidx.lifecycle.service) implementation(libs.androidx.localbroadcastmanager) implementation(libs.androidx.core) + + implementation(libs.slf4j.timber) } diff --git a/lib_service/src/main/AndroidManifest.xml b/lib_service/src/main/AndroidManifest.xml index 3931a3c2..9961c3eb 100644 --- a/lib_service/src/main/AndroidManifest.xml +++ b/lib_service/src/main/AndroidManifest.xml @@ -30,13 +30,4 @@ ~ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. --> - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt b/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt deleted file mode 100644 index 31bdb0ee..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/DisconnectAndStopEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package no.nordicsemi.android.service - -class DisconnectAndStopEvent diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt index 0b5c8515..41d81211 100644 --- a/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt +++ b/lib_service/src/main/java/no/nordicsemi/android/service/NotificationService.kt @@ -54,11 +54,19 @@ abstract class NotificationService : LifecycleService() { override fun onDestroy() { // when user has disconnected from the sensor, we have to cancel the notification that we've created some milliseconds before using unbindService - cancelNotification() stopForegroundService() + stopSelf() super.onDestroy() } + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + // This method is called when user removed the app from recent app list. + // By default, the service will be killed and recreated immediately after that. + // However, all managed devices will be lost and devices will be disconnected. + stopSelf() + } + /** * Sets the service as a foreground service */ @@ -77,7 +85,7 @@ abstract class NotificationService : LifecycleService() { /** * Stops the service as a foreground service */ - private fun stopForegroundService() { + fun stopForegroundService() { // when the activity rebinds to the service, remove the notification and stop the foreground service // on devices running Android 8.0 (Oreo) or above if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -85,6 +93,7 @@ abstract class NotificationService : LifecycleService() { } else { cancelNotification() } + stopSelf() // Ensure the service stops when it's no longer needed } /** @@ -102,9 +111,9 @@ abstract class NotificationService : LifecycleService() { val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) return NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle(getString(R.string.app_name)) .setContentText(getString(messageResId, "Device")) - .setSmallIcon(R.drawable.ic_launcher_foreground) .setColor(ContextCompat.getColor(this, R.color.md_theme_primary)) .setContentIntent(pendingIntent) .build() diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/OpenLoggerEvent.kt b/lib_service/src/main/java/no/nordicsemi/android/service/OpenLoggerEvent.kt deleted file mode 100644 index 52d8ed38..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/OpenLoggerEvent.kt +++ /dev/null @@ -1,3 +0,0 @@ -package no.nordicsemi.android.service - -class OpenLoggerEvent diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt deleted file mode 100644 index c9ae50fc..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManager.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.service - -import no.nordicsemi.android.kotlin.ble.core.ServerDevice - -const val DEVICE_DATA = "device-data" - -interface ServiceManager { - - fun startService(service: Class, device: ServerDevice) -} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt deleted file mode 100644 index b9b0eba9..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerHiltModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package no.nordicsemi.android.service - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -class ServiceManagerHiltModule { - - @Provides - fun createServiceManager( - @ApplicationContext - context: Context, - ): ServiceManager { - return ServiceManagerImpl(context) - } -} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt b/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt deleted file mode 100644 index 1872a84d..00000000 --- a/lib_service/src/main/java/no/nordicsemi/android/service/ServiceManagerImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package no.nordicsemi.android.service - -import android.content.Context -import android.content.Intent -import dagger.hilt.android.qualifiers.ApplicationContext -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import javax.inject.Inject - -class ServiceManagerImpl @Inject constructor( - @ApplicationContext - private val context: Context -): ServiceManager { - - override fun startService(service: Class, device: ServerDevice) { - val intent = Intent(context, service).apply { - putExtra(DEVICE_DATA, device) - } - context.startService(intent) - } -} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/di/ProfileServiceManagerImpModule.kt b/lib_service/src/main/java/no/nordicsemi/android/service/di/ProfileServiceManagerImpModule.kt new file mode 100644 index 00000000..3f4e050c --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/di/ProfileServiceManagerImpModule.kt @@ -0,0 +1,21 @@ +package no.nordicsemi.android.service.di + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.service.profile.ProfileServiceManager +import no.nordicsemi.android.service.profile.ProfileServiceManagerImp + +@Module +@InstallIn(SingletonComponent::class) +object ProfileServiceManagerImpModule { + + @Provides + fun provideServiceManager( + @ApplicationContext context: Context + ): ProfileServiceManager = ProfileServiceManagerImp(context) + +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileService.kt b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileService.kt new file mode 100644 index 00000000..fec9c4e2 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileService.kt @@ -0,0 +1,321 @@ +package no.nordicsemi.android.service.profile + +import android.content.Intent +import android.os.Binder +import android.os.IBinder +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import no.nordicsemi.android.log.timber.nRFLoggerTree +import no.nordicsemi.android.service.NotificationService +import no.nordicsemi.android.service.R +import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID +import no.nordicsemi.android.toolbox.profile.manager.ServiceManager +import no.nordicsemi.android.toolbox.profile.manager.ServiceManagerFactory +import no.nordicsemi.android.ui.view.internal.DisconnectReason +import no.nordicsemi.kotlin.ble.client.android.CentralManager +import no.nordicsemi.kotlin.ble.client.android.CentralManager.ConnectionOptions +import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.core.BondState +import no.nordicsemi.kotlin.ble.core.ConnectionState +import no.nordicsemi.kotlin.ble.core.Manager +import no.nordicsemi.kotlin.ble.core.Phy +import no.nordicsemi.kotlin.ble.core.PhyOption +import no.nordicsemi.kotlin.ble.core.WriteType +import timber.log.Timber +import javax.inject.Inject +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +@AndroidEntryPoint +internal class ProfileService : NotificationService() { + + @Inject + lateinit var centralManager: CentralManager + private var logger: nRFLoggerTree? = null + private val binder = LocalBinder() + + private val _connectedDevices = + MutableStateFlow>>>(emptyMap()) + private val _isMissingServices = MutableStateFlow(false) + private val _disconnectionReason = MutableStateFlow(null) + + private val connectionJobs = mutableMapOf() + private val serviceHandlingJob = mutableMapOf() + + override fun onBind(intent: Intent): IBinder { + super.onBind(intent) + return binder + } + + override fun onCreate() { + super.onCreate() + // Observe the Bluetooth state + centralManager.state.onEach { state -> + if (state == Manager.State.POWERED_OFF) { + _disconnectionReason.tryEmit(CustomReason(DisconnectReason.BLUETOOTH_OFF)) + } + }.launchIn(lifecycleScope) + } + + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + intent?.getStringExtra(DEVICE_ADDRESS)?.let { deviceAddress -> + initLogger(deviceAddress) + initiateConnection(deviceAddress) + } + return START_REDELIVER_INTENT + } + + inner class LocalBinder : Binder(), ServiceApi { + override val connectedDevices: Flow>>> + get() = _connectedDevices.asSharedFlow() + + override val isMissingServices: Flow + get() = _isMissingServices.asStateFlow() + + override val disconnectionReason: Flow + get() = _disconnectionReason.asStateFlow() + + override suspend fun getMaxWriteValue(address: String, writeType: WriteType): Int? { + val peripheral = getPeripheralById(address) ?: return null + if (!peripheral.isConnected) return null + + return try { + peripheral.requestHighestValueLength() + peripheral.requestConnectionPriority(ConnectionPriority.HIGH) + peripheral.setPreferredPhy(Phy.PHY_LE_2M, Phy.PHY_LE_2M, PhyOption.S2) + peripheral.maximumWriteValueLength(writeType) + } catch (e: Exception) { + Timber.e("Failed to configure $address for MTU change with reason: ${e.message}") + null + } + } + + override suspend fun createBonding(address: String) { + val peripheral = getPeripheralById(address) + peripheral?.bondState + ?.onEach { state -> + if (state == BondState.NONE) { + peripheral.createBond() + } + } + ?.filter { it == BondState.BONDED } + ?.first() // suspend until bonded + } + + override fun getPeripheralById(address: String?): Peripheral? = + address?.let { centralManager.getPeripheralById(it) } + + override fun disconnect(deviceAddress: String) { + lifecycleScope.launch { + try { + getPeripheralById(deviceAddress) + ?.let { peripheral -> + if (peripheral.isConnected) peripheral.disconnect() + handleDisconnection(deviceAddress) + } + } catch (e: Exception) { + Timber.e(e, "Couldn't disconnect from the $deviceAddress") + } + } + } + + override fun getConnectionState(address: String): Flow? { + val peripheral = getPeripheralById(address) ?: return null + return peripheral.state.also { stateFlow -> + connectionJobs[address]?.cancel() + val job = stateFlow.onEach { state -> + when (state) { + ConnectionState.Connected -> { + _isMissingServices.tryEmit(false) + // Discover services if not already discovered + if (_connectedDevices.value[address] == null) { + discoverServices(peripheral) + } + } + + ConnectionState.Connecting -> _disconnectionReason.tryEmit(null) + is ConnectionState.Disconnected -> { + _disconnectionReason.tryEmit(StateReason(state.reason)) + } + + ConnectionState.Closed -> return@onEach + + ConnectionState.Disconnecting -> { + connectionJobs[address]?.cancel() + handleDisconnection(address) + } + } + }.onCompletion { + connectionJobs[address]?.cancel() + connectionJobs.remove(address) + }.launchIn(lifecycleScope) + connectionJobs[address] = job + } + } + + } + + /** + * Connect to the peripheral and observe its state. + */ + private fun initiateConnection(deviceAddress: String) { + centralManager.getPeripheralById(deviceAddress)?.let { peripheral -> + lifecycleScope.launch { connectPeripheral(peripheral) } + } + } + + private suspend fun connectPeripheral(peripheral: Peripheral) { + runCatching { + centralManager.connect(peripheral, options = ConnectionOptions.Direct()) + }.onFailure { exception -> + Timber.e(exception, "Could not connect to the ${peripheral.address}") + stopForegroundService() // Stop service if connection fails + } + } + + /** + * Discover services and characteristics for the connected [peripheral]. + */ + @OptIn(ExperimentalUuidApi::class) + private fun discoverServices(peripheral: Peripheral) { + val discoveredServices = mutableListOf() + serviceHandlingJob[peripheral.address]?.cancel() + val job = peripheral.services().onEach { remoteServices -> + remoteServices?.forEach { remoteService -> + val serviceManager = ServiceManagerFactory.createServiceManager(remoteService.uuid) + serviceManager?.let { manager -> + Timber.tag("DiscoverServices").i("${manager.profile}") + discoveredServices.add(manager) + lifecycleScope.launch { + try { + val requiresBonding = + remoteService.uuid == CGMS_SERVICE_UUID.toKotlinUuid() && peripheral.hasBondInformation + + if (requiresBonding) { + peripheral.bondState + .onEach { if (it == BondState.NONE) peripheral.createBond() } + .filter { it == BondState.BONDED } + .first() + } + + manager.observeServiceInteractions( + peripheral.address, + remoteService, + this + ) + } catch (e: Exception) { + Timber.tag("ObserveServices").e(e) + handleDisconnection(peripheral.address) + } + } + } + } + when { + discoveredServices.isEmpty() -> { + if (remoteServices?.isNotEmpty() == true) { + _isMissingServices.tryEmit(true) + serviceHandlingJob[peripheral.address]?.cancel() + serviceHandlingJob.remove(peripheral.address) + } + } + + peripheral.isConnected -> { + _isMissingServices.tryEmit(false) + updateConnectedDevices(peripheral, discoveredServices) + } + } + } + .onCompletion { + serviceHandlingJob[peripheral.address]?.cancel() + serviceHandlingJob.remove(peripheral.address) + } + .launchIn(lifecycleScope) + serviceHandlingJob[peripheral.address] = job + } + + /** + * Update the connected devices with the latest state. + */ + private fun updateConnectedDevices(peripheral: Peripheral, handlers: List) { + _connectedDevices.update { + it.toMutableMap().apply { this[peripheral.address] = peripheral to handlers } + } + } + + /** + * Handle disconnection and cleanup for the given peripheral. + */ + private fun handleDisconnection(peripheral: String) { + val currentDevices = _connectedDevices.value.toMutableMap() + currentDevices[peripheral]?.let { + currentDevices.remove(peripheral) + _connectedDevices.tryEmit(currentDevices) + } + clearJobs(peripheral) + clearFlags() + stopServiceIfNoDevices() + } + + /** + * Clear any active jobs for connection and service handling. + */ + private fun clearJobs(peripheral: String) { + connectionJobs[peripheral]?.cancel() + connectionJobs.remove(peripheral) + + serviceHandlingJob[peripheral]?.cancel() + serviceHandlingJob.remove(peripheral) + + } + + /** + * Stop the service if no devices are connected. + */ + private fun stopServiceIfNoDevices() { + if (_connectedDevices.value.isEmpty()) { + stopForegroundService() + stopSelf() + } + } + + /** + * Initialize the logger for the specified device. + */ + private fun initLogger(device: String) { + logger?.let { Timber.uproot(it) } + logger = nRFLoggerTree(this, this.getString(R.string.app_name), device) + .also { Timber.plant(it) } + } + + /** + * Uproot the logger and clear the logger instance. + */ + private fun uprootLogger() { + logger?.let { Timber.uproot(it) } + logger = null + } + + /** + * Clear the missing services and battery level flags. + */ + private fun clearFlags() { + _isMissingServices.tryEmit(false) + uprootLogger() + } + +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileServiceManager.kt b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileServiceManager.kt new file mode 100644 index 00000000..21e26eec --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ProfileServiceManager.kt @@ -0,0 +1,56 @@ +package no.nordicsemi.android.service.profile + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.suspendCancellableCoroutine +import javax.inject.Inject +import kotlin.coroutines.resumeWithException + +const val DEVICE_ADDRESS = "deviceAddress" + +sealed interface ProfileServiceManager { + suspend fun bindService(): ServiceApi + fun unbindService() + fun connectToPeripheral(deviceAddress: String) +} + +internal class ProfileServiceManagerImp @Inject constructor( + @ApplicationContext private val context: Context, +) : ProfileServiceManager { + private var serviceConnection: ServiceConnection? = null + private var api: ServiceApi? = null + + override suspend fun bindService(): ServiceApi = suspendCancellableCoroutine { continuation -> + val intent = Intent(context, ProfileService::class.java) + serviceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + api = service as ServiceApi + continuation.resume(api!!) { _, _, _ -> } + } + + override fun onServiceDisconnected(p0: ComponentName?) { + continuation.resumeWithException(Exception("Service disconnected")) + } + + override fun onBindingDied(p0: ComponentName?) { + continuation.resumeWithException(Exception("Service binding died")) + } + }.apply { + context.bindService(intent, this, Context.BIND_AUTO_CREATE) + } + } + + override fun unbindService() { + serviceConnection?.let { context.unbindService(it) } + } + + override fun connectToPeripheral(deviceAddress: String) { + val intent = Intent(context, ProfileService::class.java) + intent.putExtra(DEVICE_ADDRESS, deviceAddress) + context.startService(intent) + } +} diff --git a/lib_service/src/main/java/no/nordicsemi/android/service/profile/ServiceApi.kt b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ServiceApi.kt new file mode 100644 index 00000000..93cbcfc3 --- /dev/null +++ b/lib_service/src/main/java/no/nordicsemi/android/service/profile/ServiceApi.kt @@ -0,0 +1,69 @@ +package no.nordicsemi.android.service.profile + +import kotlinx.coroutines.flow.Flow +import no.nordicsemi.android.toolbox.profile.manager.ServiceManager +import no.nordicsemi.android.ui.view.internal.DisconnectReason +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.core.ConnectionState +import no.nordicsemi.kotlin.ble.core.WriteType + +/** Device disconnection reason. */ +sealed interface DeviceDisconnectionReason + +/** Includes the [ConnectionState.Disconnected.Reason]. */ +data class StateReason(val reason: ConnectionState.Disconnected.Reason) : DeviceDisconnectionReason + +/** Includes the custom made [DisconnectReason] to include other disconnection reasons which are not included in the [ConnectionState.Disconnected.Reason]. */ +data class CustomReason(val reason: DisconnectReason) : + DeviceDisconnectionReason + +interface ServiceApi { + + /** Flow of connected devices. */ + val connectedDevices: Flow>>> + + /** Missing services flag. */ + val isMissingServices: Flow + + /** + * Get the peripheral by its [address]. + * + * @return the peripheral instance. + */ + fun getPeripheralById(address: String?): Peripheral? + + /** + * Disconnect the device with the given [deviceAddress]. + * + * @param deviceAddress the device address. + */ + fun disconnect(deviceAddress: String) + + /** + * Get the connection state of the device with the given [address]. + * + * @return the connection state flow. + */ + fun getConnectionState(address: String): Flow? + + /** + * Get the disconnection reason of the device with the given address. + * + * @return the disconnection reason flow. + */ + val disconnectionReason: Flow + + /** + * Request maximum write value length. + * For [WriteType.WITHOUT_RESPONSE] it is equal to *ATT MTU - 3 bytes*. + */ + suspend fun getMaxWriteValue( + address: String, + writeType: WriteType = WriteType.WITHOUT_RESPONSE + ): Int? + + suspend fun createBonding( + address: String + ) + +} diff --git a/lib_storage/build.gradle.kts b/lib_storage/build.gradle.kts new file mode 100644 index 00000000..85002054 --- /dev/null +++ b/lib_storage/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + alias(libs.plugins.nordic.feature) + alias(libs.plugins.ksp) + +} + +android { + namespace = "no.nordicsemi.android.toolbox.lib.storage" +} + +dependencies { + implementation(libs.room.ktx) + ksp(libs.room.compiler) +} \ No newline at end of file diff --git a/lib_storage/module-rules.pro b/lib_storage/module-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/lib_storage/module-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/lib_scanner/src/main/AndroidManifest.xml b/lib_storage/src/main/AndroidManifest.xml similarity index 62% rename from lib_scanner/src/main/AndroidManifest.xml rename to lib_storage/src/main/AndroidManifest.xml index 44008a43..1d26c87a 100644 --- a/lib_scanner/src/main/AndroidManifest.xml +++ b/lib_storage/src/main/AndroidManifest.xml @@ -1,4 +1,2 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/Configuration.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/Configuration.kt new file mode 100644 index 00000000..82486c32 --- /dev/null +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/Configuration.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.lib.storage + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "configurations") +data class ConfigurationEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = "_id") val _id: Int?, + @ColumnInfo(name = "name") val name: String, + @ColumnInfo(name = "xml") val xml: String, + @ColumnInfo(name = "deleted", defaultValue = "0") val deleted: Int +) \ No newline at end of file diff --git a/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDao.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDao.kt new file mode 100644 index 00000000..747ac560 --- /dev/null +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDao.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.toolbox.lib.storage + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import kotlinx.coroutines.flow.Flow + +@Dao +interface ConfigurationsDao { + @Query("SELECT * FROM configurations") + fun getAllConfigurations(): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertConfiguration(configuration: ConfigurationEntity): Long + + @Query("DELETE FROM configurations WHERE name = :configurationName") + suspend fun deleteConfiguration(configurationName: String) + +} diff --git a/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDatabase.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDatabase.kt new file mode 100644 index 00000000..a1399b05 --- /dev/null +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/ConfigurationDatabase.kt @@ -0,0 +1,13 @@ +package no.nordicsemi.android.toolbox.lib.storage + +import androidx.room.Database +import androidx.room.RoomDatabase + +@Database( + entities = [ConfigurationEntity::class], + version = 1, + exportSchema = false +) +internal abstract class ConfigurationDatabase : RoomDatabase() { + abstract fun configurationDao(): ConfigurationsDao +} \ No newline at end of file diff --git a/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/InitMigration.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/InitMigration.kt new file mode 100644 index 00000000..38db9418 --- /dev/null +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/InitMigration.kt @@ -0,0 +1,10 @@ +package no.nordicsemi.android.toolbox.lib.storage + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(db: SupportSQLiteDatabase) { + // Empty implementation, because the schema isn't changing. + } +} \ No newline at end of file diff --git a/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DaoHiltModule.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DaoHiltModule.kt new file mode 100644 index 00000000..e163f656 --- /dev/null +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DaoHiltModule.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.toolbox.lib.storage.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import no.nordicsemi.android.toolbox.lib.storage.ConfigurationDatabase +import no.nordicsemi.android.toolbox.lib.storage.ConfigurationsDao +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DaoHiltModule { + + @Provides + @Singleton + internal fun provideDeviceDao(db: ConfigurationDatabase): ConfigurationsDao { + return db.configurationDao() + } +} \ No newline at end of file diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DbHiltModule.kt similarity index 54% rename from profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt rename to lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DbHiltModule.kt index 327198d4..fe2523b3 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/DbHiltModule.kt +++ b/lib_storage/src/main/java/no/nordicsemi/android/toolbox/lib/storage/di/DbHiltModule.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.uart +package no.nordicsemi.android.toolbox.lib.storage.di import android.content.Context import androidx.room.Room @@ -7,8 +7,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import no.nordicsemi.android.uart.db.ConfigurationsDatabase -import no.nordicsemi.android.uart.db.MIGRATION_1_2 +import no.nordicsemi.android.toolbox.lib.storage.ConfigurationDatabase import javax.inject.Singleton @Module @@ -17,10 +16,11 @@ class DbHiltModule { @Provides @Singleton - internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase { + internal fun provideDeviceDB(@ApplicationContext context: Context): ConfigurationDatabase { return Room.databaseBuilder( context, - ConfigurationsDatabase::class.java, "toolbox_uart.db" - ).addMigrations(MIGRATION_1_2).build() + ConfigurationDatabase::class.java, + "toolbox_uart.db" + ).build() } -} +} \ No newline at end of file diff --git a/lib_ui/build.gradle.kts b/lib_ui/build.gradle.kts index 584864b2..2b83f406 100644 --- a/lib_ui/build.gradle.kts +++ b/lib_ui/build.gradle.kts @@ -31,22 +31,18 @@ plugins { alias(libs.plugins.nordic.feature) + alias(libs.plugins.kotlin.parcelize) } android { namespace = "no.nordicsemi.android.ui" - - testOptions { - unitTests.isIncludeAndroidResources = true - } } dependencies { implementation(libs.nordic.theme) + implementation(libs.nordic.ui) implementation(libs.nordic.logger) - implementation(libs.nordic.blek.client) - implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.iconsExtended) implementation(libs.androidx.core.ktx) diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimatedThreeDots.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimatedThreeDots.kt new file mode 100644 index 00000000..4f1ed4fc --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimatedThreeDots.kt @@ -0,0 +1,91 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun AnimatedThreeDots( + modifier: Modifier = Modifier, + dotSize: Dp = 8.dp +) { + val dotCount = 3 + val infiniteTransition = rememberInfiniteTransition() + + val dotAlphas = List(dotCount) { index -> + infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = 500, + delayMillis = index * 200, + easing = LinearEasing + ), + repeatMode = RepeatMode.Reverse + ) + ) + } + + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + dotAlphas.forEach { alpha -> + Box( + modifier = Modifier + .size(dotSize) + .clip(CircleShape) + .background(Color.Gray.copy(alpha = alpha.value)) + ) + } + } +} + +@Composable +fun TextWithAnimatedDots( + text: String, + modifier: Modifier = Modifier, + dotSize: Dp = 2.dp, + textStyle: TextStyle = MaterialTheme.typography.bodyLarge, + textAlign: TextAlign = TextAlign.Center +) { + Row( + verticalAlignment = Alignment.Bottom + ) { + Text( + text = text, + textAlign = textAlign, + style = textStyle, + ) + Spacer(modifier = Modifier.width(2.dp)) + AnimatedThreeDots( + modifier = modifier.padding(bottom = 4.dp), + dotSize = dotSize + ) + } + +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimationTransitionState.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimationTransitionState.kt new file mode 100644 index 00000000..d2043f11 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/AnimationTransitionState.kt @@ -0,0 +1,130 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.TweenSpec +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.tween +import androidx.compose.animation.core.updateTransition +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun createCircleTransition( + isInAccessibilityMode: Boolean, + duration: Int +): CircleTransitionState { + val transition = updateTransition(targetState = isInAccessibilityMode, label = "Transition") + return CircleTransitionState( + dotRadius = transition.animateDp( + label = "Dot Radius", + transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) } + ) { if (it) 10.dp else 5.dp }, + circleWidth = transition.animateDp( + label = "Circle Width", + transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) } + ) { if (it) 8.dp else 5.dp }, + circleColor = transition.animateColor( + label = "Circle Color", + transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) } + ) { if (it) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.primaryContainer }, + dotColor = transition.animateColor( + label = "Dot Color", + transitionSpec = { tween(duration, easing = LinearOutSlowInEasing) } + ) { if (it) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary } + ) +} + +data class CircleTransitionState( + val dotRadius: State, + val circleWidth: State, + val circleColor: State, + val dotColor: State, +) { + fun toggleAccessibilityMode() { + dotRadius.value + } +} + +data class LinearTransitionState( + val border: State, + val height: State, + val radius: State, + val color: State, + val inactiveColor: State, +) + +@Composable +fun createLinearTransition( + isInAccessibilityMode: Boolean, + duration: Int, +): LinearTransitionState { + val transition = updateTransition(targetState = isInAccessibilityMode, label = "Transition") + return LinearTransitionState( + border = transition.animateDp( + label = "Border", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) 3.dp else 0.dp }, + + height = transition.animateDp( + label = "Height", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) 30.dp else 25.dp }, + radius = transition.animateDp( + label = "Radius", + transitionSpec = { TweenSpec(duration / 2, 0, LinearOutSlowInEasing) } + ) { if (it) 4.dp else 8.dp }, + color = transition.animateColor( + label = "Color", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { + if (it) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.primary + }, + inactiveColor = transition.animateColor( + label = "In-active color", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { + if (it) MaterialTheme.colorScheme.tertiaryContainer else MaterialTheme.colorScheme.onPrimary + }, + ) +} + +@Composable +fun createAngularTransition( + isInAccessibilityMode: Boolean, + duration: Int, +): ChartTransition { + val transition = updateTransition( + targetState = isInAccessibilityMode, + label = "Accessibility transition" + ) + return ChartTransition( + height = transition.animateDp( + label = "Height", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) 100.dp else 50.dp }, + avgLineWidth = transition.animateDp( + label = "Average Line Width", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) 8.dp else 2.dp }, + chartColor = transition.animateColor( + label = "Chart Color", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.primary }, + avgLineColor = transition.animateColor( + label = "Average Line Color", + transitionSpec = { TweenSpec(duration, 0, LinearOutSlowInEasing) } + ) { if (it) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.tertiary } + ) +} + +data class ChartTransition( + val height: State, + val avgLineWidth: State, + val chartColor: State, + val avgLineColor: State, +) diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/BatteryLevelView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/BatteryLevelView.kt deleted file mode 100644 index 935095f5..00000000 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/BatteryLevelView.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.ui.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.ui.R - -@Composable -fun BatteryLevelView(batteryLevel: Int) { - ScreenSection { - KeyValueField( - stringResource(id = R.string.field_battery), - "$batteryLevel%" - ) - } -} \ No newline at end of file diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/DropdownView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/DropdownView.kt new file mode 100644 index 00000000..d6788d29 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/DropdownView.kt @@ -0,0 +1,113 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +inline fun DropdownView( + items: List, + label: String, + placeholder: String, + defaultSelectedItem: T? = null, + isError: Boolean = false, + errorMessage: String = "", + crossinline onItemSelected: (T) -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + var selectedText by rememberSaveable { mutableStateOf(defaultSelectedItem) } + + Box { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }) { + OutlinedTextField( + value = selectedText?.toString() ?: placeholder, + onValueChange = { }, // No need to handle since it's readOnly + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable), + placeholder = { Text(text = placeholder) }, + label = { Text(text = label) }, + isError = isError, + supportingText = { + if (isError) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall, + ) + } + } + }, + ) + // Animated dropdown menu + AnimatedVisibility(visible = expanded) { + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.exposedDropdownSize(), + ) { + items.forEach { + DropdownMenuItem( + text = { Text(it.toString()) }, + onClick = { + selectedText = it + expanded = false + onItemSelected(it) + }, + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun DropdownViewPreview() { + val items = listOf("Item 1", "Item 2", "Item 3") + DropdownView( + items = items, + label = "Label", + placeholder = "Placeholder", + defaultSelectedItem = items[0], + onItemSelected = {} + ) +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/FeatureSupportedRow.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/FeatureSupportedRow.kt new file mode 100644 index 00000000..4c12e6bf --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/FeatureSupportedRow.kt @@ -0,0 +1,53 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun FeatureSupported( + text: String, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box( + modifier = Modifier.background( + color = MaterialTheme.colorScheme.primary, + shape = RectangleShape + ) + ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(20.dp) + ) + } + Text( + text = text, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun FeatureSupportedPreview() { + FeatureSupported("Instantaneous stride length measurement supported") +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/KeyValueColumn.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/KeyValueColumn.kt new file mode 100644 index 00000000..4ab0c7d5 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/KeyValueColumn.kt @@ -0,0 +1,152 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun KeyValueColumn( + value: String, + key: String, + modifier: Modifier = Modifier, + verticalSpacing: Dp = 8.dp, + keyStyle: TextStyle?= null +) { + Box( + modifier = Modifier + .padding(end = 8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + horizontalAlignment = Alignment.Start, + modifier = modifier + ) { + Text( + text = value, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = key, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = keyStyle ?: MaterialTheme.typography.bodyLarge + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun KeyValueColumnPreview() { + KeyValueColumn( + value = "Sample Value", + key = "Sample Key", +// keyStyle = MaterialTheme.typography.labelLarge + ) +} + +@Composable +fun KeyValueColumn( + value: String, + modifier: Modifier = Modifier, + key: @Composable (() -> Unit) +) { + Box( + modifier = Modifier + .padding(end = 8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.Start, + modifier = modifier + ) { + Text( + text = value, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + key() + } + } +} + +@Composable +fun KeyValueColumnReverse( + value: String, + key: String, + modifier: Modifier = Modifier, + verticalSpacing: Dp = 8.dp, + keyStyle: TextStyle? = null, +) { + Box( + modifier = Modifier + .padding(start = 8.dp), + contentAlignment = Alignment.TopEnd, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(verticalSpacing), + horizontalAlignment = Alignment.End, + modifier = modifier + ) { + Text( + text = value, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Text( + text = key, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = keyStyle ?: MaterialTheme.typography.bodyLarge + ) + } + } +} + +@Composable +fun KeyValueColumnReverse( + value: String, + modifier: Modifier = Modifier, + key: @Composable (() -> Unit) +) { + Box( + modifier = Modifier + .padding(start = 8.dp), + contentAlignment = Alignment.TopEnd, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.End, + modifier = modifier + ) { + Text( + text = value, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + key() + } + } +} \ No newline at end of file diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/ScreenSection.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/ScreenSection.kt index b4c7e5ff..93110125 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/ScreenSection.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/ScreenSection.kt @@ -1,5 +1,6 @@ package no.nordicsemi.android.ui.view +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material3.OutlinedCard @@ -8,9 +9,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable -fun ScreenSection(content: @Composable () -> Unit) { +fun ScreenSection( + modifier: Modifier = Modifier.padding(16.dp), + content: @Composable () -> Unit +) { OutlinedCard { - Column(modifier = Modifier.padding(16.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + ) { content() } } diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionRow.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionRow.kt new file mode 100644 index 00000000..ac97c114 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionRow.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun SectionRow( + content: @Composable RowScope.() -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + content() + } +} \ No newline at end of file diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionTitle.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionTitle.kt index 757b64f0..0cce2d91 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionTitle.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/SectionTitle.kt @@ -31,36 +31,42 @@ package no.nordicsemi.android.ui.view +import android.annotation.SuppressLint import androidx.annotation.DrawableRes import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import no.nordicsemi.android.ui.R @Composable fun SectionTitle( @DrawableRes resId: Int, title: String, - menu: @Composable (() -> Unit)? = null, - modifier: Modifier = Modifier.fillMaxWidth() + modifier: Modifier = Modifier.fillMaxWidth(), + menu: @Composable (() -> Unit)? = null ) { Row( modifier = modifier, @@ -70,26 +76,106 @@ fun SectionTitle( Image( painter = painterResource(id = resId), contentDescription = null, - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSecondary), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary), modifier = Modifier - .background( - color = MaterialTheme.colorScheme.secondary, - shape = CircleShape - ) - .padding(8.dp) + .size(28.dp) ) Spacer(modifier = Modifier.size(8.dp)) Text( text = title, textAlign = TextAlign.Start, - fontSize = 24.sp, - fontWeight = FontWeight.Bold, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f) ) menu?.invoke() } } +@Composable +fun SectionTitle( + icon: ImageVector, + title: String, + modifier: Modifier = Modifier.fillMaxWidth(), + menu: @Composable (() -> Unit)? = null +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Image( + imageVector = icon, + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary), + modifier = Modifier + .size(28.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = title, + textAlign = TextAlign.Start, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + menu?.invoke() + } +} + +@Composable +@SuppressLint("ModifierParameter") +fun SectionTitle( + @DrawableRes resId: Int, + title: String, + modifier: Modifier = Modifier.fillMaxWidth(), + rotateArrow: Float? = null, + iconBackground: Color = MaterialTheme.colorScheme.secondary +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(id = resId), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary), + modifier = Modifier + .size(28.dp) + + ) + Spacer(modifier = Modifier.padding(8.dp)) + Text( + text = title, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + rotateArrow?.let { + Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterEnd) { + Image( + Icons.Default.ArrowDropDown, + contentDescription = null, + modifier = Modifier + .padding(8.dp) + .rotate(it) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SectionTitle_Preview() { + SectionTitle( + resId = R.drawable.ic_records, + title = stringResource(id = R.string.back_screen), + modifier = Modifier + .fillMaxWidth() + .clickable { }, + rotateArrow = 0f + ) +} + @Composable fun SectionTitle( icon: ImageVector, @@ -104,20 +190,15 @@ fun SectionTitle( Icon( imageVector = icon, contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondary, + tint = MaterialTheme.colorScheme.secondary, modifier = Modifier - .background( - color = MaterialTheme.colorScheme.secondary, - shape = CircleShape - ) - .padding(8.dp) + .size(28.dp) ) Spacer(modifier = Modifier.size(8.dp)) Text( text = title, textAlign = TextAlign.Center, - fontSize = 24.sp, - fontWeight = FontWeight.Bold + style = MaterialTheme.typography.titleMedium, ) } } diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TextInputField.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TextInputField.kt new file mode 100644 index 00000000..adf8c390 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TextInputField.kt @@ -0,0 +1,104 @@ +package no.nordicsemi.android.ui.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +private class PlaceholderTransformation(private val placeholder: String) : VisualTransformation { + override fun filter(text: AnnotatedString): TransformedText { + return placeholderFilter(placeholder) + } +} + +fun placeholderFilter(placeholder: String): TransformedText { + + val numberOffsetTranslator = object : OffsetMapping { + override fun originalToTransformed(offset: Int): Int { + return 0 + } + + override fun transformedToOriginal(offset: Int): Int { + return 0 + } + } + + return TransformedText(AnnotatedString(placeholder), numberOffsetTranslator) +} + +/** + * Compose view to input text in OutlinedTextField. + */ +@Composable +fun TextInputField( + modifier: Modifier = Modifier, + input: String, + label: String, + hint: String? = null, + placeholder: String = "", + errorMessage: String = "", + errorState: Boolean = false, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + onUpdate: (String) -> Unit +) { + val textColor = MaterialTheme.colorScheme.onSurface.copy( + alpha = if (input.isEmpty()) 0.5f else LocalContentColor.current.alpha + ) + OutlinedTextField( + value = input, + onValueChange = { onUpdate(it) }, + visualTransformation = if (input.isEmpty()) + PlaceholderTransformation(placeholder) else VisualTransformation.None, + modifier = modifier.fillMaxWidth(), + label = { Text(text = label) }, + keyboardOptions = keyboardOptions, + placeholder = { + Text( + text = placeholder, + ) + }, + supportingText = { + if (errorState) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Error, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = errorMessage, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.alpha(1f) + ) + } + } else if (hint != null) { + Text( + text = hint, + modifier = Modifier.alpha(0.38f) + ) + } + }, + colors = OutlinedTextFieldDefaults.colors(textColor), + isError = errorState, + ) +} \ No newline at end of file diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt index bf06b608..00a44099 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/TopAppBar.kt @@ -31,10 +31,9 @@ package no.nordicsemi.android.ui.view -import androidx.annotation.StringRes import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -48,21 +47,24 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus +import no.nordicsemi.android.common.theme.NordicTheme import no.nordicsemi.android.ui.R +private const val TOP_APP_BAR_TITLE = "Nordic_Appbar" + @OptIn(ExperimentalMaterial3Api::class) @Composable fun CloseIconAppBar(text: String, onClick: () -> Unit) { TopAppBar( - title = { Text(text, maxLines = 2) }, + title = { Text(text, maxLines = 1) }, colors = TopAppBarDefaults.topAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.primary, - containerColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = colorResource(id = R.color.appBarColor), titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, @@ -78,29 +80,41 @@ fun CloseIconAppBar(text: String, onClick: () -> Unit) { ) } +@Preview +@Composable +private fun CloseIconAppBarPreview() { + NordicTheme { + CloseIconAppBar(TOP_APP_BAR_TITLE) {} + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) { +fun LoggerBackIconAppBar( + text: String, + onBackClick: () -> Unit, + onLoggerClick: () -> Unit +) { TopAppBar( - title = { Text(text, maxLines = 2) }, + title = { Text(text, maxLines = 1) }, colors = TopAppBarDefaults.topAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.primary, - containerColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = colorResource(id = R.color.appBarColor), titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, ), navigationIcon = { - IconButton(onClick = { onClick() }) { + IconButton(onClick = { onBackClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) } }, actions = { - IconButton(onClick = { onClick() }) { + IconButton(onClick = { onLoggerClick() }) { Icon( painterResource(id = R.drawable.ic_logger), contentDescription = stringResource(id = R.string.open_logger), @@ -112,14 +126,22 @@ fun LoggerBackIconAppBar(text: String, onClick: () -> Unit) { ) } +@Preview +@Composable +private fun LoggerBackIconAppBarPreview() { + NordicTheme { + LoggerBackIconAppBar(TOP_APP_BAR_TITLE, {}) {} + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun BackIconAppBar(text: String, onClick: () -> Unit) { TopAppBar( - title = { Text(text, maxLines = 2) }, + title = { Text(text, maxLines = 1) }, colors = TopAppBarDefaults.topAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.primary, - containerColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = colorResource(id = R.color.appBarColor), titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, @@ -127,7 +149,7 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) { navigationIcon = { IconButton(onClick = { onClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) @@ -136,6 +158,14 @@ fun BackIconAppBar(text: String, onClick: () -> Unit) { ) } +@Preview +@Composable +private fun BackIconAppBarPreview() { + NordicTheme { + BackIconAppBar(TOP_APP_BAR_TITLE) {} + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoggerIconAppBar( @@ -145,10 +175,10 @@ fun LoggerIconAppBar( onLoggerClick: () -> Unit ) { TopAppBar( - title = { Text(text, maxLines = 2) }, + title = { Text(text, maxLines = 1) }, colors = TopAppBarDefaults.topAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.primary, - containerColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = colorResource(id = R.color.appBarColor), titleContentColor = MaterialTheme.colorScheme.onPrimary, actionIconContentColor = MaterialTheme.colorScheme.onPrimary, navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, @@ -156,7 +186,7 @@ fun LoggerIconAppBar( navigationIcon = { IconButton(onClick = { onClick() }) { Icon( - Icons.Default.ArrowBack, + Icons.AutoMirrored.Filled.ArrowBack, tint = MaterialTheme.colorScheme.onPrimary, contentDescription = stringResource(id = R.string.back_screen), ) @@ -184,23 +214,10 @@ fun LoggerIconAppBar( ) } +@Preview @Composable -fun ProfileAppBar( - deviceName: String?, - connectionState: GattConnectionStateWithStatus?, - @StringRes - title: Int, - navigateUp: () -> Unit, - disconnect: () -> Unit, - openLogger: () -> Unit -) { - if (deviceName?.isNotBlank() == true) { - if (connectionState?.state == GattConnectionState.STATE_DISCONNECTING || connectionState?.state == GattConnectionState.STATE_DISCONNECTED) { - LoggerBackIconAppBar(deviceName, openLogger) - } else { - LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger) - } - } else { - BackIconAppBar(stringResource(id = title), navigateUp) +private fun LoggerIconAppBarPreview() { + NordicTheme { + LoggerIconAppBar(TOP_APP_BAR_TITLE, {}, {}) {} } } diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/animate/AnimatedHeart.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/animate/AnimatedHeart.kt new file mode 100644 index 00000000..169b5238 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/animate/AnimatedHeart.kt @@ -0,0 +1,49 @@ +package no.nordicsemi.android.ui.view.animate + +import androidx.compose.animation.core.EaseInOutCubic +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Preview(showBackground = true) +@Composable +fun AnimatedHeart( + modifier: Modifier = Modifier +) { + // Infinite transition for pulsing animation + val infiniteTransition = rememberInfiniteTransition() + + val scale by infiniteTransition.animateFloat( + initialValue = 1f, + targetValue = 1.15f, + animationSpec = infiniteRepeatable( + animation = tween(600, easing = EaseInOutCubic), + repeatMode = RepeatMode.Reverse + ) + ) + + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = "heart icon", + modifier = modifier + .size(28.dp) + .graphicsLayer( + scaleX = scale, + scaleY = scale + ), + tint = Color.Red + ) +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/Ext.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/Ext.kt index 79b8e8e4..96734344 100644 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/Ext.kt +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/Ext.kt @@ -32,9 +32,6 @@ package no.nordicsemi.android.ui.view.dialog import androidx.compose.runtime.Composable -import androidx.compose.ui.text.buildAnnotatedString @Composable -fun String.toAnnotatedString() = buildAnnotatedString { - append(this@toAnnotatedString) -} +fun Boolean.toBooleanText(): String = if (this) "YES" else "NO" diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialog.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialog.kt deleted file mode 100644 index 1ab72eb2..00000000 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialog.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.ui.view.dialog - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.ui.R - -@Composable -fun StringListDialog(config: StringListDialogConfig) { - AlertDialog( - onDismissRequest = { config.onResult(FlowCanceled) }, - title = { Text(text = config.title ?: stringResource(id = R.string.dialog).toAnnotatedString()) }, - text = { - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - ) { - - config.items.forEachIndexed { i, entry -> - Row( - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) - .clickable { config.onResult(ItemSelectedResult(i)) } - .padding(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - config.leftIcon?.let { - Image( - modifier = Modifier.padding(horizontal = 4.dp), - painter = painterResource(it), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant), - contentDescription = "Content image", - ) - } - Text( - text = entry, - modifier = Modifier.fillMaxWidth(), - style = MaterialTheme.typography.titleLarge - ) - } - } - } - }, - confirmButton = { - TextButton(onClick = { config.onResult(FlowCanceled) }) { - Text( - text = stringResource(id = R.string.cancel), - ) - } - } - ) -} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogConfig.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogConfig.kt deleted file mode 100644 index c5be6532..00000000 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogConfig.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.ui.view.dialog - -import androidx.annotation.DrawableRes -import androidx.compose.ui.text.AnnotatedString - -data class StringListDialogConfig( - val title: AnnotatedString? = null, - @DrawableRes - val leftIcon: Int? = null, - val items: List = emptyList(), - val onResult: (StringListDialogResult) -> Unit -) diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogResult.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogResult.kt deleted file mode 100644 index 061e3c50..00000000 --- a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/dialog/StringListDialogResult.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.ui.view.dialog - -sealed class StringListDialogResult - -data class ItemSelectedResult(val index: Int): StringListDialogResult() - -object FlowCanceled : StringListDialogResult() diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceConnectingView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceConnectingView.kt new file mode 100644 index 00000000..90f2d14a --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceConnectingView.kt @@ -0,0 +1,80 @@ +package no.nordicsemi.android.ui.view.internal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HourglassTop +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.ui.view.CircularIcon +import no.nordicsemi.android.ui.R +import no.nordicsemi.android.ui.view.TextWithAnimatedDots + +@Composable +fun DeviceConnectingView( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.(PaddingValues) -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxSize() + .then(modifier), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedCard( + modifier = Modifier + .widthIn(max = 460.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularIcon(imageVector = Icons.Default.HourglassTop) + + TextWithAnimatedDots( + text = stringResource(id = R.string.device_connecting), + textStyle = MaterialTheme.typography.titleMedium, + ) + + TextWithAnimatedDots( + text = stringResource(id = R.string.device_connecting_des), + textStyle = MaterialTheme.typography.bodyMedium, + ) + } + } + + content(PaddingValues(top = 16.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun DeviceConnectingView_Preview() { + MaterialTheme { + DeviceConnectingView { padding -> + Button( + onClick = {}, + modifier = Modifier.padding(padding) + ) { + Text(text = "Cancel") + } + } + } +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceDisconnectedView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceDisconnectedView.kt new file mode 100644 index 00000000..187d43b7 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/DeviceDisconnectedView.kt @@ -0,0 +1,116 @@ +package no.nordicsemi.android.ui.view.internal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.DeveloperBoardOff +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +enum class DisconnectReason { + USER, UNKNOWN, LINK_LOSS, MISSING_SERVICE, BLUETOOTH_OFF +} + +@Composable +fun DeviceDisconnectedView( + reason: DisconnectReason, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.(PaddingValues) -> Unit = {}, +) { + val disconnectedReason = when (reason) { + DisconnectReason.USER -> "Device disconnected successfully." + DisconnectReason.UNKNOWN -> "Oops...! Connection went on a coffee break." + DisconnectReason.LINK_LOSS -> "Device signal has been lost." + DisconnectReason.MISSING_SERVICE -> "The peripheral has services that aren't supported in the nRF Toolbox." + DisconnectReason.BLUETOOTH_OFF -> "Bluetooth adapter is turned off." + } + + DeviceDisconnectedView( + disconnectedReason = disconnectedReason, + modifier = modifier, + content = content, + isMissingService = reason == DisconnectReason.MISSING_SERVICE + ) +} + +@Composable +fun DeviceDisconnectedView( + disconnectedReason: String, + modifier: Modifier = Modifier, + isMissingService: Boolean = false, + content: @Composable ColumnScope.(PaddingValues) -> Unit = {}, +) { + Column( + modifier = Modifier + .fillMaxSize() + .then(modifier), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedCard( + modifier = Modifier + .widthIn(max = 460.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + imageVector = Icons.Default.DeveloperBoardOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(48.dp) + ) + + Text( + text = if (isMissingService) "No supported services" else "Device disconnected", + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = disconnectedReason, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium + ) + } + } + + content(PaddingValues(top = 16.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun DeviceDisconnectedViewPreview() { + MaterialTheme { + DeviceDisconnectedView( + reason = DisconnectReason.UNKNOWN, + content = { padding -> + Button( + onClick = {}, + modifier = Modifier.padding(padding) + ) { + Text(text = "Retry") + } + } + ) + } +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/EmptyView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/EmptyView.kt new file mode 100644 index 00000000..e4b39a8d --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/EmptyView.kt @@ -0,0 +1,40 @@ +package no.nordicsemi.android.ui.view.internal + +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.BluetoothSearching +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.ui.view.WarningView +import no.nordicsemi.android.ui.R + +@Composable +fun EmptyView( + @StringRes title: Int, + @StringRes hint: Int, +) { + WarningView( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + imageVector = Icons.AutoMirrored.Filled.BluetoothSearching, + title = stringResource(title).uppercase(), + hint = stringResource(hint).uppercase(), + hintTextAlign = TextAlign.Justify, + ) +} + +@Preview(showBackground = true) +@Composable +private fun EmptyViewPreview() { + EmptyView( + R.string.app_name, + R.string.app_name, + ) +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/LoadingView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/LoadingView.kt new file mode 100644 index 00000000..0388dda9 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/LoadingView.kt @@ -0,0 +1,32 @@ +package no.nordicsemi.android.ui.view.internal + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun LoadingView() { + Box( + modifier = Modifier + .padding(8.dp) + .fillMaxSize() + .fillMaxHeight(), + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun LoadingViewPreview() { + LoadingView() +} diff --git a/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/ServiceDiscoveryView.kt b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/ServiceDiscoveryView.kt new file mode 100644 index 00000000..4cd78102 --- /dev/null +++ b/lib_ui/src/main/java/no/nordicsemi/android/ui/view/internal/ServiceDiscoveryView.kt @@ -0,0 +1,81 @@ +package no.nordicsemi.android.ui.view.internal + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.HourglassTop +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.ui.view.CircularIcon +import no.nordicsemi.android.ui.R +import no.nordicsemi.android.ui.view.TextWithAnimatedDots + +@Composable +fun ServiceDiscoveryView( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.(PaddingValues) -> Unit = {} +) { + Column( + modifier = Modifier + .fillMaxSize() + .then(modifier), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedCard( + modifier = Modifier + .widthIn(max = 460.dp), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + CircularIcon(imageVector = Icons.Default.HourglassTop) + + TextWithAnimatedDots( + text = stringResource(id = R.string.discovering_services), + textStyle = MaterialTheme.typography.titleMedium + ) + TextWithAnimatedDots( + text = stringResource(id = R.string.discovering_services_des), + textStyle = MaterialTheme.typography.bodyMedium + ) + } + } + + content(PaddingValues(top = 16.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun ServiceDiscoveryViewPreview() { + MaterialTheme { + ServiceDiscoveryView( + modifier = Modifier.padding(16.dp) + ) { padding -> + Button( + onClick = {}, + modifier = Modifier.padding(padding) + ) { + Text(text = "Cancel") + } + } + } +} diff --git a/lib_ui/src/main/res/drawable/ic_battery.xml b/lib_ui/src/main/res/drawable/ic_battery.xml new file mode 100644 index 00000000..a9f2b1e0 --- /dev/null +++ b/lib_ui/src/main/res/drawable/ic_battery.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_bps.xml b/lib_ui/src/main/res/drawable/ic_bps.xml similarity index 100% rename from app/src/main/res/drawable/ic_bps.xml rename to lib_ui/src/main/res/drawable/ic_bps.xml diff --git a/app/src/main/res/drawable/ic_cgm.xml b/lib_ui/src/main/res/drawable/ic_cgm.xml similarity index 98% rename from app/src/main/res/drawable/ic_cgm.xml rename to lib_ui/src/main/res/drawable/ic_cgm.xml index 13ceb40f..b700e95d 100644 --- a/app/src/main/res/drawable/ic_cgm.xml +++ b/lib_ui/src/main/res/drawable/ic_cgm.xml @@ -30,8 +30,8 @@ --> - + + + diff --git a/app/src/main/res/drawable/ic_hrs.xml b/lib_ui/src/main/res/drawable/ic_hrs.xml similarity index 100% rename from app/src/main/res/drawable/ic_hrs.xml rename to lib_ui/src/main/res/drawable/ic_hrs.xml diff --git a/app/src/main/res/drawable/ic_hts.xml b/lib_ui/src/main/res/drawable/ic_hts.xml similarity index 100% rename from app/src/main/res/drawable/ic_hts.xml rename to lib_ui/src/main/res/drawable/ic_hts.xml diff --git a/app/src/main/res/drawable/ic_prx.xml b/lib_ui/src/main/res/drawable/ic_prx.xml similarity index 100% rename from app/src/main/res/drawable/ic_prx.xml rename to lib_ui/src/main/res/drawable/ic_prx.xml diff --git a/app/src/main/res/drawable/ic_rscs.xml b/lib_ui/src/main/res/drawable/ic_rscs.xml similarity index 100% rename from app/src/main/res/drawable/ic_rscs.xml rename to lib_ui/src/main/res/drawable/ic_rscs.xml diff --git a/app/src/main/res/drawable/ic_running_indicator.xml b/lib_ui/src/main/res/drawable/ic_running_indicator.xml similarity index 100% rename from app/src/main/res/drawable/ic_running_indicator.xml rename to lib_ui/src/main/res/drawable/ic_running_indicator.xml diff --git a/app/src/main/res/drawable/ic_uart.xml b/lib_ui/src/main/res/drawable/ic_uart.xml similarity index 100% rename from app/src/main/res/drawable/ic_uart.xml rename to lib_ui/src/main/res/drawable/ic_uart.xml diff --git a/lib_ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/lib_ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index e268605d..324a2874 100644 --- a/lib_ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/lib_ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -32,5 +32,6 @@ - + + \ No newline at end of file diff --git a/lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_dfu.png b/lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_dfu.png deleted file mode 100644 index ee51ccab354329818a5a8e43daffe2763d4f4a1b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2723 zcmV;U3S9MxP)~8MqALnN8UD#c+>|G#felt6h z&E9+NIp6bppWp8aP7*1la)1Fq5ik~*0E`0007HSnK(TXq6L0`%0JZ}=fz3c2&K5SFzzCoakUl@c213A>z~h!>y>!|G zq?9TGW&_Ux7AKb!Fdz7*Wm(Oq6+lWU3z!R3IeO(((oSFzu-LLJb;>U?dy%u&v^i)&FYIvVtQ~XbwZJgp&NMJ?82}VTni*c-Ech(t{I}%A9p;gL+nJbsZY)%v>R9?1%?s<}l5YE9|8AHHV?{T}ge&+?OjgVcQ)eAT)Ew zXck_q{RT-{mQ{ER2RS)pU%{zvgsE@2-~@zba*1ZeHC-&Z!bYc9TTw2ozs}~`VdkQ` z**3qs)aL9n%=a!W_|rDYogWAywyywL4i@noq?FnWjQ8s)A0C8T)<{|+<`E@%ng=h^ z++3F4jz?`+yj^hnXiZ)KzB&R=Y!bY@%he9w8=-l2vQ2)#H;}eimNg*;kaINiF5ruu zXzGMZmPu69fHNdtvvh_{`M?ub@KPKL+QXl^ApLTj&HWR76Sr328q2b3x}Q*iIhx6s zx@?%W(go+NJk2Xto;WxV4-P<3!d>N>yUShKdggP{vHG_!`gTyxuhB`sm!On!w@Do0|DgDdNKC` zuo1(f#j>#j4thSJ2xx8oKGbRSkk-%|Hs4j$+?yAGM=y2<{MVO)#tvTqDhG;!m_hvp(dm1IDUtAuYX)!bFCnYlue z8-O>jw#kvO;1j`TErOd$HS@~N27GG>j2o!=yhTtS5>z(|W{>nmVUG=Bsw^_5^#_PK zby8?`s05h~(4zVVQCG?p_CbkHF?8 zC0FkS2WY{kS7*{6*STR)>eUv5);`BXpF=ZFJ&C5H?+Gk(y)T$*M_@b%3uRh zbJ&PWoWefAtq({T;dyR-n=c$RRAMT-GK=n_#_9kJ%z-n4sjn}}HOd;G#ZxC{1F!57 z{PhdL?oj&kIM?Iq?pD44G&qP4NqO(C56DUFacUqx0D00)r*_{Su?6>UFdVgbs?9kC z>9Nzm939hAZ4m*PYog+yUv6hCPSVGpR@63Pd2|>dFpgzFm=Gmh>hS2SJf8yBAm&iN zB$Gh`(ku#~C2T-SMgpTqk90X(lb^I&|6KsMJSCWpfiwoRx4FfjL6aJqBV-^kYt%Jl`H28pRd&X}eScDMVv zy5ws5ink@NILWSCM`?=lQja^$8Ta~?k~=?;>}WH*bor2Y5qf{Wh#8BD5?ACir0o)O z9+WZtWVAcs>H~s(?e1@0U9wvV2IXoVzR2dj@j9m10Xl8L(Q4;`i8h0BHBDi`fA)80 z32Z7nTC=kY-u%+dB1|2k{RlGvb_%7`OklYmqs{w3@Wvj)9X~kN=J_dR;rXp(SIZN{ElqNy6^8~50lMwy88pcV3K^hYLXZZ5N9R^@>Wf(JLmgS4sX#K(WR zUht#$B?mjrn8OM*&s6w&dKl`Ks;nIxh9dCOYRT$-E->exsrkoro8gx6Yerg{=ce?0 z*W06T??r|RF~GJ~!6nNAJXR~%(FU*X zmfXEg9QUYZe>*&1C%9ypo44 znjM$V@zT=Wg1gqmJ4d&Uvbq04osEjK#Kj8^&G-9h0cpJK^)qe0c~)X^s%;WnyFw<_ zOO@wntf0Afc!6f+cf0t*Wjb5sc=M0WK6p8(2>6&Rmfx*vfG0LfK0O@IO(QK$DBc4+MHYL%?x_*HR4=iDn%N^YSDg))ouwIC zz^Nn~xYx3*r;~D!15j3rI91ee!(}BfYq&n9FyRzZ!XnOIQ3iaHC}A0JLNRhKT=;(k z&cbA^n@JhmDbIh0zF%#>2B=KdvYAo?IE^0f(Rcebnt&OpS~dIIp)zTQa`aI(FsBcK zGp8s0t3C2{oX)&?eF&U+J?S>=k%%27ZhfIOb52jeNgE)KPU>$9KaNhC;h4wXWXflx z3(g4vlD8fL d)hk~U_&=J>0=>+}4n+U}002ovPDHLkV1iATCb9qk diff --git a/lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_uart.png b/lib_ui/src/main/res/mipmap-hdpi/ic_shortcut_uart.png deleted file mode 100644 index e1e7bb3b3e20bdbe880056b4c7da97c56e47ae5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2196 zcmV;F2y6F=P)C62l zH%-qy_uSw4-sgGF^E|)j`3Z&zN~tU$AD9Hp2NnUdfij>Nm+M+i3355)@lFnabX>$skW#7^SeW4l zfYVaSMST+FZq1ZtcIT z-Iyd}K*S9EnI2>34KydLlfM@bkR5;tfwXO-0>}?y@0F)~JNQ}{_Pr~3@x0)C2SkiO zUP5~#kp`n!HEwA&z}?Qs`hxN zP4lBA#?SWV2eDNaDNE16OM)!VaA<>OPCmT+p5VK03EIN;mo-VG#SE}vaf><0VkJ)q zW=Pn#QuE++OV*ET;Lv5kxBhVEQbVURPxF-amY&&+{y$Z9TJY>nf-Zu{O3t)z{b5)Pv zzionzN31@5uGAo$q-tLR#B2{+pOX?M=h(kJj;O8(Tnk$n$ngx%EYtjHjpo<)MlI{~ ziEwbe=Ez3PrjqDdU!7xkWu4~L_3>8t)%BVK>okv*MR&To2o7)1ytYwOm~CPt+NyL{ zkCnj5IdInmUj&tT*y5#>v9ZK(w%tlYZU73h>;>x>bxAx~dhL=)usdMaNH#ZC5;`OH zZ?*?FV)&etZ4ZaqR(lLAn_>v~b6$#r*vhVyv16|0@Z|t??XaW}7EFRSnjowNyWaHJ zf6?&ghUir`bP66m=8@yUsjE@z^xJm@7nR`eSK`-Sci5vm1jnyL=T?6t`0Npn<{O}T zqT?HiEsL);+W%J-`O`08s)x^{{Ja7PWgC80>mh?sF(rCY9X%qBoW4M(Rm72FrActK zQE)@YkGFPZ;&96+gahrlCCyXWOl-v0|!UehB&Yo(rC;Yn^ zzBxaB19we;?=3WZ@e_t`Er`Ciw%AZvX4t*hP?jGZuUevcWTxTg%cI22kcOwq4PPrW ztWP+1Kbdd1zsT_RRj6whY@J~Urly+^&Cu8h z|A;ZTb!~zM1!r2Jz9YKU(MCb@4LIytSYU+rw6{`$oWnCl(mavr0j0Qm4aym(%4>;pktw+$Tvw<%Yt4YY@C%=J*I;nCS9o!?altPn2c z`aJNJbY>K_8GOnjEh?Unpi2XO4?Gr>QihdM)xek2m6sO6UhpV}#x}}?dkLw=Hil&2 zJQK%>l^EAj)R5sNdWiP6S;eZ4&SaJIb81rgaOUAAx0|4t05|h^oIwzkdBD;T$jUsTYmh``9`ZIwqDJLo0{;)< WRWetKjEsc<0000 nRF Toolbox - Dialog Cancel Back @@ -44,4 +43,10 @@ Disconnect Battery + + Discovering services + Please wait + + Connecting + Please wait \ No newline at end of file diff --git a/lib_utils/build.gradle.kts b/lib_utils/build.gradle.kts index 6c2f2906..5ade25e1 100644 --- a/lib_utils/build.gradle.kts +++ b/lib_utils/build.gradle.kts @@ -1,47 +1,11 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - plugins { alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) } android { - namespace = "no.nordicsemi.android.utils" + namespace = "no.nordicsemi.android.toolbox.lib.utils" } dependencies { - implementation(libs.nordic.navigation) - - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.kotlinx.coroutines.core) -} + implementation(libs.nordic.log.timber) +} \ No newline at end of file diff --git a/lib_utils/module-rules.pro b/lib_utils/module-rules.pro index 83f673f9..481bb434 100644 --- a/lib_utils/module-rules.pro +++ b/lib_utils/module-rules.pro @@ -1,17 +1,21 @@ # Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html -# Add any project specific keep options here: - # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/lib_utils/src/main/AndroidManifest.xml b/lib_utils/src/main/AndroidManifest.xml index 07fd1f83..1d26c87a 100644 --- a/lib_utils/src/main/AndroidManifest.xml +++ b/lib_utils/src/main/AndroidManifest.xml @@ -1,35 +1,2 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Ext.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Ext.kt new file mode 100644 index 00000000..a0a97487 --- /dev/null +++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Ext.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.lib.utils + +import timber.log.Timber + +fun Throwable.logAndReport() { + Timber.e(this) +} diff --git a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt new file mode 100644 index 00000000..6de75e91 --- /dev/null +++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/Profile.kt @@ -0,0 +1,37 @@ +package no.nordicsemi.android.toolbox.lib.utils + +enum class Profile { + BPS, + CGM, + CHANNEL_SOUNDING, + CSC, + DFS, + GLS, + HRS, + HTS, + LBS, + RSCS, + + // PRX, TODO: PRX is not implemented yet, it will be added in the future. + BATTERY, + THROUGHPUT, + UART; + + override fun toString(): String = + when (this) { + BPS -> "Blood Pressure" + CGM -> "Continuous Glucose Monitoring" + CHANNEL_SOUNDING -> "Channel Sounding" + CSC -> "Cycling Speed and Cadence" + DFS -> "Direction Finder Service" + GLS -> "Glucose" + HRS -> "Heart Rate Sensor" + HTS -> "Health Thermometer" + LBS -> "Blinky/LED Button Service" + RSCS -> "Running Speed and Cadence Sensor" + BATTERY -> "Battery Service" + THROUGHPUT -> "Throughput Service" + UART -> "UART Service" + } + +} \ No newline at end of file diff --git a/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt new file mode 100644 index 00000000..704b6de4 --- /dev/null +++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/spec/Spec.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.lib.utils.spec + +import java.util.UUID + +val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") +val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") +val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb") +val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb") +val DF_SERVICE_UUID: UUID = UUID.fromString("21490000-494a-4573-98af-f126af76f490") +val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") +val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") +val PRX_SERVICE_UUID: UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") +val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB") +val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") +val BATTERY_SERVICE_UUID: UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") +val THROUGHPUT_SERVICE_UUID: UUID = UUID.fromString("0483DADD-6C9D-6CA9-5D41-03AD4FFF4ABB") +val CHANNEL_SOUND_SERVICE_UUID: UUID = UUID.fromString("0000185B-0000-1000-8000-00805F9B34FB") +val LBS_SERVICE_UUID: UUID = UUID.fromString("00001523-1212-EFDE-1523-785FEABCD123") diff --git a/lib_utils/src/main/java/no/nordicsemi/android/utils/TryOrLog.kt b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/tryOrLog.kt similarity index 74% rename from lib_utils/src/main/java/no/nordicsemi/android/utils/TryOrLog.kt rename to lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/tryOrLog.kt index e3ad5ee7..d19c3e56 100644 --- a/lib_utils/src/main/java/no/nordicsemi/android/utils/TryOrLog.kt +++ b/lib_utils/src/main/java/no/nordicsemi/android/toolbox/lib/utils/tryOrLog.kt @@ -1,4 +1,4 @@ -package no.nordicsemi.android.utils +package no.nordicsemi.android.toolbox.lib.utils suspend fun tryOrLog(block: suspend () -> Unit) { try { diff --git a/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt b/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt deleted file mode 100644 index d5bac361..00000000 --- a/lib_utils/src/main/java/no/nordicsemi/android/utils/Ext.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.utils - -import android.app.ActivityManager -import android.content.Context -import android.util.Log -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch - -val String.Companion.EMPTY - get() = "" - -fun Context.isServiceRunning(serviceClassName: String): Boolean { - val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val services = activityManager.getRunningServices(Integer.MAX_VALUE) - return services.find { it.service.className == serviceClassName } != null -} - -private val exceptionHandler = CoroutineExceptionHandler { _, t -> - Log.e("COROUTINE-EXCEPTION", "Uncaught exception", t) -} - -fun CoroutineScope.launchWithCatch(block: suspend CoroutineScope.() -> Unit) = - launch(SupervisorJob() + exceptionHandler) { - block() - } diff --git a/permissions-ranging/build.gradle.kts b/permissions-ranging/build.gradle.kts new file mode 100644 index 00000000..8d251185 --- /dev/null +++ b/permissions-ranging/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + alias(libs.plugins.nordic.feature) +} + +android { + namespace = "no.nordicsemi.android.permissions_ranging" +} + +dependencies { + implementation(libs.accompanist.permissions) +} \ No newline at end of file diff --git a/permissions-ranging/module-rules.pro b/permissions-ranging/module-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/permissions-ranging/module-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/permissions-ranging/src/main/AndroidManifest.xml b/permissions-ranging/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b406fd5b --- /dev/null +++ b/permissions-ranging/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt new file mode 100644 index 00000000..3b0350dc --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/RequestRangingPermission.kt @@ -0,0 +1,42 @@ +package no.nordicsemi.android.permissions_ranging + +import android.app.Activity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason +import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState +import no.nordicsemi.android.permissions_ranging.view.RangingPermissionRequestView +import no.nordicsemi.android.permissions_ranging.viewmodel.RangingPermissionViewModel + +@Composable +fun RequestRangingPermission( + onChanged: (Boolean) -> Unit = {}, + content: @Composable (Boolean) -> Unit, +) { + val permissionViewModel = hiltViewModel() + val context = LocalContext.current + val activity = context as? Activity + + val state by activity?.let { permissionViewModel.requestRangingPermission(it) }!! + .collectAsStateWithLifecycle() + + + LaunchedEffect(state) { + onChanged(state is RangingPermissionState.Available) + } + + when (val s = state) { + is RangingPermissionState.Available -> content(true) + is RangingPermissionState.NotAvailable -> { + when (s.reason) { + RangingNotAvailableReason.NOT_AVAILABLE -> RangingPermissionRequestView(content) + RangingNotAvailableReason.PERMISSION_DENIED -> content(false) + } + } + } + +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt new file mode 100644 index 00000000..0ef4c454 --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/repository/RangingStateManager.kt @@ -0,0 +1,85 @@ +package no.nordicsemi.android.permissions_ranging.repository + +import android.app.Activity +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow +import no.nordicsemi.android.permissions_ranging.utils.LocalDataProvider +import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason +import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState +import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionUtils +import javax.inject.Inject +import javax.inject.Singleton + +private const val REFRESH_PERMISSIONS = + "no.nordicsemi.android.permissions_ranging.repository.REFRESH_RANGING_PERMISSIONS" +private const val RANGING_PERMISSION_REQUEST_CODE = 1001 + +@Singleton +internal class RangingStateManager @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val dataProvider = LocalDataProvider(context) + private val utils = RangingPermissionUtils(context, dataProvider) + + fun rangingPermissionState(activity: Activity) = callbackFlow { + trySend(getRangingPermissionState()) + + val rangingStateChangeHandler = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + trySend(getRangingPermissionState()) + } + } + ContextCompat.registerReceiver( + context, + rangingStateChangeHandler, + IntentFilter(), + ContextCompat.RECEIVER_EXPORTED + ) + + ActivityCompat.requestPermissions( + activity, + arrayOf("android.permission.RANGING"), + RANGING_PERMISSION_REQUEST_CODE + ) + + awaitClose { + + context.unregisterReceiver(rangingStateChangeHandler) + } + + } + + fun refreshRangingPermissionState() { + val intent = Intent(REFRESH_PERMISSIONS) + context.sendBroadcast(intent) + } + + fun markRangingPermissionAsRequested() { + dataProvider.isRangingPermissionRequested = true + } + + fun isRangingPermissionDenied(): Boolean { + return utils.isRangingPermissionDenied() + } + + private fun getRangingPermissionState(): RangingPermissionState { + return when { + !utils.isRangingPermissionAvailable -> RangingPermissionState.NotAvailable( + RangingNotAvailableReason.NOT_AVAILABLE + ) + + utils.isRangingPermissionAvailable && !utils.isRangingPermissionGranted -> RangingPermissionState.NotAvailable( + RangingNotAvailableReason.PERMISSION_DENIED + ) + + else -> RangingPermissionState.Available + } + } +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt new file mode 100644 index 00000000..2420d825 --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/LocalDataProvider.kt @@ -0,0 +1,35 @@ +package no.nordicsemi.android.permissions_ranging.utils + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.core.app.ActivityCompat +import androidx.core.content.edit +import javax.inject.Inject +import javax.inject.Singleton + +private const val SHARED_PREFS_NAME = "SHARED_PREFS_RANGING" +private const val PREFS_PERMISSION_REQUESTED = "ranging_permission_requested" + +@Singleton +internal class LocalDataProvider @Inject constructor( + private val context: Context, +) { + private val sharedPrefs: SharedPreferences + get() = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE) + + val isBaklavaOrAbove: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA) + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA + + /** + * The first time an app requests a permission there is no 'Don't Allow' checkbox and + * [ActivityCompat.shouldShowRequestPermissionRationale] returns false. + */ + var isRangingPermissionRequested: Boolean + get() = sharedPrefs.getBoolean(PREFS_PERMISSION_REQUESTED, false) + set(value) { + sharedPrefs.edit { putBoolean(PREFS_PERMISSION_REQUESTED, value) } + } +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt new file mode 100644 index 00000000..f6521b95 --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionState.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.permissions_ranging.utils + +internal sealed class RangingPermissionState { + /** + * Ranging is available and the app has the required permissions. + */ + data object Available : RangingPermissionState() + + /** + * Ranging is not available. + */ + data class NotAvailable( + val reason: RangingNotAvailableReason, + ) : RangingPermissionState() +} + +internal enum class RangingNotAvailableReason { + /** Ranging is not available on this device. */ + NOT_AVAILABLE, + + /** The app does not have the required permissions. */ + PERMISSION_DENIED, +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt new file mode 100644 index 00000000..e3f0ccab --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/utils/RangingPermissionUtils.kt @@ -0,0 +1,53 @@ +package no.nordicsemi.android.permissions_ranging.utils + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.PackageManager +import android.os.Build +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.core.content.ContextCompat + +internal class RangingPermissionUtils( + private val context: Context, + private val dataProvider: LocalDataProvider, +) { + val isRangingPermissionAvailable: Boolean + @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.BAKLAVA) + get() = Build.VERSION.SDK_INT >= 36 + + + val isRangingPermissionGranted: Boolean + get() = isRangingPermissionAvailable && + ContextCompat.checkSelfPermission( + context, + Manifest.permission.RANGING + ) == PackageManager.PERMISSION_GRANTED + + fun isRangingPermissionDenied(): Boolean { + return dataProvider.isBaklavaOrAbove && + dataProvider.isRangingPermissionRequested && // Ranging permission was requested. + !isRangingPermissionGranted // Ranging permission is not granted + && !context.findActivity() + .shouldShowRequestPermissionRationale(Manifest.permission.RANGING) + + } + + /** + * Finds the activity from the given context. + * + * https://github.com/google/accompanist/blob/6611ebda55eb2948eca9e1c89c2519e80300855a/permissions/src/main/java/com/google/accompanist/permissions/PermissionsUtil.kt#L99 + * + * @throws IllegalStateException if no activity was found. + * @return the activity. + */ + private fun Context.findActivity(): Activity { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + throw IllegalStateException("no activity") + } +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt new file mode 100644 index 00000000..72a95df8 --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/view/RangingPermissionRequestView.kt @@ -0,0 +1,49 @@ +package no.nordicsemi.android.permissions_ranging.view + +import android.Manifest +import android.os.Build +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.PermissionStatus +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import no.nordicsemi.android.permissions_ranging.viewmodel.RangingPermissionViewModel + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +internal fun RangingPermissionRequestView( + content: @Composable (Boolean) -> Unit, +) { + val rangingPermissionViewModel = hiltViewModel() + + val permission = if (Build.VERSION.SDK_INT >= 36) + Manifest.permission.RANGING else null + + val rangingPermission = permission?.let { + rememberPermissionState(it) + } + + if (rangingPermission != null) { + when (rangingPermission.status) { + is PermissionStatus.Denied -> { + LaunchedEffect(!rangingPermission.status.isGranted) { + rangingPermissionViewModel.markRangingPermissionRequested() + rangingPermission.launchPermissionRequest() + if (!rangingPermission.status.isGranted) { + rangingPermissionViewModel.markRangingPermissionDenied() + } + rangingPermissionViewModel.refreshRangingPermissionState() + } + content(rangingPermission.status.isGranted) + } + + PermissionStatus.Granted -> content(true) + } + } else { + rangingPermissionViewModel.refreshRangingPermissionState() + content(true) + } + +} \ No newline at end of file diff --git a/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt new file mode 100644 index 00000000..87a3103d --- /dev/null +++ b/permissions-ranging/src/main/java/no/nordicsemi/android/permissions_ranging/viewmodel/RangingPermissionViewModel.kt @@ -0,0 +1,37 @@ +package no.nordicsemi.android.permissions_ranging.viewmodel + +import android.app.Activity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import no.nordicsemi.android.permissions_ranging.repository.RangingStateManager +import no.nordicsemi.android.permissions_ranging.utils.RangingNotAvailableReason +import no.nordicsemi.android.permissions_ranging.utils.RangingPermissionState +import javax.inject.Inject + +@HiltViewModel +internal class RangingPermissionViewModel @Inject constructor( + private val rangingStateManager: RangingStateManager, +) : ViewModel() { + fun requestRangingPermission(activity: Activity) = + rangingStateManager.rangingPermissionState(activity) + .stateIn( + viewModelScope, + SharingStarted.Lazily, + RangingPermissionState.NotAvailable(RangingNotAvailableReason.NOT_AVAILABLE), + ) + + fun refreshRangingPermissionState() { + rangingStateManager.refreshRangingPermissionState() + } + + fun markRangingPermissionRequested() { + rangingStateManager.markRangingPermissionAsRequested() + } + + fun markRangingPermissionDenied() { + rangingStateManager.isRangingPermissionDenied() + } +} \ No newline at end of file diff --git a/profile-parsers/build.gradle.kts b/profile-parsers/build.gradle.kts new file mode 100644 index 00000000..75cc41ec --- /dev/null +++ b/profile-parsers/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + alias(libs.plugins.nordic.feature) +} + +android { + namespace = "no.nordicsemi.android.toolbox.profile.parser" +} + +dependencies { + implementation(libs.nordic.blek.client.android) + implementation(libs.nordic.kotlin.data) + + // Unit test dependencies + testImplementation(libs.junit4) + testImplementation(libs.kotlin.junit) +} \ No newline at end of file diff --git a/profile-parsers/module-rules.pro b/profile-parsers/module-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/profile-parsers/module-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/profile-parsers/src/main/AndroidManifest.xml b/profile-parsers/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1d26c87a --- /dev/null +++ b/profile-parsers/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/WorkingMode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParser.kt similarity index 89% rename from profile_gls/src/main/java/no/nordicsemi/android/gls/data/WorkingMode.kt rename to profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParser.kt index 33ee437c..4120e58d 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/WorkingMode.kt +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParser.kt @@ -29,10 +29,10 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.gls.data +package no.nordicsemi.android.toolbox.profile.parser.battery -internal enum class WorkingMode { - ALL, - LAST, - FIRST +object BatteryLevelParser { + + fun parse(data: ByteArray): Int? = + if (data.size == 1) data[0].toInt() and 0xFF else null } diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPMStatus.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPMStatus.kt new file mode 100644 index 00000000..04afcd12 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPMStatus.kt @@ -0,0 +1,21 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +class BPMStatus( + val bodyMovementDetected: Boolean, + val cuffTooLose: Boolean, + val irregularPulseDetected: Boolean, + val pulseRateInRange: Boolean, + val pulseRateExceedsUpperLimit: Boolean, + val pulseRateIsLessThenLowerLimit: Boolean, + val improperMeasurementPosition: Boolean +) { + constructor(value: Int) : this( + bodyMovementDetected = value and 0x01 != 0, + cuffTooLose = value and 0x02 != 0, + irregularPulseDetected = value and 0x04 != 0, + pulseRateInRange = value and 0x18 shr 3 == 0, + pulseRateExceedsUpperLimit = value and 0x18 shr 3 == 1, + pulseRateIsLessThenLowerLimit = value and 0x18 shr 3 == 2, + improperMeasurementPosition = value and 0x20 != 0 + ) +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPSData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPSData.kt new file mode 100644 index 00000000..34b0fc8c --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BPSData.kt @@ -0,0 +1,30 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +import androidx.annotation.FloatRange +import androidx.annotation.IntRange +import java.util.Calendar + +enum class BloodPressureType(internal val value: Int) { + UNIT_MMHG(0), + UNIT_KPA(1) +} + +data class BloodPressureMeasurementData( + val systolic: Float, + val diastolic: Float, + val meanArterialPressure: Float, + val unit: BloodPressureType, + val pulseRate: Float?, + val userID: Int?, + val status: BPMStatus?, + val calendar: Calendar? +) + +data class IntermediateCuffPressureData( + @param:FloatRange(from = 0.0) val cuffPressure: Float, + val unit: BloodPressureType, + @param:FloatRange(from = 0.0) val pulseRate: Float? = null, + @param:IntRange(from = 0, to = 255) val userID: Int? = null, + val status: BPMStatus? = null, + val calendar: Calendar? = null +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureData.kt new file mode 100644 index 00000000..97d0e308 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureData.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +/** + * Data class to hold the Blood Pressure Feature data. + * @param bodyMovementDetection Indicates if body movement detection is supported. + * @param cuffFitDetection Indicates if cuff fit detection is supported. + * @param irregularPulseDetection Indicates if irregular pulse detection is supported. + * @param pulseRateRangeDetection Indicates if pulse rate range detection is supported. + * @param measurementPositionDetection Indicates if measurement position detection is supported. + */ +data class BloodPressureFeatureData( + val bodyMovementDetection: Boolean, + val cuffFitDetection: Boolean, + val irregularPulseDetection: Boolean, + val pulseRateRangeDetection: Boolean, + val measurementPositionDetection: Boolean +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureParser.kt new file mode 100644 index 00000000..1c26c906 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureFeatureParser.kt @@ -0,0 +1,27 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +/** + * Blood Pressure Feature data parser. + * This parser is used to parse the Blood Pressure Feature data from the Bluetooth GATT characteristic. + * The data is a 2-byte value that contains flags indicating the supported features of the Blood Pressure service. + */ +object BloodPressureFeatureParser { + + fun parse(data: ByteArray): BloodPressureFeatureData? { + if (data.size != 2) return null + val flags: Int = data[0].toInt() or (data[1].toInt() shl 8) + val bodyMovementDetection = flags and 0x0001 != 0 + val cuffFitDetection = flags and 0x0002 != 0 + val irregularPulseDetection = flags and 0x0004 != 0 + val pulseRateRangeDetection = flags and 0x0008 != 0 + val measurementPositionDetection = flags and 0x0010 != 0 + + return BloodPressureFeatureData( + bodyMovementDetection = bodyMovementDetection, + cuffFitDetection = cuffFitDetection, + irregularPulseDetection = irregularPulseDetection, + pulseRateRangeDetection = pulseRateRangeDetection, + measurementPositionDetection = measurementPositionDetection + ) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParser.kt new file mode 100644 index 00000000..f8cc107a --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParser.kt @@ -0,0 +1,89 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +import no.nordicsemi.android.toolbox.profile.parser.date.DateTimeParser +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import java.util.Calendar + +object BloodPressureMeasurementParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): BloodPressureMeasurementData? { + if (data.size < 7) return null + + var offset = 0 + val flags: Int = data.getInt(offset, IntFormat.UINT8, byteOrder) + .also { offset++ } + + // See UNIT_* for unit options + val unit: BloodPressureType = if (flags and 0x01 == BloodPressureType.UNIT_KPA.value) { + BloodPressureType.UNIT_KPA + } else { + BloodPressureType.UNIT_MMHG + } + val timestampPresent = flags and 0x02 != 0 + val pulseRatePresent = flags and 0x04 != 0 + val userIdPresent = flags and 0x08 != 0 + val measurementStatusPresent = flags and 0x10 != 0 + + if (data.size < (7 + + (if (timestampPresent) 7 else 0) + (if (pulseRatePresent) 2 else 0) + + (if (userIdPresent) 1 else 0) + if (measurementStatusPresent) 2 else 0) + ) { + return null + } + + // Following bytes - systolic, diastolic and mean arterial pressure + val systolic: Float = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + + val diastolic: Float = data.getFloat(offset + 2, FloatFormat.IEEE_11073_16_BIT, byteOrder) + val meanArterialPressure: Float = + data.getFloat(offset + 4, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 6 + + // Parse timestamp if present + var calendar: Calendar? = null + if (timestampPresent) { + calendar = DateTimeParser.parse(data, offset) + offset += 7 + } + + // Parse pulse rate if present + var pulseRate: Float? = null + if (pulseRatePresent) { + pulseRate = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 2 + } + + // Read user id if present + var userId: Int? = null + if (userIdPresent) { + userId = data.getInt(offset, IntFormat.UINT8) + offset += 1 + } + + // Read measurement status if present + var status: BPMStatus? = null + if (measurementStatusPresent) { + val measurementStatus: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + // offset += 2; + status = BPMStatus(measurementStatus) + } + + return BloodPressureMeasurementData( + systolic, + diastolic, + meanArterialPressure, + unit, + pulseRate, + userId, + status, + calendar + ) + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParser.kt new file mode 100644 index 00000000..556c7910 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParser.kt @@ -0,0 +1,76 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +import no.nordicsemi.android.toolbox.profile.parser.date.DateTimeParser +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import java.util.Calendar + +object IntermediateCuffPressureParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): IntermediateCuffPressureData? { + if (data.size < 7) return null + + // First byte: flags + var offset = 0 + val flags: Int = data.getInt(offset++, IntFormat.UINT8) + + // See UNIT_* for unit options + val unit = if (flags and 0x01 == BloodPressureType.UNIT_KPA.value) { + BloodPressureType.UNIT_KPA + } else { + BloodPressureType.UNIT_MMHG + } + val timestampPresent = flags and 0x02 != 0 + val pulseRatePresent = flags and 0x04 != 0 + val userIdPresent = flags and 0x08 != 0 + val measurementStatusPresent = flags and 0x10 != 0 + + if (data.size < (7 + + (if (timestampPresent) 7 else 0) + (if (pulseRatePresent) 2 else 0) + + (if (userIdPresent) 1 else 0) + if (measurementStatusPresent) 2 else 0) + ) { + return null + } + + // Following bytes - systolic, diastolic and mean arterial pressure + val cuffPressure: Float = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 6 + + // Parse timestamp if present + var calendar: Calendar? = null + if (timestampPresent) { + calendar = DateTimeParser.parse(data, offset) + offset += 7 + } + + // Parse pulse rate if present + var pulseRate: Float? = null + if (pulseRatePresent) { + pulseRate = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 2 + } + + // Read user id if present + var userId: Int? = null + if (userIdPresent) { + userId = data.getInt(offset, IntFormat.UINT8) + offset += 1 + } + + // Read measurement status if present + var status: BPMStatus? = null + if (measurementStatusPresent) { + val measurementStatus: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + // offset += 2; + status = BPMStatus(measurementStatus) + } + + return IntermediateCuffPressureData(cuffPressure, unit, pulseRate, userId, status, calendar) + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMFeatureParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMFeatureParser.kt new file mode 100644 index 00000000..8db280d8 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMFeatureParser.kt @@ -0,0 +1,43 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms + +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMFeatures +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMFeaturesEnvelope +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object CGMFeatureParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): CGMFeaturesEnvelope? { + if (data.size != 6) return null + + val featuresValue = data.getInt(0, IntFormat.UINT24, byteOrder) + val typeAndSampleLocation = data.getInt(3, IntFormat.UINT8) + val expectedCrc = data.getInt(4, IntFormat.UINT16, byteOrder) + + val features = CGMFeatures(featuresValue) + if (features.e2eCrcSupported) { + val actualCrc = CRC16.MCRF4XX(data, 0, 4) + if (actualCrc != expectedCrc) return null + } else { + // If the device doesn't support E2E-safety the value of the field shall be set to 0xFFFF. + if (expectedCrc != 0xFFFF) return null + } + + val type = typeAndSampleLocation and 0x0F // least significant nibble + + val sampleLocation = typeAndSampleLocation shr 4 // most significant nibble + + return CGMFeaturesEnvelope( + features, + type, + sampleLocation, + features.e2eCrcSupported, + features.e2eCrcSupported + ) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMMeasurementParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMMeasurementParser.kt new file mode 100644 index 00000000..2feb047d --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMMeasurementParser.kt @@ -0,0 +1,118 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms + +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMRecord +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMStatus +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object CGMMeasurementParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): List? { + + if (data.isEmpty()) return null + + var offset = 0 + + val result = mutableListOf() + + while (offset < data.size) { + // Packet size + val size: Int = data.getInt(offset, IntFormat.UINT8) + + if (size < 6 || offset + size > data.size) return null + + // Flags + val flags: Int = data.getInt(offset + 1, IntFormat.UINT8) + + val cgmTrendInformationPresent = flags and 0x01 != 0 + val cgmQualityInformationPresent = flags and 0x02 != 0 + val sensorWarningOctetPresent = flags and 0x20 != 0 + val sensorCalTempOctetPresent = flags and 0x40 != 0 + val sensorStatusOctetPresent = flags and 0x80 != 0 + + val dataSize = + (6 + (if (cgmTrendInformationPresent) 2 else 0) + (if (cgmQualityInformationPresent) 2 else 0) + + (if (sensorWarningOctetPresent) 1 else 0) + (if (sensorCalTempOctetPresent) 1 else 0) + + if (sensorStatusOctetPresent) 1 else 0) + + if (size != dataSize && size != dataSize + 2) { + return null + } + val crcPresent = size == dataSize + 2 + + if (crcPresent) { + val expectedCrc: Int = data.getInt(offset + dataSize, IntFormat.UINT16, byteOrder) + val actualCrc: Int = CRC16.MCRF4XX(data, offset, dataSize) + if (expectedCrc != actualCrc) { + continue + } + } + offset += 2 + + // Glucose concentration + val glucoseConcentration: Float = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 2 + + // Time offset (in minutes since Session Start) + val timeOffset: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + offset += 2 + + // Sensor Status Annunciation + var warningStatus = 0 + var calibrationTempStatus = 0 + var sensorStatus = 0 + var status: CGMStatus? = null + + if (sensorWarningOctetPresent) { + warningStatus = data.getInt(offset++, IntFormat.UINT8) + } + + if (sensorCalTempOctetPresent) { + calibrationTempStatus = data.getInt(offset++, IntFormat.UINT8) + } + + if (sensorStatusOctetPresent) { + sensorStatus = data.getInt(offset++, IntFormat.UINT8) + } + + if (sensorWarningOctetPresent || sensorCalTempOctetPresent || sensorStatusOctetPresent) { + status = CGMStatus(warningStatus, calibrationTempStatus, sensorStatus) + } + + // CGM Trend Information + var trend: Float? = null + if (cgmTrendInformationPresent) { + trend = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 2 + } + + // CGM Quality Information + var quality: Float? = null + if (cgmQualityInformationPresent) { + quality = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + offset += 2 + } + + // E2E-CRC + if (crcPresent) { + offset += 2 + } + CGMRecord( + glucoseConcentration = glucoseConcentration, + trend = trend, + quality = quality, + status = status, + timeOffset = timeOffset, + crcPresent = crcPresent + ).let { result.add(it) } + } + return result.toList() + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMSpecificOpsControlPointParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMSpecificOpsControlPointParser.kt new file mode 100644 index 00000000..36e82cba --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMSpecificOpsControlPointParser.kt @@ -0,0 +1,200 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms + +import android.annotation.SuppressLint +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMCalibrationStatus +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMErrorCode +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMOpCode +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMSpecificOpsControlPointData +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object CGMSpecificOpsControlPointParser { + + private const val OP_CODE_COMMUNICATION_INTERVAL_RESPONSE = 3 + private const val OP_CODE_CALIBRATION_VALUE_RESPONSE = 6 + private const val OP_CODE_PATIENT_HIGH_ALERT_LEVEL_RESPONSE = 9 + private const val OP_CODE_PATIENT_LOW_ALERT_LEVEL_RESPONSE = 12 + private const val OP_CODE_HYPO_ALERT_LEVEL_RESPONSE = 15 + private const val OP_CODE_HYPER_ALERT_LEVEL_RESPONSE = 18 + private const val OP_CODE_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE = 21 + private const val OP_CODE_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE = 24 + private const val OP_CODE_RESPONSE_CODE = 28 + private const val CGM_RESPONSE_SUCCESS = 1 + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): CGMSpecificOpsControlPointData? { + if (data.size < 2) return null + + // Read the Op Code + val opCode: Int = data.getInt(0, IntFormat.UINT8) + + // Estimate the expected operand size based on the Op Code + val expectedOperandSize: Int = when (opCode) { + OP_CODE_COMMUNICATION_INTERVAL_RESPONSE -> 1 + OP_CODE_CALIBRATION_VALUE_RESPONSE -> 10 + OP_CODE_PATIENT_HIGH_ALERT_LEVEL_RESPONSE, + OP_CODE_PATIENT_LOW_ALERT_LEVEL_RESPONSE, + OP_CODE_HYPO_ALERT_LEVEL_RESPONSE, + OP_CODE_HYPER_ALERT_LEVEL_RESPONSE, + OP_CODE_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE, + OP_CODE_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE -> 2 + + OP_CODE_RESPONSE_CODE -> 2 + else -> return null + } + + // Verify packet length + if (data.size != 1 + expectedOperandSize && data.size != 1 + expectedOperandSize + 2) { + return null + } + + // Verify CRC if present + val crcPresent = data.size == 1 + expectedOperandSize + 2 // opCode + expected operand + CRC + + if (crcPresent) { + val expectedCrc: Int = data.getInt(1 + expectedOperandSize, IntFormat.UINT16, byteOrder) + val actualCrc: Int = CRC16.MCRF4XX(data, 0, 1 + expectedOperandSize) + if (expectedCrc != actualCrc) { + return CGMSpecificOpsControlPointData( + isOperationCompleted = false, + secured = true, + crcValid = false + ) + } + } + + when (opCode) { + OP_CODE_COMMUNICATION_INTERVAL_RESPONSE -> { + val interval: Int = data.getInt(1, IntFormat.UINT8) + + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_COMMUNICATION_INTERVAL, + glucoseCommunicationInterval = interval, + secured = crcPresent, + crcValid = crcPresent, + ) + } + + OP_CODE_CALIBRATION_VALUE_RESPONSE -> { + val glucoseConcentrationOfCalibration = + data.getFloat(1, FloatFormat.IEEE_11073_16_BIT, byteOrder) + val calibrationTime = data.getInt(3, IntFormat.UINT16, byteOrder) + val calibrationTypeAndSampleLocation = data.getInt(5, IntFormat.UINT8) + + @SuppressLint("WrongConstant") val calibrationType = + calibrationTypeAndSampleLocation and 0x0F + val calibrationSampleLocation = calibrationTypeAndSampleLocation shr 4 + val nextCalibrationTime: Int = data.getInt(6, IntFormat.UINT16, byteOrder) + val calibrationDataRecordNumber: Int = data.getInt(8, IntFormat.UINT16, byteOrder) + val calibrationStatus: Int = data.getInt(10, IntFormat.UINT8) + + return CGMSpecificOpsControlPointData( + glucoseConcentrationOfCalibration = glucoseConcentrationOfCalibration, + calibrationTime = calibrationTime, + nextCalibrationTime = nextCalibrationTime, + type = calibrationType, + sampleLocation = calibrationSampleLocation, + calibrationDataRecordNumber = calibrationDataRecordNumber, + calibrationStatus = CGMCalibrationStatus(calibrationStatus), + crcValid = crcPresent, + secured = crcPresent + ) + } + + OP_CODE_RESPONSE_CODE -> { + val requestCode: Int = data.getInt(1, IntFormat.UINT8) + val responseCode: Int = data.getInt(2, IntFormat.UINT8) + + if (responseCode == CGM_RESPONSE_SUCCESS) { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.create(requestCode), + secured = crcPresent, + crcValid = crcPresent, + ) + } else { + return CGMSpecificOpsControlPointData( + isOperationCompleted = false, + requestCode = CGMOpCode.create(requestCode), + errorCode = CGMErrorCode.create(responseCode), + secured = crcPresent, + crcValid = crcPresent + ) + } + } + } + + // Read SFLOAT value + val alertLevel: Float = data.getFloat(1, FloatFormat.IEEE_11073_16_BIT, byteOrder) + + when (opCode) { + OP_CODE_PATIENT_HIGH_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_PATIENT_HIGH_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + + OP_CODE_PATIENT_LOW_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_PATIENT_LOW_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + + OP_CODE_HYPO_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_HYPO_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + + OP_CODE_HYPER_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_HYPER_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + + OP_CODE_RATE_OF_DECREASE_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_RATE_OF_DECREASE_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + + OP_CODE_RATE_OF_INCREASE_ALERT_LEVEL_RESPONSE -> { + return CGMSpecificOpsControlPointData( + isOperationCompleted = true, + requestCode = CGMOpCode.CGM_OP_CODE_SET_RATE_OF_INCREASE_ALERT_LEVEL, + alertLevel = alertLevel, + secured = crcPresent, + crcValid = crcPresent + ) + } + } + return null + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMStatusParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMStatusParser.kt new file mode 100644 index 00000000..cb579cc3 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/CGMStatusParser.kt @@ -0,0 +1,35 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms + +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMStatus +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMStatusEnvelope +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object CGMStatusParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): CGMStatusEnvelope? { + if (data.size != 5 && data.size != 7) return null + + val timeOffset: Int = data.getInt(0, IntFormat.UINT16, byteOrder) + val warningStatus: Int = data.getInt(2, IntFormat.UINT8) + val calibrationTempStatus: Int = data.getInt(3, IntFormat.UINT8) + val sensorStatus: Int = data.getInt(4, IntFormat.UINT8) + + val crcPresent = data.size == 7 + if (crcPresent) { + val actualCrc: Int = CRC16.MCRF4XX(data, 0, 5) + val expectedCrc: Int = data.getInt(5, IntFormat.UINT16, byteOrder) + if (actualCrc != expectedCrc) { + return null + } + } + + val status = CGMStatus(warningStatus, calibrationTempStatus, sensorStatus) + return CGMStatusEnvelope(status, timeOffset, crcPresent, crcPresent) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMCalibrationStatus.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMCalibrationStatus.kt new file mode 100644 index 00000000..22151191 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMCalibrationStatus.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +class CGMCalibrationStatus(val value: Int) { + val rejected: Boolean = value and 0x01 != 0 + val dataOutOfRange: Boolean = value and 0x02 != 0 + val processPending: Boolean = value and 0x04 != 0 +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMErrorCode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMErrorCode.kt new file mode 100644 index 00000000..fc4abc35 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMErrorCode.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +enum class CGMErrorCode(val value: Int) { + CGM_ERROR_OP_CODE_NOT_SUPPORTED(2), + CGM_ERROR_INVALID_OPERAND(3), + CGM_ERROR_PROCEDURE_NOT_COMPLETED(4), + CGM_ERROR_PARAMETER_OUT_OF_RANGE(5); + + companion object { + fun create(value: Int): CGMErrorCode { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create error code for value: $value") + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMFeatures.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMFeatures.kt new file mode 100644 index 00000000..6c39bf07 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMFeatures.kt @@ -0,0 +1,50 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +data class CGMFeaturesEnvelope( + val features: CGMFeatures, + val type: Int, + val sampleLocation: Int, + val secured: Boolean, + val crcValid: Boolean +) + +class CGMFeatures( + val calibrationSupported: Boolean, + val patientHighLowAlertsSupported: Boolean, + val hypoAlertsSupported: Boolean, + val hyperAlertsSupported: Boolean, + val rateOfIncreaseDecreaseAlertsSupported: Boolean, + val deviceSpecificAlertSupported: Boolean, + val sensorMalfunctionDetectionSupported: Boolean, + val sensorTempHighLowDetectionSupported: Boolean, + val sensorResultHighLowSupported: Boolean, + val lowBatteryDetectionSupported: Boolean, + val sensorTypeErrorDetectionSupported: Boolean, + val generalDeviceFaultSupported: Boolean, + val e2eCrcSupported: Boolean, + val multipleBondSupported: Boolean, + val multipleSessionsSupported: Boolean, + val cgmTrendInfoSupported: Boolean, + val cgmQualityInfoSupported: Boolean +) { + + constructor(value: Int) : this( + calibrationSupported = value and 0x000001 != 0, + patientHighLowAlertsSupported = value and 0x000002 != 0, + hypoAlertsSupported = value and 0x000004 != 0, + hyperAlertsSupported = value and 0x000008 != 0, + rateOfIncreaseDecreaseAlertsSupported = value and 0x000010 != 0, + deviceSpecificAlertSupported = value and 0x000020 != 0, + sensorMalfunctionDetectionSupported = value and 0x000040 != 0, + sensorTempHighLowDetectionSupported = value and 0x000080 != 0, + sensorResultHighLowSupported = value and 0x000100 != 0, + lowBatteryDetectionSupported = value and 0x000200 != 0, + sensorTypeErrorDetectionSupported = value and 0x000400 != 0, + generalDeviceFaultSupported = value and 0x000800 != 0, + e2eCrcSupported = value and 0x001000 != 0, + multipleBondSupported = value and 0x002000 != 0, + multipleSessionsSupported = value and 0x004000 != 0, + cgmTrendInfoSupported = value and 0x008000 != 0, + cgmQualityInfoSupported = value and 0x010000 != 0 + ) +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMOpCode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMOpCode.kt new file mode 100644 index 00000000..cd39fbd3 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMOpCode.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +enum class CGMOpCode(val value: Int) { + CGM_OP_CODE_SET_COMMUNICATION_INTERVAL(1), + CGM_OP_CODE_SET_CALIBRATION_VALUE(4), + CGM_OP_CODE_SET_PATIENT_HIGH_ALERT_LEVEL(7), + CGM_OP_CODE_SET_PATIENT_LOW_ALERT_LEVEL(10), + CGM_OP_CODE_SET_HYPO_ALERT_LEVEL(13), + CGM_OP_CODE_SET_HYPER_ALERT_LEVEL(16), + CGM_OP_CODE_SET_RATE_OF_DECREASE_ALERT_LEVEL(19), + CGM_OP_CODE_SET_RATE_OF_INCREASE_ALERT_LEVEL(22), + CGM_OP_CODE_RESET_DEVICE_SPECIFIC_ERROR(25), + CGM_OP_CODE_START_SESSION(26), + CGM_OP_CODE_STOP_SESSION(27); + + companion object { + fun create(value: Int): CGMOpCode { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create op code for value: $value") + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMRecord.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMRecord.kt new file mode 100644 index 00000000..63a6656f --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMRecord.kt @@ -0,0 +1,10 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +data class CGMRecord( + val glucoseConcentration: Float, + val trend: Float?, + val quality: Float?, + val status: CGMStatus?, + val timeOffset: Int, + val crcPresent: Boolean +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMSpecificOpsControlPointData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMSpecificOpsControlPointData.kt new file mode 100644 index 00000000..22b93b05 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMSpecificOpsControlPointData.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +data class CGMSpecificOpsControlPointData( + var isOperationCompleted: Boolean = false, + val secured: Boolean = false, + val crcValid: Boolean = false, + val requestCode: CGMOpCode? = null, + val errorCode: CGMErrorCode? = null, + val glucoseCommunicationInterval: Int = 0, + val glucoseConcentrationOfCalibration: Float = 0f, + val calibrationTime: Int = 0, + val nextCalibrationTime: Int = 0, + val type: Int = 0, + val sampleLocation: Int = 0, + val calibrationDataRecordNumber: Int = 0, + val calibrationStatus: CGMCalibrationStatus? = null, + val alertLevel: Float = 0f +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMStatus.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMStatus.kt new file mode 100644 index 00000000..f5a1bc28 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/cgms/data/CGMStatus.kt @@ -0,0 +1,55 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgms.data + +data class CGMStatusEnvelope( + val status: CGMStatus, + val timeOffset: Int, + val secured: Boolean, + val crcValid: Boolean +) + +data class CGMStatus( + val sessionStopped: Boolean, + val deviceBatteryLow: Boolean, + val sensorTypeIncorrectForDevice: Boolean, + val sensorMalfunction: Boolean, + val deviceSpecificAlert: Boolean, + val generalDeviceFault: Boolean, + val timeSyncRequired: Boolean, + val calibrationNotAllowed: Boolean, + val calibrationRecommended: Boolean, + val calibrationRequired: Boolean, + val sensorTemperatureTooHigh: Boolean, + val sensorTemperatureTooLow: Boolean, + val sensorResultLowerThenPatientLowLevel: Boolean, + val sensorResultHigherThenPatientHighLevel: Boolean, + val sensorResultLowerThenHypoLevel: Boolean, + val sensorResultHigherThenHyperLevel: Boolean, + val sensorRateOfDecreaseExceeded: Boolean, + val sensorRateOfIncreaseExceeded: Boolean, + val sensorResultLowerThenDeviceCanProcess: Boolean, + val sensorResultHigherThenDeviceCanProcess: Boolean +) { + + constructor(warningStatus: Int, calibrationTempStatus: Int, sensorStatus: Int) : this( + sessionStopped = warningStatus and 0x01 != 0, + deviceBatteryLow = warningStatus and 0x02 != 0, + sensorTypeIncorrectForDevice = warningStatus and 0x04 != 0, + sensorMalfunction = warningStatus and 0x08 != 0, + deviceSpecificAlert = warningStatus and 0x10 != 0, + generalDeviceFault = warningStatus and 0x20 != 0, + timeSyncRequired = calibrationTempStatus and 0x01 != 0, + calibrationNotAllowed = calibrationTempStatus and 0x02 != 0, + calibrationRecommended = calibrationTempStatus and 0x04 != 0, + calibrationRequired = calibrationTempStatus and 0x08 != 0, + sensorTemperatureTooHigh = calibrationTempStatus and 0x10 != 0, + sensorTemperatureTooLow = calibrationTempStatus and 0x20 != 0, + sensorResultLowerThenPatientLowLevel = sensorStatus and 0x01 != 0, + sensorResultHigherThenPatientHighLevel = sensorStatus and 0x02 != 0, + sensorResultLowerThenHypoLevel = sensorStatus and 0x04 != 0, + sensorResultHigherThenHyperLevel = sensorStatus and 0x08 != 0, + sensorRateOfDecreaseExceeded = sensorStatus and 0x10 != 0, + sensorRateOfIncreaseExceeded = sensorStatus and 0x20 != 0, + sensorResultLowerThenDeviceCanProcess = sensorStatus and 0x40 != 0, + sensorResultHigherThenDeviceCanProcess = sensorStatus and 0x80 != 0 + ) +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/CRC16.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/CRC16.kt new file mode 100644 index 00000000..af00091e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/CRC16.kt @@ -0,0 +1,175 @@ +package no.nordicsemi.android.toolbox.profile.parser.common + +/** + * CRC-16 class is a helper that calculates different types of CRC. + * Catalogue of CRC-16 algorithms: + * [http://reveng.sourceforge.net/crc-catalogue/16.htm](http://reveng.sourceforge.net/crc-catalogue/16.htm) + * + * Testing is based on 'check' from the link above and + * [https://www.lammertbies.nl/comm/info/crc-calculation.html](https://www.lammertbies.nl/comm/info/crc-calculation.html). + */ +object CRC16 { + /** + * Calculates CRC CCITT (Kermit) over given range of bytes from the block of data. + * It is using the 0x1021 polynomial and 0x0000 initial value. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.kermit + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16 CCITT (Kermit). + */ + fun CCITT_Kermit(data: ByteArray, offset: Int, length: Int): Int { + return CRC(0x1021, 0x0000, data, offset, length, true, true, 0x0000) + } + + /** + * Calculates CRC CCITT-FALSE over given range of bytes from the block of data. + * It is using the 0x1021 polynomial and 0xFFFF initial value. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.crc-16-ccitt-false + * See: http://srecord.sourceforge.net/crc16-ccitt.html + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16 CCITT-FALSE. + */ + fun CCITT_FALSE(data: ByteArray, offset: Int, length: Int): Int { +// Other implementation of the same algorithm: +// int crc = 0xFFFF; +// +// for (int i = offset; i < offset + length && i < data.length; ++i) { +// crc = (((crc & 0xFFFF) >> 8) | (crc << 8)); +// crc ^= data[i]; +// crc ^= (crc & 0xFF) >> 4; +// crc ^= (crc << 8) << 4; +// crc ^= ((crc & 0xFF) << 4) << 1; +// } + return CRC(0x1021, 0xFFFF, data, offset, length, false, false, 0x0000) + } + + /** + * Calculates CRC MCRF4XX over given range of bytes from the block of data. + * It is using the 0x1021 polynomial and 0xFFFF initial value. + * + * + * This method is used in Bluetooth LE CGMS service E2E-CRC calculation. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.crc-16-mcrf4xx

+ * See: http://ww1.microchip.com/downloads/en/AppNotes/00752a.pdf

+ * See: https://www.bluetooth.com/specifications/gatt -> CGMS (1.0.1) + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16 MCRF4XX. + */ + fun MCRF4XX(data: ByteArray, offset: Int, length: Int): Int { + return CRC(0x1021, 0xFFFF, data, offset, length, true, true, 0x0000) + } + + /** + * Calculates CRC AUG-CCITT over given range of bytes from the block of data. + * It is using the 0x1021 polynomial and 0x1D0F initial value. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.crc-16-aug-ccitt + * See: http://srecord.sourceforge.net/crc16-ccitt.html + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16 AUG-CCITT. + */ + fun AUG_CCITT(data: ByteArray, offset: Int, length: Int): Int { + return CRC(0x1021, 0x1D0F, data, offset, length, false, false, 0x0000) + } + + /** + * Calculates CRC-16 ARC over given range of bytes from the block of data. + * It is using the 0x8005 polynomial and 0x0000 initial value. + * + * + * Input data and output CRC are reversed. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.crc-16-arc + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16. + */ + fun ARC(data: ByteArray, offset: Int, length: Int): Int { + return CRC(0x8005, 0x0000, data, offset, length, true, true, 0x0000) + } + + /** + * Calculates CRC-16 MAXIM over given range of bytes from the block of data. + * It is using the 0x8005 polynomial and 0x0000 initial value and XORs output with 0xFFFF. + * + * + * Input data and output CRC are reversed. + * + * + * See: http://reveng.sourceforge.net/crc-catalogue/16.htm#crc.cat.crc-16-maxim + * + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @return the CRC-16 MAXIM. + */ + fun MAXIM(data: ByteArray, offset: Int, length: Int): Int { + return CRC(0x8005, 0x0000, data, offset, length, true, true, 0xFFFF) + } + + /** + * Calculates the CRC over given range of bytes from the block of data with given polynomial and initial value. + * This method may also reverse input bytes and reverse output CRC. + * + * See: http://www.zorc.breitbandkatze.de/crc.html + * + * @param poly Polynomial used to calculate the CRC16. + * @param init Initial value to feed the buffer. + * @param data The input data block for computation. + * @param offset Offset from where the range starts. + * @param length Length of the range in bytes. + * @param refin True if the input data should be reversed. + * @param refout True if the output data should be reversed. + * @return CRC calculated with given parameters. + */ + fun CRC( + poly: Int, + init: Int, + data: ByteArray, + offset: Int, + length: Int, + refin: Boolean, + refout: Boolean, + xorout: Int + ): Int { + var crc = init + var i = offset + while (i < offset + length && i < data.size) { + val b = data[i] + for (j in 0..7) { + val k = if (refin) 7 - j else j + val bit = (b.toInt() shr (7 - k) and 1) == 1 + val c15 = (crc shr 15 and 1) == 1 + crc = crc shl 1 + if (c15 xor bit) crc = crc xor poly + } + ++i + } + return if (refout) { + (Integer.reverse(crc) ushr 16) xor xorout + } else { + (crc xor xorout) and 0xFFFF + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/WorkingMode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/WorkingMode.kt new file mode 100644 index 00000000..a8794f9e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/common/WorkingMode.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.toolbox.profile.parser.common + +enum class WorkingMode { + ALL, LAST, FIRST; +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCData.kt new file mode 100644 index 00000000..46310273 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCData.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +data class CSCData( + val scanDevices: Boolean = false, + val speed: Float = 0f, + val cadence: Float = 0f, + val distance: Float = 0f, + val totalDistance: Float = 0f, + val gearRatio: Float = 0f, + val wheelSize: WheelSize = WheelSizes.default +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParser.kt new file mode 100644 index 00000000..feabc741 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParser.kt @@ -0,0 +1,198 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import kotlin.experimental.and + +object CSCDataParser { + + internal var previousData: CSCDataSnapshot = CSCDataSnapshot() + + internal var wheelRevolutions: Long = -1 + internal var wheelEventTime: Int = -1 + internal var crankRevolutions: Long = -1 + internal var crankEventTime: Int = -1 + + fun parse( + data: ByteArray, + wheelSize: WheelSize = WheelSizes.default, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): CSCData? { + if (data.isEmpty()) return null + + // Decode the new data + var offset = 0 + val flags = data[offset].also { offset += 1 } + + val wheelRevPresent = (flags and 0x01).toInt() != 0 + val crankRevPreset = (flags and 0x02).toInt() != 0 + + if (data.size < 1 + (if (wheelRevPresent) 6 else 0) + (if (crankRevPreset) 4 else 0)) { + return null + } + + if (wheelRevPresent) { + wheelRevolutions = + data.getInt(offset, IntFormat.UINT32, byteOrder).toLong() and 0xFFFFFFFFL + offset += 4 + wheelEventTime = data.getInt(offset, IntFormat.UINT16, byteOrder) // 1/1024 s + offset += 2 + } + + if (crankRevPreset) { + crankRevolutions = data.getInt(offset, IntFormat.UINT16, byteOrder).toLong() + offset += 2 + crankEventTime = data.getInt(offset, IntFormat.UINT16, byteOrder) + offset += 2 + } + + if (!wheelRevPresent && !crankRevPreset) { + // No data to process, return null + return null + } + + val wheelCircumference = wheelSize.value.toFloat() + + return CSCData( + totalDistance = getTotalDistance(wheelSize.value.toFloat()), + distance = getDistance(wheelCircumference, previousData), + speed = getSpeed(wheelCircumference, previousData), + wheelSize = wheelSize, + cadence = getCrankCadence(previousData), + gearRatio = getGearRatio(previousData), + ).also { + previousData = CSCDataSnapshot( + wheelRevolutions, + wheelEventTime, + crankRevolutions, + crankEventTime + ) + } + } + + private fun getTotalDistance(wheelCircumference: Float): Float { + if (wheelRevolutions < 0) { + return 0.0f + } + return wheelRevolutions.toFloat() * wheelCircumference / 1000.0f // [m] + } + + /** + * Returns the distance traveled since the given response was received. + * + * @param wheelCircumference the wheel circumference in millimeters. + * @param previous a previous response. + * @return distance traveled since the previous response, in meters. + */ + private fun getDistance( + wheelCircumference: Float, + previous: CSCDataSnapshot + ): Float { + if (wheelRevolutions < 0 || previous.wheelRevolutions < 0) return 0f + + val difference = wheelRevolutions - previous.wheelRevolutions + if (difference < 0) return 0f + + return difference.toFloat() * wheelCircumference / 1000.0f + } + + /** + * Returns the average speed since the previous response was received. + * + * @param wheelCircumference the wheel circumference in millimeters. + * @param previous a previous response. + * @return speed in meters per second. + */ + private fun getSpeed( + wheelCircumference: Float, + previous: CSCDataSnapshot + ): Float { + // Check for valid input + if (wheelEventTime < 0 || previous.wheelEventTime < 0 || + wheelRevolutions < 0 || previous.wheelRevolutions < 0 + ) { + return 0f + } + + val timeDifference: Float = if (wheelEventTime < previous.wheelEventTime) { + (65536 + wheelEventTime - previous.wheelEventTime) / 1024.0f + } else { + (wheelEventTime - previous.wheelEventTime) / 1024.0f + } + + if (timeDifference == 0f) return 0f + + val distance = getDistance(wheelCircumference, previous) + return distance / timeDifference + } + + + /** + * Returns average wheel cadence since the previous message was received. + * + * @param previous a previous response. + * @return wheel cadence in revolutions per minute. + */ + private fun getWheelCadence(previous: CSCDataSnapshot): Float { + if (previous.crankEventTime < 0 || crankEventTime < 0 || crankRevolutions < 0) { + return 0f + } + return previous.wheelRevolutions.let { previousWheelRevolutions -> + previous.wheelEventTime.let { + val timeDifference: Float = if (wheelEventTime < it) { + (65536 + wheelEventTime - it) / 1024.0f + } else (wheelEventTime - it) / 1024.0f // [s] + + if (timeDifference <= 0f) { + 0.0f + } else { + val revDiff = wheelRevolutions - previousWheelRevolutions + if (revDiff < 0) 0.0f else revDiff * 60.0f / timeDifference + } + } + } + } + + /** + * Returns average crank cadence since the previous message was received. + * + * @param previous a previous response. + * @return crank cadence in revolutions per minute. + */ + private fun getCrankCadence(previous: CSCDataSnapshot): Float { + val newRevs = crankRevolutions + val newTime = crankEventTime + val oldRevs = previous.crankRevolutions + val oldTime = previous.crankEventTime + + if (newRevs < 0 || oldRevs < 0 || newTime < 0 || oldTime < 0) return 0f + + val revDiff = newRevs - oldRevs + if (revDiff <= 0) return 0f + + val timeDiff: Float = if (newTime < oldTime) { + (65536 + newTime - oldTime) / 1024.0f + } else { + (newTime - oldTime) / 1024.0f + } + + if (timeDiff <= 0f) return 0f + + return revDiff * 60.0f / timeDiff + } + + /** + * Returns the gear ratio (equal to wheel cadence / crank cadence). + * @param previous a previous response. + * @return gear ratio. + */ + private fun getGearRatio(previous: CSCDataSnapshot): Float { + val crankCadence = getCrankCadence(previous) + return if (crankCadence > 0) { + getWheelCadence(previous) / crankCadence + } else { + 0.0f + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataSnapshot.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataSnapshot.kt new file mode 100644 index 00000000..94f4f873 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataSnapshot.kt @@ -0,0 +1,8 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +internal data class CSCDataSnapshot( + var wheelRevolutions: Long = -1, + var wheelEventTime: Int = -1, + var crankRevolutions: Long = -1, + var crankEventTime: Int = -1 +) \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/SpeedUnit.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/SpeedUnit.kt new file mode 100644 index 00000000..03e1dc7d --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/SpeedUnit.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +enum class SpeedUnit(val displayName: String) { + M_S("Meter per second (m/s)"), + KM_H("Kilometer per hour (km/h)"), + MPH("Mile per hour (mph)") +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/WheelSize.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/WheelSize.kt new file mode 100644 index 00000000..0ffc54d8 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/csc/WheelSize.kt @@ -0,0 +1,64 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +data class WheelSize( + val value: Int, + val name: String +) + +object WheelSizes { + val data = listOf( + WheelSize(2340, "60-622"), + WheelSize(2284, "50-622"), + WheelSize(2268, "47-622"), + WheelSize(2224, "44-622"), + WheelSize(2265, "40-635"), + WheelSize(2224, "40-622"), + WheelSize(2180, "38-622"), + WheelSize(2205, "37-622"), + WheelSize(2168, "35-622"), + WheelSize(2199, "32-630"), + WheelSize(2174, "32-622"), + WheelSize(2155, "32-622"), + WheelSize(2149, "28-622"), + WheelSize(2146, "60-559"), + WheelSize(2136, "28-622"), + WheelSize(2146, "25-622"), + WheelSize(2105, "25-622"), + WheelSize(2133, "23-622"), + WheelSize(2114, "20-622"), + WheelSize(2102, "18-622"), + WheelSize(2169, "35-630"), + WheelSize(2161, "32-630"), + WheelSize(2155, "28-630"), + WheelSize(2133, "57-559"), + WheelSize(2114, "54-559"), + WheelSize(2105, "37-590"), + WheelSize(2097, "23-622"), + WheelSize(2089, "50-559"), + WheelSize(2086, "20-622"), + WheelSize(2114, "54-559"), + WheelSize(2070, "47-559"), + WheelSize(2068, "35-590"), + WheelSize(2105, "37-590"), + WheelSize(2055, "47-559"), + WheelSize(2089, "50-559"), + WheelSize(2051, "44-559"), + WheelSize(2026, "40-559"), + WheelSize(1973, "23-571"), + WheelSize(1954, "20-571"), + WheelSize(1953, "32-559"), + WheelSize(1952, "25-571"), + WheelSize(1948, "34-540"), + WheelSize(1910, "50-507"), + WheelSize(1907, "47-507"), + WheelSize(1618, "28-451"), + WheelSize(1593, "50-406"), + WheelSize(1590, "47-406"), + WheelSize(1325, "28-369"), + WheelSize(1282, "35-349"), + WheelSize(1272, "47-305") + ) + val default = data.first() + + fun getWheelSizeByName(name: String) = data.find { it.name == name } ?: default +} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/date/DateTimeParser.kt similarity index 51% rename from profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt rename to profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/date/DateTimeParser.kt index f423ebaa..26e44437 100644 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSContentView.kt +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/date/DateTimeParser.kt @@ -29,50 +29,49 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.rscs.view +package no.nordicsemi.android.toolbox.profile.parser.date -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.rscs.R -import no.nordicsemi.android.rscs.data.RSCSServiceData -import no.nordicsemi.android.ui.view.BatteryLevelView +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import java.util.Calendar -@Composable -internal fun RSCSContentView(state: RSCSServiceData, onEvent: (RSCScreenViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SensorsReadingView(state = state) +internal object DateTimeParser { - Spacer(modifier = Modifier.height(16.dp)) + fun parse(byte: ByteArray, offset: Int): Calendar? { + if (byte.size < offset + 7) return null - state.batteryLevel?.let { - BatteryLevelView(it) + val calendar = Calendar.getInstance() + val year = byte.getInt(offset, IntFormat.UINT16, ByteOrder.LITTLE_ENDIAN) + val month = byte.getInt(offset + 2, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) + val day = byte.getInt(offset + 3, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) + val hourOfDay = byte.getInt(offset + 4, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) + val minute = byte.getInt(offset + 5, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) + val second = byte.getInt(offset + 6, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) - Spacer(modifier = Modifier.height(16.dp)) + if (year > 0) { + calendar[Calendar.YEAR] = year + } else { + calendar.clear(Calendar.YEAR) } - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) + if (month > 0) { + calendar[Calendar.MONTH] = month - 1 + } else { + calendar.clear(Calendar.MONTH) } + + if (day > 0) { + calendar[Calendar.DATE] = day + } else { + calendar.clear(Calendar.DATE) + } + + calendar[Calendar.HOUR_OF_DAY] = hourOfDay + calendar[Calendar.MINUTE] = minute + calendar[Calendar.SECOND] = second + calendar[Calendar.MILLISECOND] = 0 + + return calendar } } - -@Preview -@Composable -private fun RSCSContentViewPreview() { - RSCSContentView(RSCSServiceData()) { } -} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/Mapper.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/Mapper.kt new file mode 100644 index 00000000..78b50d49 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/Mapper.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointMode +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode + +fun ControlPointMode.toDistanceMode(): DistanceMode { + return when (this) { + ControlPointMode.RTT -> DistanceMode.RTT + ControlPointMode.MCPD -> DistanceMode.MCPD + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/PeripheralBluetoothAddress.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/PeripheralBluetoothAddress.kt new file mode 100644 index 00000000..fb87e20d --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/PeripheralBluetoothAddress.kt @@ -0,0 +1,25 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder + +data class PeripheralBluetoothAddress( + val type: AddressType, + val address: String +) { + + companion object { + val TEST = PeripheralBluetoothAddress(AddressType.PUBLIC, "AA:BB:CC:DD:EE:FF") + } +} + +enum class AddressType(val id: Int) { + PUBLIC(0), + RANDOM(1), + PUBLIC_ID(2), + RANDOM_ID(3); + + companion object { + fun create(id: Int): AddressType { + return entries.find { it.id == id } + ?: throw IllegalArgumentException("Cannot find AddressType for specified id: $id") + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementData.kt new file mode 100644 index 00000000..db880273 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementData.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.QualityIndicator + +/** + * Azimuth represents the horizontal direction of a signal source relative to a + * receiver or reference point. Represents the horizontal direction (angle on a flat plane) + * Example: AzimuthMeasurementData( + * flags=0, + * quality=GOOD, + * address=PeripheralBluetoothAddress(type=RANDOM, address=aa:bb:cc:dd:ee:ff), + * azimuth=156 + * ) here azimuth = 156° indicates that the detected device is 156° clockwise from the reference direction (e.g., true north or some defined zero-point). + * The quality=GOOD suggests the measurement is reliable. Azimuthal data in devices like yours is calculated using signal phase differences and other electronic measurements. + */ +data class AzimuthMeasurementData( + val flags: Byte = Byte.MAX_VALUE, + val quality: QualityIndicator = QualityIndicator.GOOD, + val address: PeripheralBluetoothAddress, + val azimuth: Int = 0 +) \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementDataParser.kt new file mode 100644 index 00000000..b476ea9e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/azimuthal/AzimuthalMeasurementDataParser.kt @@ -0,0 +1,43 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.AddressType +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.QualityIndicator +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +class AzimuthalMeasurementDataParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): AzimuthMeasurementData? { + if (data.size < 10) return null + + var offset = 0 + val flags = data[offset].also { offset++ } + val qualityIndicator = data.getInt(offset++, IntFormat.UINT8) + + val address = StringBuilder().apply { + for (i in 0..5) { + data.getInt(offset++, IntFormat.UINT8).let { + insert(0, Integer.toHexString(it)) + if (i != 5) insert(0, ":") + } + } + }.toString() + + val addressType = data.getInt(offset++, IntFormat.UINT8) + + val azimuth = data.getInt(offset, IntFormat.UINT16, byteOrder).also { offset += 2 } + + return AzimuthMeasurementData( + flags, + QualityIndicator.create(qualityIndicator), + PeripheralBluetoothAddress(AddressType.create(addressType), address), + azimuth + ) + + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointDataParser.kt new file mode 100644 index 00000000..6d354dac --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointDataParser.kt @@ -0,0 +1,68 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt + +class ControlPointDataParser { + + fun parse(data: ByteArray): ControlPointResult? { + + if (data.isEmpty()) return null + + var offset = 0 + val responseCode = data.getInt(offset++, IntFormat.UINT8) + + if (responseCode != 0x10) return null + + data.getInt(offset, IntFormat.UINT8) + .let { ControlPointRequestCode.create(it) } + ?.let { controlPointRequestCode -> + val result = data.getInt(offset, IntFormat.UINT8) + .let { ControlPointResponseCodeValue.create(it) } + if (result == null) { + return@let + } + return when (controlPointRequestCode) { + ControlPointRequestCode.CHANGE_MODE -> onChangeModeResult( + result, + data.getInt(offset, IntFormat.UINT8) + ) + ControlPointRequestCode.CHECK_MODE -> { + onCheckModeResult(result, data.getInt(offset, IntFormat.UINT8)) + } + } + } + return null + } + + private fun onChangeModeResult( + opCode: ControlPointResponseCodeValue, + value: Int + ): ControlPointResult { + return when (opCode) { + ControlPointResponseCodeValue.SUCCESS -> ControlPointChangeModeSuccess( + ControlPointMode.create(value) ?: return ControlPointChangeModeError + ) + ControlPointResponseCodeValue.OP_CODE_NOT_SUPPORTED, + ControlPointResponseCodeValue.INVALID, + ControlPointResponseCodeValue.FAILED -> ControlPointChangeModeError + } + } + + private fun onCheckModeResult( + opCode: ControlPointResponseCodeValue, + value: Int + ): ControlPointResult { + return when (opCode) { + ControlPointResponseCodeValue.SUCCESS -> { + ControlPointMode.create(value)?.let { + ControlPointCheckModeSuccess(it) + } ?: ControlPointCheckModeError + } + + ControlPointResponseCodeValue.OP_CODE_NOT_SUPPORTED, + ControlPointResponseCodeValue.INVALID, + ControlPointResponseCodeValue.FAILED -> ControlPointCheckModeError + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointMode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointMode.kt new file mode 100644 index 00000000..59894763 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointMode.kt @@ -0,0 +1,36 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint + +enum class ControlPointMode(val value: Int) { + RTT(0x00), + MCPD(0x01); + + companion object { + fun create(value: Int): ControlPointMode? { + return entries.find { it.value == value } + } + } +} + +enum class ControlPointRequestCode(val value: Int) { + CHANGE_MODE(0x01), + CHECK_MODE(0x0A); + + companion object { + fun create(value: Int): ControlPointRequestCode? { + return entries.find { it.value == value } + } + } +} + +enum class ControlPointResponseCodeValue(val value: Int) { + SUCCESS(0x01), + OP_CODE_NOT_SUPPORTED(0x02), + INVALID(0x03), + FAILED(0x04); + + companion object { + fun create(value: Int): ControlPointResponseCodeValue? { + return entries.find { it.value == value } + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointResult.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointResult.kt new file mode 100644 index 00000000..ff822b15 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/controlPoint/ControlPointResult.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint + +sealed class ControlPointResult + +data class ControlPointCheckModeSuccess( + val mode: ControlPointMode +) : ControlPointResult() + +data object ControlPointCheckModeError : ControlPointResult() + +data class ControlPointChangeModeSuccess( + val mode: ControlPointMode +) : ControlPointResult() + +data object ControlPointChangeModeError : ControlPointResult() diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFData.kt new file mode 100644 index 00000000..6b9164f5 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFData.kt @@ -0,0 +1,6 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf + +data class DDFData( + val isMcpdAvailable: Boolean, + val isRttAvailable: Boolean +) \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFDataParser.kt new file mode 100644 index 00000000..db8e7b90 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/ddf/DDFDataParser.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf + +import kotlin.experimental.and + +class DDFDataParser { + + fun parse(data: ByteArray): DDFData? { + if (data.isEmpty()) return null + + var offset = 0 + val flags = data[offset].also { offset++ } + + val isRTTPresent = flags and 0x01 > 0 + val isMCPDPresent = flags and 0x02 > 0 + + return DDFData(isMCPDPresent, isRTTPresent) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DirectionMode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DirectionMode.kt new file mode 100644 index 00000000..04bb3120 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DirectionMode.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance + +enum class DistanceMode { + MCPD, RTT +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementData.kt new file mode 100644 index 00000000..a255298c --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementData.kt @@ -0,0 +1,52 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress + +sealed interface DistanceMeasurementData { + val flags: Byte + val quality: QualityIndicator + val address: PeripheralBluetoothAddress +} + +data class McpdMeasurementData( + override val flags: Byte = Byte.MAX_VALUE, + override val quality: QualityIndicator = QualityIndicator.GOOD, + override val address: PeripheralBluetoothAddress, + val mcpd: MCPDEstimate = MCPDEstimate() +) : DistanceMeasurementData + +data class RttMeasurementData( + override val flags: Byte = Byte.MAX_VALUE, + override val quality: QualityIndicator = QualityIndicator.GOOD, + override val address: PeripheralBluetoothAddress, + val rtt: RTTEstimate = RTTEstimate() +) : DistanceMeasurementData + +data class MCPDEstimate( + val ifft: Int = 0, + val phaseSlope: Int = 0, + val rssi: Int = 0, + val best: Int = 0 +) { + + operator fun plus(value: Int): MCPDEstimate { + return MCPDEstimate( + ifft + value, + phaseSlope + value, + rssi + value, + best + value + ) + } +} + +data class RTTEstimate( + val value: Int = 0 +) { + operator fun inc(): RTTEstimate { + return RTTEstimate(value + 1) + } + + operator fun plus(value: Int): RTTEstimate { + return RTTEstimate(this.value + value) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementDataParser.kt new file mode 100644 index 00000000..feffa38d --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/DistanceMeasurementDataParser.kt @@ -0,0 +1,66 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.AddressType +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import kotlin.experimental.and + +class DistanceMeasurementDataParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): DistanceMeasurementData? { + if (data.size < 10) return null + + var offset = 0 + val flags = data[offset].also { offset++ } + val isRTTPresent = flags and 0x01 > 0 + val isMCPDPresent = flags and 0x02 > 0 + val qualityIndicator = data.getInt(offset++, IntFormat.UINT8) + + val address = StringBuilder().apply { + for (i in 0..5) { + data.getInt(offset++, IntFormat.UINT8).let { + insert(0, Integer.toHexString(it)) + if (i != 5) insert(0, ":") + } + } + }.toString() + + val addressType = data.getInt(offset++, IntFormat.UINT8) + + val rtt = if (isRTTPresent) { + RTTEstimate(data.getInt(offset, IntFormat.UINT16, byteOrder)).also { offset += 2 } + } else null + + val mcpd = if (isMCPDPresent) { + MCPDEstimate( + data.getInt(offset, IntFormat.UINT16, byteOrder).also { offset += 2 }, + data.getInt(offset, IntFormat.UINT16, byteOrder).also { offset += 2 }, + data.getInt(offset, IntFormat.UINT16, byteOrder).also { offset += 2 }, + data.getInt(offset, IntFormat.UINT16, byteOrder).also { offset += 2 }, + ) + } else null + + val result = if (isRTTPresent) { + RttMeasurementData( + flags, + QualityIndicator.create(qualityIndicator), + PeripheralBluetoothAddress(AddressType.create(addressType), address), + rtt!! + ) + } else { + McpdMeasurementData( + flags, + QualityIndicator.create(qualityIndicator), + PeripheralBluetoothAddress(AddressType.create(addressType), address), + mcpd!!, + ) + } + + return result + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/QualityIndicator.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/QualityIndicator.kt new file mode 100644 index 00000000..1cda7dc3 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/distance/QualityIndicator.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance + +enum class QualityIndicator(val id: Int) { + GOOD(0), + POOR(1), + NOT_FOR_USE(2), + NOT_SPECIFIED(3); + + companion object { + fun create(id: Int): QualityIndicator { + return entries.find { it.id == id } + ?: throw IllegalArgumentException("Cannot find QualityIndicator for specified id: $id") + } + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementData.kt new file mode 100644 index 00000000..da7704e5 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementData.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.QualityIndicator + +data class ElevationMeasurementData( + val flags: Byte = Byte.MAX_VALUE, + val quality: QualityIndicator = QualityIndicator.GOOD, + val address: PeripheralBluetoothAddress, + val elevation: Int = 0 +) \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementDataParser.kt new file mode 100644 index 00000000..b7f2ad1f --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/directionFinder/elevation/ElevationMeasurementDataParser.kt @@ -0,0 +1,39 @@ +package no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.AddressType +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.QualityIndicator +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt + +class ElevationMeasurementDataParser { + + fun parse(data: ByteArray): ElevationMeasurementData? { + if (data.size < 10) return null + + var offset = 0 + val flags = data[offset].also { offset++ } + + val qualityIndicator = data.getInt(offset++, IntFormat.UINT8) + + val address = StringBuilder().apply { + for (i in 0..5) { + data.getInt(offset++, IntFormat.UINT8).let { + insert(0, Integer.toHexString(it).padStart(2, '0')) + if (i != 5) insert(0, ":") + } + } + }.toString() + + val addressType = data.getInt(offset++, IntFormat.UINT8) + + val elevation = data.getInt(offset++, IntFormat.INT8) + + return ElevationMeasurementData( + flags, + QualityIndicator.create(qualityIndicator), + PeripheralBluetoothAddress(AddressType.create(addressType), address), + elevation + ) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/CGMSpecificOpsControlPoint.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/CGMSpecificOpsControlPoint.kt new file mode 100644 index 00000000..b670db65 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/CGMSpecificOpsControlPoint.kt @@ -0,0 +1,67 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import java.nio.ByteBuffer + +/* +* Copyright (c) 2018, Nordic Semiconductor +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* +* 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this +* software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +* USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +@Suppress("unused") +object CGMSpecificOpsControlPointDataParser { + + private const val OP_CODE_SET_COMMUNICATION_INTERVAL: Byte = 1 + private const val OP_CODE_GET_COMMUNICATION_INTERVAL: Byte = 2 + private const val OP_CODE_SET_CALIBRATION_VALUE: Byte = 4 + private const val OP_CODE_GET_CALIBRATION_VALUE: Byte = 5 + private const val OP_CODE_SET_PATIENT_HIGH_ALERT_LEVEL: Byte = 7 + private const val OP_CODE_GET_PATIENT_HIGH_ALERT_LEVEL: Byte = 8 + private const val OP_CODE_SET_PATIENT_LOW_ALERT_LEVEL: Byte = 10 + private const val OP_CODE_GET_PATIENT_LOW_ALERT_LEVEL: Byte = 11 + private const val OP_CODE_SET_HYPO_ALERT_LEVEL: Byte = 13 + private const val OP_CODE_GET_HYPO_ALERT_LEVEL: Byte = 14 + private const val OP_CODE_SET_HYPER_ALERT_LEVEL: Byte = 16 + private const val OP_CODE_GET_HYPER_ALERT_LEVEL: Byte = 17 + private const val OP_CODE_SET_RATE_OF_DECREASE_ALERT_LEVEL: Byte = 19 + private const val OP_CODE_GET_RATE_OF_DECREASE_ALERT_LEVEL: Byte = 20 + private const val OP_CODE_SET_RATE_OF_INCREASE_ALERT_LEVEL: Byte = 22 + private const val OP_CODE_GET_RATE_OF_INCREASE_ALERT_LEVEL: Byte = 23 + private const val OP_CODE_RESET_DEVICE_SPECIFIC_ERROR: Byte = 25 + private const val OP_CODE_START_SESSION: Byte = 26 + private const val OP_CODE_STOP_SESSION: Byte = 27 + + fun startSession(secure: Boolean): ByteArray { + return create( + OP_CODE_START_SESSION, + secure + ) + } + + private fun create(opCode: Byte, secure: Boolean): ByteArray { + val data = ByteArray(1 + if (secure) 2 else 0) + val buffer = ByteBuffer.wrap(data).put(opCode) + if (secure) { + val crc = CRC16.MCRF4XX(buffer.array(), 0, data.size).toShort() + buffer.putShort(crc) + } + return buffer.array() + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementContextParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementContextParser.kt new file mode 100644 index 00000000..e7cda6fc --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementContextParser.kt @@ -0,0 +1,123 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Carbohydrate +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Health +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Meal +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Medication +import no.nordicsemi.android.toolbox.profile.parser.gls.data.MedicationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Tester +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object GlucoseMeasurementContextParser { + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): GLSMeasurementContext? { + if (data.size < 3) return null + + var offset = 0 + + val flags: Int = data.getInt(offset++, IntFormat.UINT8) + val carbohydratePresent = flags and 0x01 != 0 + val mealPresent = flags and 0x02 != 0 + val testerHealthPresent = flags and 0x04 != 0 + val exercisePresent = flags and 0x08 != 0 + val medicationPresent = flags and 0x10 != 0 + val medicationUnitLiter = (flags and 0x20) > 0 + val hbA1cPresent = flags and 0x40 != 0 + val extendedFlagsPresent = flags and 0x80 != 0 + + if (data.size < (3 + (if (carbohydratePresent) 3 else 0) + (if (mealPresent) 1 else 0) + (if (testerHealthPresent) 1 else 0) + + (if (exercisePresent) 3 else 0) + (if (medicationPresent) 3 else 0) + (if (hbA1cPresent) 2 else 0) + + if (extendedFlagsPresent) 1 else 0) + ) { + return null + } + + val sequenceNumber: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + offset += 2 + + // Optional fields + if (extendedFlagsPresent) { + // ignore extended flags + offset += 1 + } + + var carbohydrate: Carbohydrate? = null + var carbohydrateAmount: Float? = null + + if (carbohydratePresent) { + val carbohydrateId: Int = data.getInt(offset, IntFormat.UINT8) + carbohydrate = Carbohydrate.create(carbohydrateId) + + carbohydrateAmount = + data.getFloat(offset + 1, FloatFormat.IEEE_11073_16_BIT, byteOrder) // in grams + offset += 3 + } + + var meal: Meal? = null + if (mealPresent) { + val mealId: Int = data.getInt(offset, IntFormat.UINT8) + meal = Meal.create(mealId) + offset += 1 + } + + var tester: Tester? = null + var health: Health? = null + if (testerHealthPresent) { + val testerAndHealth: Int = data.getInt(offset, IntFormat.UINT8) + tester = Tester.create((testerAndHealth and 0xF0) shr 4) + health = Health.create(testerAndHealth and 0x0F) + offset += 1 + } + + var exerciseDuration: Int? = null + var exerciseIntensity: Int? = null + if (exercisePresent) { + exerciseDuration = data.getInt(offset, IntFormat.UINT16, byteOrder) // in seconds + exerciseIntensity = data.getInt(offset + 2, IntFormat.UINT8) // in percentage + offset += 3 + } + + var medication: Medication? = + null + var medicationAmount: Float? = null + var medicationUnit: MedicationUnit? = null + if (medicationPresent) { + val medicationId: Int = data.getInt(offset, IntFormat.UINT8) + medication = Medication.create(medicationId) + medicationAmount = + data.getFloat(offset + 1, FloatFormat.IEEE_11073_16_BIT, byteOrder) // mg or ml + medicationUnit = + if (medicationUnitLiter) MedicationUnit.UNIT_LITER else MedicationUnit.UNIT_KG + offset += 3 + } + + var hbA1c: Float? = null + if (hbA1cPresent) { + hbA1c = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + // offset += 2; + } + + return GLSMeasurementContext( + sequenceNumber, + carbohydrate, + carbohydrateAmount, + meal, + tester, + health, + exerciseDuration, + exerciseIntensity, + medication, + medicationAmount, + medicationUnit, + hbA1c + ) + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParser.kt new file mode 100644 index 00000000..2c79826e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParser.kt @@ -0,0 +1,80 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ConcentrationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GlucoseStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordType +import no.nordicsemi.android.toolbox.profile.parser.gls.data.SampleLocation +import no.nordicsemi.android.toolbox.profile.parser.date.DateTimeParser +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import java.util.Calendar + +object GlucoseMeasurementParser { + + fun parse(data: ByteArray, byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): GLSRecord? { + if (data.size < 10) return null + + var offset = 0 + + val flags: Int = data.getInt(offset++, IntFormat.UINT8) + val timeOffsetPresent = flags and 0x01 != 0 + val glucoseDataPresent = flags and 0x02 != 0 + val unitMolL = flags and 0x04 != 0 + val sensorStatusAnnunciationPresent = flags and 0x08 != 0 + val contextInformationFollows = flags and 0x10 != 0 + + if (data.size < (10 + (if (timeOffsetPresent) 2 else 0) + (if (glucoseDataPresent) 3 else 0) + + if (sensorStatusAnnunciationPresent) 2 else 0) + ) { + return null + } + + // Required fields + val sequenceNumber: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + offset += 2 + val baseTime: Calendar = DateTimeParser.parse(data, 3) ?: return null + offset += 7 + + // Optional fields + if (timeOffsetPresent) { + val timeOffset: Int = data.getInt(offset, IntFormat.INT16, byteOrder) + offset += 2 + baseTime.add(Calendar.MINUTE, timeOffset) + } + + var glucoseConcentration: Float? = null + var unit: ConcentrationUnit? = null + var type: Int? = null + var sampleLocation: Int? = null + if (glucoseDataPresent) { + glucoseConcentration = data.getFloat(offset, FloatFormat.IEEE_11073_16_BIT, byteOrder) + val typeAndSampleLocation: Int = data.getInt(offset + 2, IntFormat.UINT8, byteOrder) + offset += 3 + type = typeAndSampleLocation and 0x0F + sampleLocation = typeAndSampleLocation shr 4 + unit = if (unitMolL) ConcentrationUnit.UNIT_MOLPL else ConcentrationUnit.UNIT_KGPL + } + + var status: GlucoseStatus? = null + if (sensorStatusAnnunciationPresent) { + val value: Int = data.getInt(offset, IntFormat.UINT16, byteOrder) + // offset += 2; + status = GlucoseStatus(value) + } + + return GLSRecord( + sequenceNumber, + baseTime /* with offset */, + glucoseConcentration, + unit, + RecordType.createOrNull(type), + status, + SampleLocation.createOrNull(sampleLocation), + contextInformationFollows + ) + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointInputParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointInputParser.kt new file mode 100644 index 00000000..d7dd38a3 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointInputParser.kt @@ -0,0 +1,257 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import androidx.annotation.IntRange +import java.nio.ByteBuffer + +object RecordAccessControlPointInputParser { + private const val OP_CODE_REPORT_STORED_RECORDS: Byte = 1 + private const val OP_CODE_DELETE_STORED_RECORDS: Byte = 2 + private const val OP_CODE_ABORT_OPERATION: Byte = 3 + private const val OP_CODE_REPORT_NUMBER_OF_RECORDS: Byte = 4 + private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE: Byte = 5 + private const val OP_CODE_RESPONSE_CODE: Byte = 6 + private const val OPERATOR_NULL: Byte = 0 + private const val OPERATOR_ALL_RECORDS: Byte = 1 + private const val OPERATOR_LESS_THEN_OR_EQUAL: Byte = 2 + private const val OPERATOR_GREATER_THEN_OR_EQUAL: Byte = 3 + private const val OPERATOR_WITHING_RANGE: Byte = 4 + private const val OPERATOR_FIRST_RECORD: Byte = 5 + private const val OPERATOR_LAST_RECORD: Byte = 6 + + fun reportAllStoredRecords(): ByteArray { + return create(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_ALL_RECORDS) + } + + fun reportFirstStoredRecord(): ByteArray { + return create(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_FIRST_RECORD) + } + + fun reportLastStoredRecord(): ByteArray { + return create(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_LAST_RECORD) + } + + fun reportStoredRecordsLessThenOrEqualTo( + filter: FilterType, + parameter: Short + ): ByteArray { + return create(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_LESS_THEN_OR_EQUAL, filter, parameter) + } + + fun reportStoredRecordsGreaterThenOrEqualTo( + filter: FilterType, parameter: Short + ): ByteArray { + return create( + OP_CODE_REPORT_STORED_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + filter, + parameter + ) + } + + fun reportStoredRecordsFromRange( + filter: FilterType, + start: Short, end: Short + ): ByteArray { + return create(OP_CODE_REPORT_STORED_RECORDS, OPERATOR_WITHING_RANGE, filter, start, end) + } + + fun reportStoredRecordsLessThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_REPORT_STORED_RECORDS, + OPERATOR_LESS_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun reportStoredRecordsGreaterThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_REPORT_STORED_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun reportStoredRecordsFromRange( + @IntRange(from = 0) startSequenceNumber: Short, + @IntRange(from = 0) endSequenceNumber: Short + ): ByteArray { + return create( + OP_CODE_REPORT_STORED_RECORDS, + OPERATOR_WITHING_RANGE, + FilterType.SEQUENCE_NUMBER, + startSequenceNumber, + endSequenceNumber + ) + } + + fun deleteAllStoredRecords(): ByteArray { + return create(OP_CODE_DELETE_STORED_RECORDS, OPERATOR_ALL_RECORDS) + } + + fun deleteFirstStoredRecord(): ByteArray { + return create(OP_CODE_DELETE_STORED_RECORDS, OPERATOR_FIRST_RECORD) + } + + fun deleteLastStoredRecord(): ByteArray { + return create(OP_CODE_DELETE_STORED_RECORDS, OPERATOR_LAST_RECORD) + } + + fun deleteStoredRecordsLessThenOrEqualTo( + filter: FilterType, + parameter: Short + ): ByteArray { + return create(OP_CODE_DELETE_STORED_RECORDS, OPERATOR_LESS_THEN_OR_EQUAL, filter, parameter) + } + + fun deleteStoredRecordsGreaterThenOrEqualTo( + filter: FilterType, + parameter: Short + ): ByteArray { + return create( + OP_CODE_DELETE_STORED_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + filter, + parameter + ) + } + + fun deleteStoredRecordsFromRange( + filter: FilterType, + start: Short, + end: Short + ): ByteArray { + return create(OP_CODE_DELETE_STORED_RECORDS, OPERATOR_WITHING_RANGE, filter, start, end) + } + + fun deleteStoredRecordsLessThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_DELETE_STORED_RECORDS, + OPERATOR_LESS_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun deleteStoredRecordsGreaterThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_DELETE_STORED_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun deleteStoredRecordsFromRange( + @IntRange(from = 0) startSequenceNumber: Short, + @IntRange(from = 0) endSequenceNumber: Short + ): ByteArray { + return create( + OP_CODE_DELETE_STORED_RECORDS, OPERATOR_WITHING_RANGE, + FilterType.SEQUENCE_NUMBER, + startSequenceNumber, endSequenceNumber + ) + } + + fun reportNumberOfAllStoredRecords(): ByteArray { + return create(OP_CODE_REPORT_NUMBER_OF_RECORDS, OPERATOR_ALL_RECORDS) + } + + fun reportNumberOfStoredRecordsLessThenOrEqualTo( + filter: FilterType, + parameter: Short + ): ByteArray { + return create( + OP_CODE_REPORT_NUMBER_OF_RECORDS, + OPERATOR_LESS_THEN_OR_EQUAL, + filter, + parameter + ) + } + + fun reportNumberOfStoredRecordsGreaterThenOrEqualTo( + filter: FilterType, + parameter: Short + ): ByteArray { + return create( + OP_CODE_REPORT_NUMBER_OF_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + filter, + parameter + ) + } + + fun reportNumberOfStoredRecordsFromRange( + filter: FilterType, + start: Short, + end: Short + ): ByteArray { + return create(OP_CODE_REPORT_NUMBER_OF_RECORDS, OPERATOR_WITHING_RANGE, filter, start, end) + } + + fun reportNumberOfStoredRecordsLessThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_REPORT_NUMBER_OF_RECORDS, + OPERATOR_LESS_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun reportNumberOfStoredRecordsGreaterThenOrEqualTo(@IntRange(from = 0) sequenceNumber: Short): ByteArray { + return create( + OP_CODE_REPORT_NUMBER_OF_RECORDS, + OPERATOR_GREATER_THEN_OR_EQUAL, + FilterType.SEQUENCE_NUMBER, + sequenceNumber + ) + } + + fun reportNumberOfStoredRecordsFromRange( + @IntRange(from = 0) startSequenceNumber: Short, + @IntRange(from = 0) endSequenceNumber: Short + ): ByteArray { + return create( + OP_CODE_REPORT_NUMBER_OF_RECORDS, + OPERATOR_WITHING_RANGE, + FilterType.SEQUENCE_NUMBER, + startSequenceNumber, + endSequenceNumber + ) + } + + fun abortOperation(): ByteArray { + return create(OP_CODE_ABORT_OPERATION, OPERATOR_NULL) + } + + private fun create(opCode: Byte, operator: Byte): ByteArray { + return byteArrayOf(opCode, operator) + } + + private fun create( + opCode: Byte, operator: Byte, + filter: FilterType, + vararg parameters: Short + ): ByteArray { + val data = ByteArray(2 + 1 + parameters.size * 2) + val buffer = ByteBuffer.wrap(data).put(opCode).put(operator) + + if (parameters.isNotEmpty()) { + buffer.put(filter.type).putShort(parameters[0]) + } + if (parameters.size == 2) { + buffer.putShort(parameters[1]) + } + return buffer.array() + } + + enum class FilterType(type: Int) { + TIME_OFFSET(0x01), + + /** Alias of [.TIME_OFFSET] */ + SEQUENCE_NUMBER(0x01), USER_FACING_TIME(0x02); + + val type: Byte = type.toByte() + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointParser.kt new file mode 100644 index 00000000..6a7778f3 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/RecordAccessControlPointParser.kt @@ -0,0 +1,59 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import no.nordicsemi.android.toolbox.profile.parser.gls.data.NumberOfRecordsData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordAccessControlPointData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ResponseData +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object RecordAccessControlPointParser { + private const val OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE = 5 + private const val OP_CODE_RESPONSE_CODE = 6 + private const val OPERATOR_NULL = 0 + + fun parse( + data: ByteArray, + byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + ): RecordAccessControlPointData? { + + if (data.size < 3) return null + + val opCode: Int = data.getInt(0, IntFormat.UINT8) + if (opCode != OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE && opCode != OP_CODE_RESPONSE_CODE) { + return null + } + + val operator: Int = data.getInt(1, IntFormat.UINT8) + if (operator != OPERATOR_NULL) return null + + when (opCode) { + OP_CODE_NUMBER_OF_STORED_RECORDS_RESPONSE -> { + // Field size is defined per service + val numberOfRecords: Int = when (data.size - 2) { + 1 -> data.getInt(2, IntFormat.UINT8) + 2 -> data.getInt(2, IntFormat.UINT16, byteOrder) + 4 -> data.getInt(2, IntFormat.UINT32, byteOrder) + else -> { + // Other field sizes are not supported + return null + } + } + return NumberOfRecordsData(numberOfRecords) + } + + OP_CODE_RESPONSE_CODE -> { + if (data.size != 4) return null + + val requestCode: Int = data.getInt(2, IntFormat.UINT8) + val racpOpCode = RACPOpCode.create(requestCode) + val responseCode: Int = data.getInt(3, IntFormat.UINT8) + val racpResponseCode = RACPResponseCode.create(responseCode) + return ResponseData(racpOpCode, racpResponseCode) + } + } + return null + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Carbohydrate.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Carbohydrate.kt new file mode 100644 index 00000000..b99ab2ff --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Carbohydrate.kt @@ -0,0 +1,19 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class Carbohydrate(internal val value: Int) { + RESERVED(0), + BREAKFAST(1), + LUNCH(2), + DINNER(3), + SNACK(4), + DRINK(5), + SUPPER(6), + BRUNCH(7); + + companion object { + fun create(value: Int): Carbohydrate { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create Carbohydrate for value $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GLSRecord.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GLSRecord.kt new file mode 100644 index 00000000..eae19220 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GLSRecord.kt @@ -0,0 +1,91 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +import java.util.Calendar + +data class GLSRecord( + val sequenceNumber: Int, + val time: Calendar? = null, + val glucoseConcentration: Float? = null, + val unit: ConcentrationUnit? = null, + val type: RecordType? = null, + val status: GlucoseStatus? = null, + val sampleLocation: SampleLocation? = null, + val contextInformationFollows: Boolean +) + +enum class RecordType(val id: Int) { + CAPILLARY_WHOLE_BLOOD(1), + CAPILLARY_PLASMA(2), + VENOUS_WHOLE_BLOOD(3), + VENOUS_PLASMA(4), + ARTERIAL_WHOLE_BLOOD(5), + ARTERIAL_PLASMA(6), + UNDETERMINED_WHOLE_BLOOD(7), + UNDETERMINED_PLASMA(8), + INTERSTITIAL_FLUID(9), + CONTROL_SOLUTION(10); + + companion object { + fun create(value: Int): RecordType { + return entries.firstOrNull { it.id == value } + ?: throw IllegalArgumentException("Cannot find element for provided value.") + } + + fun createOrNull(value: Int?): RecordType? { + return entries.firstOrNull { it.id == value } + } + } +} + +data class GLSMeasurementContext( + val sequenceNumber: Int = 0, + val carbohydrate: Carbohydrate? = null, + val carbohydrateAmount: Float? = null, + val meal: Meal? = null, + val tester: Tester? = null, + val health: Health? = null, + val exerciseDuration: Int? = null, + val exerciseIntensity: Int? = null, + val medication: Medication?, + val medicationQuantity: Float? = null, + val medicationUnit: MedicationUnit? = null, + val HbA1c: Float? = null +) + +enum class ConcentrationUnit(val id: Int) { + UNIT_KGPL(1), + UNIT_MOLPL(0); + + companion object { + fun create(value: Int): ConcentrationUnit { + return entries.firstOrNull { it.id == value } + ?: throw IllegalArgumentException("Cannot find element for provided value.") + } + } +} + +enum class MedicationUnit(val id: Int) { + UNIT_KG(0), + UNIT_LITER(1); + + companion object { + fun create(value: Int): MedicationUnit { + return entries.firstOrNull { it.id == value } + ?: throw IllegalArgumentException("Cannot find element for provided value.") + } + } +} + +enum class SampleLocation(val id: Int) { + FINGER(1), + AST(2), + EARLOBE(3), + CONTROL_SOLUTION(4), + NOT_AVAILABLE(15); + + companion object { + fun createOrNull(value: Int?): SampleLocation? { + return entries.firstOrNull { it.id == value } + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseMeasurementUnit.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseMeasurementUnit.kt new file mode 100644 index 00000000..665c81b8 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseMeasurementUnit.kt @@ -0,0 +1,6 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class GlucoseMeasurementUnit { + UNIT_mol_L, + UNIT_kg_L +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseStatus.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseStatus.kt new file mode 100644 index 00000000..4f5f9e61 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/GlucoseStatus.kt @@ -0,0 +1,41 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +class GlucoseStatus( + val deviceBatteryLow: Boolean, + val sensorMalfunction: Boolean, + val sampleSizeInsufficient: Boolean, + val stripInsertionError: Boolean, + val stripTypeIncorrect: Boolean, + val sensorResultLowerThenDeviceCanProcess: Boolean, + val sensorResultHigherThenDeviceCanProcess: Boolean, + val sensorTemperatureTooHigh: Boolean, + val sensorTemperatureTooLow: Boolean, + val sensorReadInterrupted: Boolean, + val generalDeviceFault: Boolean, + val timeFault: Boolean +) { + + constructor(value: Int) : this( + deviceBatteryLow = value and 0x0001 != 0, + sensorMalfunction = value and 0x0002 != 0, + sampleSizeInsufficient = value and 0x0004 != 0, + stripInsertionError = value and 0x0008 != 0, + stripTypeIncorrect = value and 0x0010 != 0, + sensorResultLowerThenDeviceCanProcess = value and 0x0020 != 0, + sensorResultHigherThenDeviceCanProcess = value and 0x0040 != 0, + sensorTemperatureTooHigh = value and 0x0080 != 0, + sensorTemperatureTooLow = value and 0x0100 != 0, + sensorReadInterrupted = value and 0x0200 != 0, + generalDeviceFault = value and 0x0400 != 0, + timeFault = value and 0x0800 != 0 + ) + + override fun toString(): String { + return "GlucoseStatus(deviceBatteryLow=$deviceBatteryLow, sensorMalfunction=$sensorMalfunction, " + + "sampleSizeInsufficient=$sampleSizeInsufficient, stripInsertionError=$stripInsertionError, " + + "stripTypeIncorrect=$stripTypeIncorrect, sensorResultLowerThenDeviceCanProcess=$sensorResultLowerThenDeviceCanProcess, " + + "sensorResultHigherThenDeviceCanProcess=$sensorResultHigherThenDeviceCanProcess, sensorTemperatureTooHigh=$sensorTemperatureTooHigh, " + + "sensorTemperatureTooLow=$sensorTemperatureTooLow, sensorReadInterrupted=$sensorReadInterrupted, " + + "generalDeviceFault=$generalDeviceFault, timeFault=$timeFault)" + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Health.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Health.kt new file mode 100644 index 00000000..8200c39e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Health.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class Health(internal val value: Int) { + RESERVED(0), + MINOR_HEALTH_ISSUES(1), + MAJOR_HEALTH_ISSUES(2), + DURING_MENSES(3), + UNDER_STRESS(4), + NO_HEALTH_ISSUES(5), + NOT_AVAILABLE(15); + + companion object { + fun create(value: Int): Health { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create Health for value $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Meal.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Meal.kt new file mode 100644 index 00000000..abb94761 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Meal.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class Meal(internal val value: Int) { + RESERVED(0), + PREPRANDIAL(1), + POSTPRANDIAL(2), + FASTING(3), + CASUAL(4), + BEDTIME(5); + + companion object { + fun create(value: Int): Meal { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create Meal for value $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Medication.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Medication.kt new file mode 100644 index 00000000..db76022e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Medication.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class Medication(internal val value: Int) { + RESERVED(0), + RAPID_ACTING_INSULIN(1), + SHORT_ACTING_INSULIN(2), + INTERMEDIATE_ACTING_INSULIN(3), + LONG_ACTING_INSULIN(4), + PRE_MIXED_INSULIN(5); + + companion object { + fun create(value: Int): Medication { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create Medication for value $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RecordAccessControlPointData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RecordAccessControlPointData.kt new file mode 100644 index 00000000..054d754c --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RecordAccessControlPointData.kt @@ -0,0 +1,33 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode + +sealed interface RecordAccessControlPointData { + val operationCompleted: Boolean +} + +data class NumberOfRecordsData( + val numberOfRecords: Int +) : RecordAccessControlPointData { + + override val operationCompleted: Boolean = true +} + +data class ResponseData( + val requestCode: RACPOpCode, + val responseCode: RACPResponseCode +) : RecordAccessControlPointData { + + override val operationCompleted: Boolean = when (responseCode) { + RACPResponseCode.RACP_RESPONSE_SUCCESS, + RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> true + RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED, + RACPResponseCode.RACP_ERROR_INVALID_OPERATOR, + RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED, + RACPResponseCode.RACP_ERROR_INVALID_OPERAND, + RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL, + RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED, + RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> false + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RequestStatus.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RequestStatus.kt new file mode 100644 index 00000000..8ee7e242 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/RequestStatus.kt @@ -0,0 +1,5 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class RequestStatus { + IDLE, PENDING, SUCCESS, ABORTED, FAILED, NOT_SUPPORTED +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Tester.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Tester.kt new file mode 100644 index 00000000..f6d7428c --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/gls/data/Tester.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls.data + +enum class Tester(internal val value: Int) { + RESERVED(0), + SELF(1), + HEALTH_CARE_PROFESSIONAL(2), + LAB_TEST(3), + NOT_AVAILABLE(15); + + companion object { + fun create(value: Int): Tester { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create Tester for value $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/BodySensorLocationParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/BodySensorLocationParser.kt new file mode 100644 index 00000000..d1679860 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/BodySensorLocationParser.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.profile.parser.hrs + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object BodySensorLocationParser { + + fun parse(bytes: ByteArray): Int? { + if (bytes.isEmpty()) return null + + return bytes.getInt(0, IntFormat.UINT8, ByteOrder.LITTLE_ENDIAN) + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSData.kt new file mode 100644 index 00000000..4398bcd9 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSData.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.hrs + +/** + * Heart Rate data. + * @param heartRate the heart rate value. + * @param sensorContact true if the sensor contact is supported. + * @param energyExpanded the energy expanded in joules. + * @param rrIntervals the RR intervals in milliseconds. + */ +data class HRSData( + val heartRate: Int, + val sensorContact: Boolean, + val energyExpanded: Int?, + val rrIntervals: List +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParser.kt new file mode 100644 index 00000000..2d5e9c3a --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParser.kt @@ -0,0 +1,53 @@ +package no.nordicsemi.android.toolbox.profile.parser.hrs + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object HRSDataParser { + + fun parse(data: ByteArray, byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): HRSData? { + if (data.size < 2) return null + + var offset = 0 + val flag = data.getInt(offset, IntFormat.UINT8, byteOrder) + val heartRateType = if (flag and 0x01 == 0) IntFormat.UINT8 else IntFormat.UINT16 + + val sensorContactStatus = flag and 0x06 shr 1 + val sensorContactSupported = sensorContactStatus == 2 || sensorContactStatus == 3 + val sensorContactDetected = sensorContactStatus == 3 + val energyExpandedPresent = flag and 0x08 != 0 + val rrIntervalsPresent = flag and 0x10 != 0 + offset += 1 + + // Validate packet length + if (data.size < (1 + (heartRateType.length) + (if (energyExpandedPresent) 2 else 0) + if (rrIntervalsPresent) 2 else 0)) { + return null + } + // Prepare data + val sensorContact = if (sensorContactSupported) sensorContactDetected else false + + val heartRate: Int = data.getInt(offset, heartRateType, byteOrder) + offset += heartRateType.length + + var energyExpanded: Int? = null + if (energyExpandedPresent) { + energyExpanded = data.getInt(offset, IntFormat.UINT16, byteOrder) + offset += 2 + } + + val rrIntervals = if (rrIntervalsPresent) { + val count: Int = (data.size - offset) / 2 + val intervals: MutableList = ArrayList(count) + for (i in 0 until count) { + intervals.add(data.getInt(offset, IntFormat.UINT16, byteOrder)) + offset += 2 + } + intervals.toList() + } else { + emptyList() + } + + return HRSData(heartRate, sensorContact, energyExpanded, rrIntervals) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSData.kt new file mode 100644 index 00000000..fb538ae2 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSData.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2022, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.toolbox.profile.parser.hts + +import java.util.Calendar + +/** + * HTS data class that holds the temperature data. + * + * @param temperature The temperature value. + * @param unit The unit of the temperature value. + * @param timestamp The timestamp of the measurement. + * @param type The type of the measurement. + */ +data class HTSData( + val temperature: Float = 0f, + val unit: TemperatureUnitData = TemperatureUnitData.CELSIUS, + val timestamp: Calendar? = null, + val type: Int? = null +) + +/** + * The temperature unit data class. + */ +enum class TemperatureUnitData { + CELSIUS, FAHRENHEIT; + + companion object { + fun create(flag: Int): TemperatureUnitData? { + return when (flag) { + 0 -> CELSIUS + 1 -> FAHRENHEIT + else -> null + } + } + } +} + +/** + * HTS measurement type enum. + * + * @property value The integer value representing the measurement type. + */ +enum class HTSMeasurementType(val value: Int) { + FUTURE_USE(0), + ARMPIT(1), + BODY(2), + EAR_LOBE(3), + FINGER(4), + GASTROINTESTINAL(5), + MOUTH(6), + RECTUM(7), + TOE(8), + TYMPANIC(7); + + override fun toString(): String = + when (this) { + FUTURE_USE -> "Future Use" + ARMPIT -> "Armpit" + BODY -> "Body" + EAR_LOBE -> "Ear Lobe" + FINGER -> "Finger" + GASTROINTESTINAL -> "Gastrointestinal" + MOUTH -> "Mouth" + RECTUM -> "Rectum" + TOE -> "Toe" + TYMPANIC -> "Tympanic (Ear Drum)" + } + + companion object { + fun fromValue(value: Int): HTSMeasurementType? { + return entries.find { it.value == value } + } + } +} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/AlarmHandler.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParser.kt similarity index 52% rename from profile_prx/src/main/java/no/nordicsemi/android/prx/repository/AlarmHandler.kt rename to profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParser.kt index cb909807..354675b6 100644 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/AlarmHandler.kt +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParser.kt @@ -29,51 +29,49 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.prx.repository +package no.nordicsemi.android.toolbox.profile.parser.hts -import android.content.Context -import android.media.RingtoneManager -import android.os.Build -import dagger.hilt.android.qualifiers.ApplicationContext -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel -import javax.inject.Inject +import no.nordicsemi.android.toolbox.profile.parser.date.DateTimeParser +import no.nordicsemi.kotlin.data.FloatFormat +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getFloat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder +import java.util.Calendar -internal class AlarmHandler @Inject constructor( - @ApplicationContext - private val context: Context -) { +object HTSDataParser { - private val highLevelRingtone = RingtoneManager - .getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)) - ?.apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - volume = 1f - } + fun parse(byte: ByteArray, byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): HTSData? { + if (byte.size < 5) return null + + var offset = 0 + val flag: Int = byte.getInt(offset, IntFormat.UINT8, byteOrder) + + val unit: TemperatureUnitData = TemperatureUnitData.create(flag and 0x01) ?: return null + + val timestampPresent = flag and 0x02 != 0 + + val temperatureTypePresent = flag and 0x04 != 0 + offset += 1 + + if (byte.size < 5 + (if (timestampPresent) 7 else 0) + (if (temperatureTypePresent) 1 else 0)) { + return null } - private val mediumLevelRingtone = RingtoneManager - .getRingtone(context, RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE)) - ?.apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - volume = 0.5f - } + val temperature: Float = byte.getFloat(offset, FloatFormat.IEEE_11073_32_BIT, byteOrder) + offset += 4 + + var calendar: Calendar? = null + if (timestampPresent) { + calendar = DateTimeParser.parse(byte, offset) + offset += 7 } - fun playAlarm(alarmLevel: AlarmLevel) { - if (alarmLevel == AlarmLevel.NONE) { - pauseAlarm() - return + var type: Int? = null + if (temperatureTypePresent) { + type = byte.getInt(offset, IntFormat.UINT8, byteOrder) + offset += 1 } - val ringtone = when (alarmLevel) { - AlarmLevel.NONE -> null - AlarmLevel.MEDIUM -> mediumLevelRingtone - AlarmLevel.HIGH -> highLevelRingtone - } - ringtone?.play() - } - - fun pauseAlarm() { - highLevelRingtone?.takeIf { it.isPlaying }?.stop() - mediumLevelRingtone?.takeIf { it.isPlaying }?.stop() + return HTSData(temperature, unit, calendar, type) } } diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevel.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevel.kt new file mode 100644 index 00000000..c4658687 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevel.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.profile.parser.prx + +enum class AlarmLevel(internal val value: Byte) { + NONE(0x00), + MEDIUM(0x01), + HIGH(0x02); + + companion object { + internal fun create(value: Int): AlarmLevel { + return entries.firstOrNull { it.value.toInt() == value } + ?: throw IllegalArgumentException("Cannot find AlarmLevel for provided value: $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevelParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevelParser.kt new file mode 100644 index 00000000..b6fd2374 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlarmLevelParser.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.prx + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt + +object AlarmLevelParser { + + fun parse(data: ByteArray): AlarmLevel? { + if (data.size == 1) { + val level: Int = data.getInt(0, IntFormat.UINT8) + return AlarmLevel.create(level) + } + return null + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlertLevelInputParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlertLevelInputParser.kt new file mode 100644 index 00000000..cd34f02d --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/AlertLevelInputParser.kt @@ -0,0 +1,8 @@ +package no.nordicsemi.android.toolbox.profile.parser.prx + +object AlertLevelInputParser { + + fun parse(alarmLevel: AlarmLevel): Byte { + return alarmLevel.value + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/PRXData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/PRXData.kt new file mode 100644 index 00000000..710229be --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/prx/PRXData.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.parser.prx + +data class PRXData( + val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, + val isRemoteAlarm: Boolean = false, + val linkLossAlarmLevel: AlarmLevel = AlarmLevel.HIGH +) \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPOpCode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPOpCode.kt new file mode 100644 index 00000000..dd3edd5e --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPOpCode.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.parser.racp + +enum class RACPOpCode(internal val value: Int) { + RACP_OP_CODE_REPORT_STORED_RECORDS(1), + RACP_OP_CODE_DELETE_STORED_RECORDS(2), + RACP_OP_CODE_ABORT_OPERATION(3), + RACP_OP_CODE_REPORT_NUMBER_OF_RECORDS(4); + + companion object { + fun create(value: Int): RACPOpCode { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create RACP op code for value: $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPResponseCode.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPResponseCode.kt new file mode 100644 index 00000000..21c345ae --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/racp/RACPResponseCode.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.toolbox.profile.parser.racp + +enum class RACPResponseCode(internal val value: Int) { + RACP_RESPONSE_SUCCESS(1), + RACP_ERROR_OP_CODE_NOT_SUPPORTED(2), + RACP_ERROR_INVALID_OPERATOR(3), + RACP_ERROR_OPERATOR_NOT_SUPPORTED(4), + RACP_ERROR_INVALID_OPERAND(5), + RACP_ERROR_NO_RECORDS_FOUND(6), + RACP_ERROR_ABORT_UNSUCCESSFUL(7), + RACP_ERROR_PROCEDURE_NOT_COMPLETED(8), + RACP_ERROR_OPERAND_NOT_SUPPORTED(9); + + companion object { + fun create(value: Int): RACPResponseCode { + return entries.firstOrNull { it.value == value } + ?: throw IllegalArgumentException("Cannot create RACP response code for value: $value") + } + } +} \ No newline at end of file diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSData.kt new file mode 100644 index 00000000..e144adaa --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSData.kt @@ -0,0 +1,9 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +data class RSCSData( + val running: Boolean = false, + val instantaneousSpeed: Float = 1.0f, + val instantaneousCadence: Int = 0, + val strideLength: Int? = null, + val totalDistance: Long? = null +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParser.kt new file mode 100644 index 00000000..28abf6c9 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParser.kt @@ -0,0 +1,54 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object RSCSDataParser { + + fun parse(data: ByteArray, byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): RSCSData? { + if (data.size < 4) return null + + var offset = 0 + + // Flag + val flags: Int = data.getInt(offset, IntFormat.UINT8).also { offset += 1 } + val instantaneousStrideLengthPresent = flags and 0x01 != 0 + val totalDistancePresent = flags and 0x02 != 0 + val statusRunning = flags and 0x04 != 0 + + // Speed + val speed = data.getInt(offset, IntFormat.UINT16, byteOrder) + .toFloat() + .let { + it / 256f // [m/s] + }.also { offset += 2 } + + // Cadence + val cadence: Int = data.getInt(offset, IntFormat.UINT8) + .also { offset += 1 } + + // Check if the data size is correct. + if (data.size < (4 + (if (instantaneousStrideLengthPresent) 2 else 0) + if (totalDistancePresent) 4 else 0)) { + return null + } + + // Stride length + var strideLength: Int? = null + if (instantaneousStrideLengthPresent) { + strideLength = + data.getInt(offset, IntFormat.UINT16, byteOrder) + .also { offset += 2 } + } + + // Total distance + var totalDistance: Long? = null + if (totalDistancePresent) { + totalDistance = + data.getInt(offset, IntFormat.UINT32, byteOrder).toLong() + // offset += 4; + } + + return RSCSData(statusRunning, speed, cadence, strideLength, totalDistance) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureData.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureData.kt new file mode 100644 index 00000000..d3032666 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureData.kt @@ -0,0 +1,17 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +/** + * Data class representing the feature data of the Running Speed and Cadence Sensor (RSCS). + * @param instantaneousStrideLengthMeasurementSupported Indicates if instantaneous stride length measurement is supported. + * @param totalDistanceMeasurementSupported Indicates if total distance measurement is supported. + * @param walkingOrRunningStatusSupported Indicates if walking or running status is supported. + * @param calibrationSupported Indicates if calibration is supported. + * @param multipleSensorLocationsSupported Indicates if multiple sensor locations are supported. + */ +data class RSCFeatureData( + val instantaneousStrideLengthMeasurementSupported: Boolean, + val totalDistanceMeasurementSupported: Boolean, + val walkingOrRunningStatusSupported: Boolean, + val calibrationSupported: Boolean, + val multipleSensorLocationsSupported: Boolean +) diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureParser.kt new file mode 100644 index 00000000..613bf066 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSFeatureParser.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +/** + * Parses the RSCS Feature data from a byte array. + * + * @return An instance of [RSCFeatureData] if the data is valid, null otherwise. + */ +object RSCSFeatureDataParser { + + fun parse(data: ByteArray): RSCFeatureData? { + if (data.size != 2) return null + + val featureFlags = ((data[1].toInt() and 0xFF) shl 8) or (data[0].toInt() and 0xFF) + return RSCFeatureData( + instantaneousStrideLengthMeasurementSupported = (featureFlags and 0x01) != 0, + totalDistanceMeasurementSupported = (featureFlags and 0x02) != 0, + walkingOrRunningStatusSupported = (featureFlags and 0x04) != 0, + calibrationSupported = (featureFlags and 0x08) != 0, + multipleSensorLocationsSupported = (featureFlags and 0x10) != 0 + ) + } + +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSSettingsUnit.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSSettingsUnit.kt new file mode 100644 index 00000000..14cc0015 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSSettingsUnit.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +enum class RSCSSettingsUnit { + UNIT_CM, + UNIT_M, + UNIT_KM, + UNIT_MPH, ; + + override fun toString(): String { + return when (this) { + UNIT_KM -> "Kilometer [km/h]" + UNIT_M -> "Meter [m/s]" + UNIT_MPH -> "Miles [mph]" + UNIT_CM -> "Centimeter [cm/s]" + } + } + +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputDataParser.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputDataParser.kt new file mode 100644 index 00000000..8aaec656 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputDataParser.kt @@ -0,0 +1,26 @@ +package no.nordicsemi.android.toolbox.profile.parser.throughput + +import no.nordicsemi.kotlin.data.IntFormat +import no.nordicsemi.kotlin.data.getInt +import java.nio.ByteOrder + +object ThroughputDataParser { + + fun parse(data: ByteArray, byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN): ThroughputMetrics? { + if (data.size != 12) return null + + var offset = 0 + + val numberOfGattWrite = data.getInt(offset, IntFormat.UINT32, byteOrder).toLong() + .also { offset += 4 } + val totalBytesReceived = data.getInt(offset, IntFormat.UINT32, byteOrder).toLong() + .also { offset += 4 } + val throughput = data.getInt(offset, IntFormat.UINT32, byteOrder).toLong() + + return ThroughputMetrics( + gattWritesReceived = numberOfGattWrite, + totalBytesReceived = totalBytesReceived, + throughputBitsPerSecond = throughput, + ) + } +} diff --git a/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputMetrics.kt b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputMetrics.kt new file mode 100644 index 00000000..fb3d46a5 --- /dev/null +++ b/profile-parsers/src/main/java/no/nordicsemi/android/toolbox/profile/parser/throughput/ThroughputMetrics.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.parser.throughput + +data class ThroughputMetrics( + val gattWritesReceived: Long = 0, // Number of GATT writes received + val totalBytesReceived: Long = 0, // Total bytes received + val throughputBitsPerSecond: Long = 0 // Throughput in bits per second +) \ No newline at end of file diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParserTest.kt new file mode 100644 index 00000000..ce6d1172 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/battery/BatteryLevelParserTest.kt @@ -0,0 +1,43 @@ +package no.nordicsemi.android.toolbox.profile.parser.battery + +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class BatteryLevelParserTest { + + @Test + fun `test parse with valid UINT8 data`() { + val data = byteArrayOf(75.toByte()) + val result = BatteryLevelParser.parse(data) + assertEquals(75, result) + } + + @Test + fun `test parse with UINT8 data at maximum value`() { + val data = byteArrayOf(255.toByte()) + val result = BatteryLevelParser.parse(data) + assertEquals(255, result) + } + + @Test + fun `test parse with UINT8 data at minimum value`() { + val data = byteArrayOf(0.toByte()) + val result = BatteryLevelParser.parse(data) + assertEquals(0, result) + } + + @Test + fun `test parse with more than one byte returns null`() { + val data = byteArrayOf(102.toByte(), 108.toByte()) + val result = BatteryLevelParser.parse(data) + assertNull(result) + } + + @Test + fun `test parse with empty data returns null`() { + val data = byteArrayOf() + val result = BatteryLevelParser.parse(data) + assertNull(result) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParserTest.kt new file mode 100644 index 00000000..1e1cba04 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/BloodPressureMeasurementParserTest.kt @@ -0,0 +1,105 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.util.Calendar + +class BloodPressureMeasurementParserTest { + + @Test + fun `test parse valid data with all fields`() { + val data = byteArrayOf( + 0x1f.toByte(), // Flags: All fields present + 0x79.toByte(), 0x00.toByte(), // Systolic: 121 + 0x51.toByte(), 0x00.toByte(), // Diastolic: 81 + 0x6a.toByte(), 0x00.toByte(), // Mean Arterial Pressure: 106 + 0xE4.toByte(), // Year LSB (2020) + 0x07.toByte(), // Year MSB (2020) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + 0x48.toByte(), 0x00.toByte(), // Pulse Rate: 72.0 bpm + 0x01.toByte(), // User ID: 1 + 0x06.toByte(), 0x00.toByte() // Measurement Status: Irregular pulse detected + ) + + val result = BloodPressureMeasurementParser.parse(data) + + assertNotNull(result) + assertEquals(121.0f, result?.systolic) + assertEquals(81.0f, result?.diastolic) + assertEquals(106.0f, result?.meanArterialPressure) + assertEquals(BloodPressureType.UNIT_KPA, result?.unit) + assertEquals(72.0f, result?.pulseRate) + assertEquals(1, result?.userID) + assertNotNull(result?.status) + assertEquals(true, result?.status?.irregularPulseDetected) + assertEquals(Calendar.MAY, result?.calendar?.get(Calendar.MONTH)) + assertEquals(2020, result?.calendar?.get(Calendar.YEAR)) + } + + @Test + fun `test parse valid data without optional fields`() { + val data = byteArrayOf( + 0x00.toByte(), // Flags: No optional fields + 0x48.toByte(), 0x00.toByte(), // Systolic: 72.0 mmHg + 0x51.toByte(), 0x00.toByte(), // Diastolic: 81.0 mmHg + 0x40.toByte(), 0x00.toByte() // Mean Arterial Pressure: 64.0 mmHg + ) + + val result = BloodPressureMeasurementParser.parse(data) + + assertNotNull(result) + assertEquals(72.0f, result?.systolic) + assertEquals(81.0f, result?.diastolic) + assertEquals(64.0f, result?.meanArterialPressure) + assertEquals(BloodPressureType.UNIT_MMHG, result?.unit) + assertNull(result?.pulseRate) + assertNull(result?.userID) + assertNull(result?.status) + assertNull(result?.calendar) + } + + @Test + fun `test parse invalid data length`() { + val data = byteArrayOf( + 0x1F.toByte(), // Flags indicating all fields present + 0x00, 0x48 // Insufficient data + ) + + val result = BloodPressureMeasurementParser.parse(data) + + assertNull(result) + } + + @Test + fun `test parse with missing timestamp and pulse rate`() { + val data = byteArrayOf( + 0x08.toByte(), // Flags: Only systolic, diastolic, mean arterial pressure present + 0x48.toByte(), 0x00.toByte(), // Systolic: 72.0 mmHg + 0x51.toByte(), 0x00.toByte(), // Diastolic: 81.0 mmHg + 0x40.toByte(), 0x00.toByte(), // Mean Arterial Pressure: 64.0 mmHg + 0x01 // User ID: 1 + ) + + val result = BloodPressureMeasurementParser.parse(data) + + assertNotNull(result) + assertEquals(72.0f, result?.systolic) + assertEquals(81.0f, result?.diastolic) + assertEquals(64.0f, result?.meanArterialPressure) + assertNull(result?.calendar) + assertNull(result?.pulseRate) + assertEquals(1, result?.userID) + } + + @Test + fun `test parse null data`() { + val result = BloodPressureMeasurementParser.parse(ByteArray(0)) + assertNull(result) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParserTest.kt new file mode 100644 index 00000000..c23046b6 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/bps/IntermediateCuffPressureParserTest.kt @@ -0,0 +1,121 @@ +package no.nordicsemi.android.toolbox.profile.parser.bps + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.nio.ByteOrder +import java.util.Calendar + +class IntermediateCuffPressureParserTest { + + @Test + fun `parse valid data with all flags set`() { + val data = byteArrayOf( + 0x1F.toByte(), // Flags: all features enabled + 0x51.toByte(), 0x00.toByte(), // Cuff pressure (81.0 mmHg) + 0x00.toByte(), 0x00.toByte(), // following bytes - cuff pressure. Diastolic and MAP are unused + 0x00.toByte(), 0x00.toByte(), + 0xE4.toByte(), // Year LSB (2020) + 0x07.toByte(), // Year MSB (2020) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + 0x64.toByte(), 0x00.toByte(), // Pulse rate (100 bpm) + 0x01.toByte(), // User ID (1) + 0x06.toByte(), 0x00.toByte() // Measurement status + ) + + val result = IntermediateCuffPressureParser.parse(data) + + assertNotNull(result) + assertEquals(81.0f, result?.cuffPressure) + assertEquals(BloodPressureType.UNIT_KPA, result?.unit) + assertNotNull(result?.calendar) + assertEquals(100.0f, result?.pulseRate) + assertEquals(1, result?.userID) + assertNotNull(result?.status) + assertEquals(Calendar.MAY, result?.calendar?.get(Calendar.MONTH)) + assertEquals(2020, result?.calendar?.get(Calendar.YEAR)) + } + + @Test + fun `parse valid data with no optional fields`() { + val data = byteArrayOf( + 0x01.toByte(), // Flags: no optional fields + 0x51.toByte(), 0x00.toByte(), // Cuff pressure (81.0 mmHg) + 0x00.toByte(), 0x00.toByte(), // following bytes - cuff pressure. Diastolic and MAP are unused + 0x00.toByte(), 0x00.toByte(), + ) + + val result = IntermediateCuffPressureParser.parse(data) + + assertNotNull(result) + assertEquals(81.0f, result?.cuffPressure) + assertEquals(BloodPressureType.UNIT_KPA, result?.unit) + assertNull(result?.calendar) + assertNull(result?.pulseRate) + assertNull(result?.userID) + assertNull(result?.status) + } + + @Test + fun `parse data with insufficient length`() { + val data = byteArrayOf( + 0x00.toByte(), // Flags: no optional fields + 0x34.toByte() // Incomplete cuff pressure + ) + + val result = IntermediateCuffPressureParser.parse(data, ByteOrder.LITTLE_ENDIAN) + + assertNull(result) + } + + @Test + fun `parse valid data with timestamp only`() { + val data = byteArrayOf( + 0x02.toByte(), // Flags: timestamp present + 0x51.toByte(), 0x00.toByte(), // Cuff pressure (81.0 mmHg) + 0x00.toByte(), 0x00.toByte(), // Following bytes - cuff pressure. Diastolic and MAP are unused + 0x00.toByte(), 0x00.toByte(), + 0xE4.toByte(), // Year LSB (2020) + 0x07.toByte(), // Year MSB (2020) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + ) + + val result = IntermediateCuffPressureParser.parse(data, ByteOrder.LITTLE_ENDIAN) + + assertNotNull(result) + assertEquals(81.0f, result?.cuffPressure) + assertNotNull(result?.calendar) + assertNull(result?.pulseRate) + assertNull(result?.userID) + assertNull(result?.status) + } + + @Test + fun `parse valid data with kPa unit`() { + val data = byteArrayOf( + 0x01.toByte(), // Flags: kPa unit, no optional fields + 0x51.toByte(), 0x00.toByte(), // Cuff pressure (81.0 mmHg) + 0x00.toByte(), 0x00.toByte(), // Following bytes - cuff pressure. Diastolic and MAP are unused + 0x00.toByte(), 0x00.toByte(), + ) + + val result = IntermediateCuffPressureParser.parse(data, ByteOrder.LITTLE_ENDIAN) + + assertNotNull(result) + assertEquals(81.0f, result?.cuffPressure) + assertEquals(BloodPressureType.UNIT_KPA, result?.unit) + assertNull(result?.calendar) + assertNull(result?.pulseRate) + assertNull(result?.userID) + assertNull(result?.status) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMFeatureParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMFeatureParserTest.kt new file mode 100644 index 00000000..aca94b34 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMFeatureParserTest.kt @@ -0,0 +1,46 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgm + +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMFeatureParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.nio.ByteOrder + +class CGMFeatureParserTest { + + @Test + fun `valid input with E2E CRC supported and non-matching CRC`() { + val data = byteArrayOf(0x01, 0x00, 0x10, 0x12, 0xFF.toByte(), 0xEE.toByte()) // Example data + val result = CGMFeatureParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNull(result) // CRC mismatch should return null + } + + + @Test + fun `valid input without E2E CRC and expected CRC as 0xFFFF`() { + val data = byteArrayOf(0x00, 0x00, 0x10, 0x12, 0xFF.toByte(), 0xFF.toByte()) + val result = CGMFeatureParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNotNull(result) + result?.features?.e2eCrcSupported?.let { assertFalse(it) } + } + + @Test + fun `invalid input - byte array size not equal to 6`() { + val data = byteArrayOf(0x01, 0x00, 0x10) // Too short + val result = CGMFeatureParser.parse(data) + assertNull(result) + } + + @Test + fun `type and sample location parsing`() { + val data = byteArrayOf(0x01, 0x00, 0x10, 0x21, 0xFF.toByte(), 0xFF.toByte()) + val result = CGMFeatureParser.parse(data) + + assertNotNull(result) + assertEquals(1, result?.type) + assertEquals(2, result?.sampleLocation) + } +} + diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMMeasurementParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMMeasurementParserTest.kt new file mode 100644 index 00000000..90872203 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/cgm/CGMMeasurementParserTest.kt @@ -0,0 +1,103 @@ +package no.nordicsemi.android.toolbox.profile.parser.cgm + +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMMeasurementParser +import no.nordicsemi.android.toolbox.profile.parser.common.CRC16 +import org.junit.Assert.* +import org.junit.Test +import java.nio.ByteOrder + +class CGMMeasurementParserTest { + + @Test + fun `test valid data with all optional fields`() { + // Constructing a valid byte array + val data = byteArrayOf( + 0x0F, // Size: 14 bytes (6 base + 2 trend + 2 quality + 1 warning + 1 temp + 1 status + 2 CRC) + 0xE3.toByte(), // Flags: All optional fields present (binary 11100011) + 0x78, 0x00, // Glucose concentration: 120 mg/dL + 0x1E, 0x00, // Time offset: 30 minutes + 0x01, // Warning status + 0x02, // Calibration temp status + 0x03, // Sensor status + 0x50, 0x00, // Trend: 80 mg/dL/min + 0x60, 0x00, // Quality: 96 mg/dL + 0x34, 0x12 // CRC: Placeholder (valid CRC for this data) + ) + + // Calculate correct CRC and update the placeholder + val expectedCrc = CRC16.MCRF4XX(data, 0, 12) // Excluding the CRC bytes + data[data.size - 2] = (expectedCrc and 0xFF).toByte() + data[data.size - 1] = (expectedCrc shr 8).toByte() + + val records = CGMMeasurementParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNotNull(records) + assertEquals(1, records!!.size) + + val record = records[0] + assertEquals(120f, record.glucoseConcentration) + assertEquals(30, record.timeOffset) + assertNotNull(record.status) + assertEquals(1, record.status!!.deviceSpecificAlert) + assertEquals(2, record.status.calibrationRequired) + assertEquals(3, record.status.sensorMalfunction) + assertEquals(80f, record.trend!!) + assertEquals(96f, record.quality!!) + assertTrue(record.crcPresent) + } + + @Test + fun `test valid data without optional fields`() { + val data = byteArrayOf( + 0x06, // Size: 6 bytes (base packet size without any optional fields) + 0x00, // Flags: No optional fields present + 0x78, 0x00, // Glucose concentration: 120 mg/dL + 0x1E, 0x00 // Time offset: 30 minutes + ) + + val records = CGMMeasurementParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNotNull(records) + assertEquals(1, records!!.size) + + val record = records[0] + assertEquals(120f, record.glucoseConcentration) + assertEquals(30, record.timeOffset) + assertNull(record.status) + assertNull(record.trend) + assertNull(record.quality) + assertFalse(record.crcPresent) + } + + @Test + fun `test invalid data with incorrect size`() { + val data = byteArrayOf( + 0x05, // Size: 5 bytes (less than minimum size of 6) + 0x00, // Flags: No optional fields present + 0x78, 0x00, // Glucose concentration: 120 mg/dL + 0x1E // Incomplete time offset + ) + + val records = CGMMeasurementParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNull(records) + } + + @Test + fun `test invalid data with mismatched CRC`() { + val data = byteArrayOf( + 0x08, // Size: 8 bytes (6 base + 2 CRC) + 0x00, // Flags: No optional fields present + 0x78, 0x00, // Glucose concentration: 120 mg/dL + 0x1E, 0x00, // Time offset: 30 minutes + 0x12, 0x34 // CRC: Invalid placeholder + ) + + val records = CGMMeasurementParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNull(records) + } + + @Test + fun `test empty data`() { + val data = byteArrayOf() + val records = CGMMeasurementParser.parse(data, ByteOrder.LITTLE_ENDIAN) + assertNull(records) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParserTest.kt new file mode 100644 index 00000000..c39e8fd9 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/csc/CSCDataParserTest.kt @@ -0,0 +1,127 @@ +package no.nordicsemi.android.toolbox.profile.parser.csc + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import java.nio.ByteBuffer +import java.nio.ByteOrder + +class CSCDataParserTest { + + @Before + fun setup() { + // Reset the parser state before each test + CSCDataParser.previousData = CSCDataSnapshot() + CSCDataParser.wheelRevolutions = -1 + CSCDataParser.wheelEventTime = -1 + CSCDataParser.crankRevolutions = -1 + CSCDataParser.crankEventTime = -1 + } + + @Test + fun `test parse with empty data returns null`() { + val result = CSCDataParser.parse(byteArrayOf()) + assertNull(result) + } + + @Test + fun `test parse with invalid data length returns null`() { + val invalidData = byteArrayOf(0x01) // Insufficient data + val result = CSCDataParser.parse(invalidData) + assertNull(result) + } + + @Test + fun `test parse with wheel revolutions only`() { + val data = byteArrayOf( + 0x01.toByte(), 0x0A.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(), 0xA0.toByte(), 0x00.toByte() + ) + val result = CSCDataParser.parse(data) + + assertEquals(0.0f, result?.distance) // No previous data to compare + assertEquals(0.0f, result?.speed) + assertEquals(0.0f, result?.cadence) + } + + @Test + fun `test parse with wheel and crank revolutions`() { + val data = createData( + flags = 0x03, // Both wheel and crank revolutions present + wheelRevolutions = 1000, + wheelEventTime = 200, + crankRevolutions = 500, + crankEventTime = 150 + ) + + val result = CSCDataParser.parse(data) + + assertEquals(0.0f, result?.distance) // No previous data to compare + assertEquals(0.0f, result?.speed) + assertEquals(0.0f, result?.cadence) + } + + @Test + fun `test parse with sequential data updates distance and speed`() { + val initialData = createData( + flags = 0x01, // Wheel revolution present + wheelRevolutions = 1000, + wheelEventTime = 200 + ) + CSCDataParser.parse(initialData) + + val updatedData = createData( + flags = 0x01, + wheelRevolutions = 1100, // Increased revolutions + wheelEventTime = 1000 // Increased event time + ) + val result = CSCDataParser.parse(updatedData) + + assertEquals(234.0f, result?.distance) // Example calculation + result?.speed?.let { assertEquals(299.52f, it, 0.1f) } // Example calculation + } + + @Test + fun `test parse calculates cadence and gear ratio`() { + val initialData = createData( + flags = 0x03, // Both wheel and crank revolutions present + wheelRevolutions = 10, + wheelEventTime = 200, + crankRevolutions = 5, + crankEventTime = 150 + ) + CSCDataParser.parse(initialData) + + val updatedData = createData( + flags = 0x03, + wheelRevolutions = 15, + wheelEventTime = 300, + crankRevolutions = 6, + crankEventTime = 200 + ) + val result = CSCDataParser.parse(updatedData) + + result?.cadence?.let { assertEquals(1228.8f, it, 0.1f) } // Example calculation + result?.gearRatio?.let { assertEquals(2.5f, it, 0.1f) } // Example calculation + } + + private fun createData( + flags: Int, + wheelRevolutions: Int = 0, + wheelEventTime: Int = 0, + crankRevolutions: Int = 0, + crankEventTime: Int = 0 + ): ByteArray { + val buffer = ByteBuffer.allocate(11).order(ByteOrder.LITTLE_ENDIAN) + buffer.put(flags.toByte()) + if ((flags and 0x01) != 0) { + buffer.putInt(wheelRevolutions) + buffer.putShort(wheelEventTime.toShort()) + } + if ((flags and 0x02) != 0) { + buffer.putShort(crankRevolutions.toShort()) + buffer.putShort(crankEventTime.toShort()) + } + return buffer.array() + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParserTest.kt new file mode 100644 index 00000000..0eecb3da --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/gls/GlucoseMeasurementParserTest.kt @@ -0,0 +1,142 @@ +package no.nordicsemi.android.toolbox.profile.parser.gls + +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ConcentrationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GlucoseStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordType +import no.nordicsemi.android.toolbox.profile.parser.gls.data.SampleLocation +import java.nio.ByteOrder +import java.util.Calendar +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class GlucoseMeasurementParserNewTest { + + @Test + fun `parse should return null for data size less than 10`() { + val data = ByteArray(9) { 0x00.toByte() } + val result = GlucoseMeasurementParser.parse(data) + assertNull(result, "Expected null for data size less than 10") + } + + @Test + fun `parse should parse record with only required fields`() { + val data = byteArrayOf( + 0x00.toByte(), // Flags: No optional fields + 0x01.toByte(), 0x00.toByte(), // Sequence Number + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte() // Second: 45 + ) + val expectedBaseTime = Calendar.getInstance().apply { + set(2020, Calendar.MAY, 21, 10, 30, 45) + set(Calendar.MILLISECOND, 0) + } + + val result = GlucoseMeasurementParser.parse(data) + + assertEquals(1, result?.sequenceNumber) + assertEquals(expectedBaseTime, result?.time) + assertNull(result?.glucoseConcentration) + assertNull(result?.unit) + assertNull(result?.status) + assertNull(result?.type) + assertNull(result?.sampleLocation) + assertEquals(false, result?.contextInformationFollows) + } + + @Test + fun `parse should parse record with all optional fields`() { + val data = byteArrayOf( + 0x1F.toByte(), // Flags: All optional fields present + 0x02.toByte(), + 0x00.toByte(), // Sequence Number + 0xE4.toByte(), + 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + 0x00.toByte(), + 0x00.toByte(), // Time Offset: 0 minutes + 0x51.toByte(), + 0x00.toByte(), + 0x14.toByte(), // Glucose concentration (IEEE 11073 format) and type/sample location + 0x06.toByte(), + 0x00.toByte(), // Sensor Status Annunciation + ) + val expectedBaseTime = Calendar.getInstance().apply { + set(2020, Calendar.MAY, 21, 10, 30, 45) + set(Calendar.MILLISECOND, 0) + } + + val result = GlucoseMeasurementParser.parse(data) + + assertEquals(2, result?.sequenceNumber) + assertEquals(expectedBaseTime, result?.time) + result?.glucoseConcentration?.let { + assertEquals( + 81.0f, + it, 0.01f + ) + } + assertEquals(ConcentrationUnit.UNIT_MOLPL, result?.unit) + assertEquals(RecordType.VENOUS_PLASMA, result?.type) + assertEquals(SampleLocation.FINGER, result?.sampleLocation) + assertEquals(true, result?.contextInformationFollows) + assertEquals(GlucoseStatus(6).toString(), result?.status.toString()) + + } + + @Test + fun `parse should return null for incomplete optional fields`() { + val data = byteArrayOf( + 0x0F, // Flags: All optional fields except context present + 0x03, 0x00, // Sequence Number + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + 0x05, 0x00, // Time Offset + 0xCD.toByte(), 0xCC.toByte() // Incomplete glucose concentration field + ) + val result = GlucoseMeasurementParser.parse(data) + assertNull(result, "Expected null for incomplete optional fields") + } + + @Test + fun `parse should handle little endian and big endian byte orders`() { + val littleEndianData = byteArrayOf( + 0x00, // Flags + 0x01, 0x00, // Sequence Number (Little Endian: 1) + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + ) + val bigEndianData = byteArrayOf( + 0x00, // Flags + 0x00, 0x01, // Sequence Number (Big Endian: 1) + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + ) + + val littleEndianResult = GlucoseMeasurementParser.parse(littleEndianData) + val bigEndianResult = GlucoseMeasurementParser.parse(bigEndianData, ByteOrder.BIG_ENDIAN) + + assertEquals(1, littleEndianResult?.sequenceNumber) + assertEquals(1, bigEndianResult?.sequenceNumber) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParserTest.kt new file mode 100644 index 00000000..02c826db --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hrs/HRSDataParserTest.kt @@ -0,0 +1,68 @@ +package no.nordicsemi.android.toolbox.profile.parser.hrs + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class HRSDataParserTest { + + @Test + fun `test parse with UINT8 heart rate and no additional data`() { + val rawData = byteArrayOf( + 0x00.toByte(), // Flags: UINT8 heart rate, no sensor contact, no energy, no RR intervals + 0x64.toByte() // Heart rate: 100 + ) + val result = HRSDataParser.parse(rawData) + assertEquals(100, result?.heartRate) + assertEquals(false, result?.sensorContact) + assertEquals(null, result?.energyExpanded) + assertEquals(emptyList(), result?.rrIntervals) + } + + @Test + fun `test parse with UINT16 heart rate and sensor contact`() { + val rawData = byteArrayOf( + 0x07.toByte(), // Flags: UINT16 heart rate, sensor contact supported + 0x64.toByte(), // Byte array (little-endian): [0x64, 0x00] for 100 + 0x00.toByte(), + ) + val result = HRSDataParser.parse(rawData) + assertEquals(100, result?.heartRate) + assertEquals(true, result?.sensorContact) + assertEquals(null, result?.energyExpanded) + assertEquals(emptyList(), result?.rrIntervals) + } + + @Test + fun `test parse with energy expanded and RR intervals`() { + val rawData = byteArrayOf( + 0x19.toByte(), // Flags: UINT8 heart rate, energy expanded, RR intervals present + 0x4B.toByte(), 0x00.toByte(), // Byte array (little-endian) Heart rate: 75 + 0x38.toByte(), 0x04.toByte(), // Byte array (little-endian): 1080 + 0x04.toByte(), 0x04.toByte(), // Byte array (little-endian): 1028 + 0x38.toByte(), 0x04.toByte() // RR interval 2 MSB: 1080 + ) + val result = HRSDataParser.parse(rawData) + assertEquals(75, result?.heartRate) + assertEquals(false, result?.sensorContact) + assertEquals(1080, result?.energyExpanded) + assertEquals(listOf(1028, 1080), result?.rrIntervals) + } + + @Test + fun `test parse with insufficient data`() { + val rawData = byteArrayOf(0x00.toByte()) // Only flags byte, insufficient for parsing + val result = HRSDataParser.parse(rawData) + assertNull(result) + } + + @Test + fun `test parse with unsupported flag combinations`() { + val rawData = byteArrayOf( + 0xFF.toByte(), // Flags: Unsupported combination + 0x64.toByte() // Heart rate: 100 + ) + val result = HRSDataParser.parse(rawData) + assertNull(result) + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/DateTimeParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/DateTimeParserTest.kt new file mode 100644 index 00000000..a8cb29e3 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/DateTimeParserTest.kt @@ -0,0 +1,76 @@ +package no.nordicsemi.android.toolbox.profile.parser.hts + +import no.nordicsemi.android.toolbox.profile.parser.date.DateTimeParser +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.util.Calendar + +class DateTimeParserTest { + + @Test + fun `test parse with valid input`() { + val byteArray = byteArrayOf( + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte() // Second: 45 + ) + val offset = 0 + + val calendar = DateTimeParser.parse(byteArray, offset) + + assertNotNull(calendar) + calendar?.let { + assertEquals(2020, it.get(Calendar.YEAR)) + assertEquals(Calendar.MAY, it.get(Calendar.MONTH)) + assertEquals(21, it.get(Calendar.DATE)) + assertEquals(10, it.get(Calendar.HOUR_OF_DAY)) + assertEquals(30, it.get(Calendar.MINUTE)) + assertEquals(45, it.get(Calendar.SECOND)) + } + } + + @Test + fun `test parse with insufficient byte array`() { + val byteArray = byteArrayOf( + 0xE4.toByte(), 0x07.toByte(), // Year: 2020 (little-endian) + 0x05.toByte() // Incomplete data + ) + val offset = 0 + + val calendar = DateTimeParser.parse(byteArray, offset) + + assertNull(calendar) + } + + @Test + fun `test parse with zeroed fields`() { + val byteArray = byteArrayOf( + 0x00.toByte(), 0x00.toByte(), // Year: 0 (invalid) + 0x00.toByte(), // Month: 0 (invalid) + 0x00.toByte(), // Day: 0 (invalid) + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte() // Second: 45 + ) + val offset = 0 + + val calendar = DateTimeParser.parse(byteArray, offset) + + assertNotNull(calendar) + + calendar?.let { + assertFalse(it.isSet(Calendar.YEAR)) + assertFalse(it.isSet(Calendar.MONTH)) + assertFalse(it.isSet(Calendar.DATE)) + assertEquals(10, it.get(Calendar.HOUR_OF_DAY)) + assertEquals(30, it.get(Calendar.MINUTE)) + assertEquals(45, it.get(Calendar.SECOND)) + } + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParserTest.kt new file mode 100644 index 00000000..2d53e626 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/hts/HTSDataParserTest.kt @@ -0,0 +1,95 @@ +package no.nordicsemi.android.toolbox.profile.parser.hts + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import java.util.Calendar + +class HTSDataParserTest { + + @Test + fun `test parse with all fields present`() { + val rawData = byteArrayOf( + 0x06.toByte(), + 0x71.toByte(), // Temperature byte 1 (LSB) + 0x0E.toByte(), // Temperature byte 2 + 0x00.toByte(), // Temperature byte 3 + 0xFE.toByte(), // Temperature byte 4 (MSB) + 0xE4.toByte(), // Year LSB (2020) + 0x07.toByte(), // Year MSB (2020) + 0x05.toByte(), // Month: May + 0x15.toByte(), // Day: 21 + 0x0A.toByte(), // Hour: 10 + 0x1E.toByte(), // Minute: 30 + 0x2D.toByte(), // Second: 45 + 0x00.toByte(), // Type 0: CELSIUS + 0xFE.toByte() // Reserved + ) + + val result = HTSDataParser.parse(rawData) + + assertNotNull(result) + result?.let { + assertEquals(36.97f, it.temperature, 0.01f) + assertEquals(TemperatureUnitData.CELSIUS, it.unit) + assertNotNull(it.timestamp) + assertEquals(2020, it.timestamp?.get(Calendar.YEAR)) + assertEquals(Calendar.MAY, it.timestamp?.get(Calendar.MONTH)) + assertEquals(21, it.timestamp?.get(Calendar.DATE)) + assertEquals(10, it.timestamp?.get(Calendar.HOUR_OF_DAY)) + assertEquals(30, it.timestamp?.get(Calendar.MINUTE)) + assertEquals(45, it.timestamp?.get(Calendar.SECOND)) + } + } + + @Test + fun `test parse without optional fields`() { + val rawData = + byteArrayOf(0x00.toByte(), 0xC4.toByte(), 0x09.toByte(), 0x00.toByte(), 0xFE.toByte()) + + val result = HTSDataParser.parse(rawData) + + assertNotNull(result) + result?.let { + assertEquals(25.0f, it.temperature, 0.01f) + assertEquals(TemperatureUnitData.CELSIUS, it.unit) + assertNull(it.timestamp) + assertNull(it.type) + } + } + + @Test + fun `test parse with invalid float`() { + val byteArray = byteArrayOf( + 0x00.toByte(), // Flags: Unit (Celsius), No Timestamp, No Temperature Type + 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0x7F.toByte() // Temperature: +Infinity + ) + + val result = HTSDataParser.parse(byteArray) + + assertNull(result) // Invalid temperature should result in null + } + + @Test + fun `test parse with insufficient byte array`() { + val byteArray = + byteArrayOf(0x07.toByte()) // Flags indicate more data, but size is insufficient + + val result = HTSDataParser.parse(byteArray) + + assertNull(result) + } + + @Test + fun `test parse with invalid flag`() { + val byteArray = byteArrayOf( + 0x08.toByte(), // Invalid Flag + 0xC4.toByte(), 0x09.toByte(), 0x80.toByte(), 0x3F.toByte() // Temperature: 25.0 + ) + + val result = HTSDataParser.parse(byteArray) + + assertNull(result) // Should return null due to invalid unit flag + } +} diff --git a/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParserTest.kt b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParserTest.kt new file mode 100644 index 00000000..306f7c22 --- /dev/null +++ b/profile-parsers/src/test/java/no/nordicsemi/android/toolbox/profile/parser/rscs/RSCSDataParserTest.kt @@ -0,0 +1,81 @@ +package no.nordicsemi.android.toolbox.profile.parser.rscs + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test + +class RSCSDataParserTest { + + @Test + fun `test parse with all fields present`() { + val data = byteArrayOf( + 0x07.toByte(), // Flags: all fields present (0b00000111) + 0x80.toByte(), + 0x02.toByte(), // Speed: 640 [0x0280] -> 2.5 m/s (640 / 256) + 0x50.toByte(), // Cadence: 80 + 0x20.toByte(), + 0x03.toByte(), // Stride length: 800 [0x0320] + 0x00.toByte(), // Total distance: 4096 [0x00100000] + 0x10.toByte(), + 0x00.toByte(), + 0x00.toByte(), + ) + + val result = RSCSDataParser.parse(data) + + // Validate parsed data + assertNotNull(result) + assertEquals(true, result?.running) // Status: running + assertEquals(2.5f, result?.instantaneousSpeed) // Speed: 2.5 m/s + assertEquals(80, result?.instantaneousCadence) // Cadence: 80 + assertEquals(800, result?.strideLength) // Stride length: 800 mm + assertEquals(4096L, result?.totalDistance) // Total distance: 4096 m + } + + @Test + fun `test parse with only mandatory fields`() { + val data = byteArrayOf( + 0x00.toByte(), // Flags: no optional fields (0b00000000) + 0x80.toByte(), 0x02.toByte(), // Speed: 640 [0x0280] -> 2.5 m/s (640 / 256) + 0x50.toByte() // Cadence: 80 + ) + + val result = RSCSDataParser.parse(data) + + // Validate parsed data + assertNotNull(result) + assertEquals(false, result?.running) // Status: not running + assertEquals(2.5f, result?.instantaneousSpeed) // Speed: 2.5 m/s + assertEquals(80, result?.instantaneousCadence) // Cadence: 80 + assertNull(result?.strideLength) // Stride length: null + assertNull(result?.totalDistance) // Total distance: null + } + + @Test + fun `test parse with insufficient data`() { + val data = byteArrayOf( + 0x00.toByte(), // Flags: no optional fields + 0x80.toByte() // Incomplete speed data + ) + + val result = RSCSDataParser.parse(data) + + assertNull(result) // Parsing should fail due to insufficient data + } + + @Test + fun `test parse with incorrect data size`() { + val data = byteArrayOf( + 0x03.toByte(), // Flags: stride length present + 0x80.toByte(), 0x02.toByte(), // Speed: 640 [0x0280] + 0x50.toByte(), // Cadence: 80 + 0x20.toByte() // Incomplete stride length data + ) + + val result = RSCSDataParser.parse(data) + + // Validate parsed data + assertNull(result) // Parsing should fail due to incorrect data size + } +} diff --git a/profile/build.gradle.kts b/profile/build.gradle.kts new file mode 100644 index 00000000..ff7e789e --- /dev/null +++ b/profile/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + alias(libs.plugins.nordic.feature) +} + +android { + namespace = "no.nordicsemi.android.toolbox.profile" +} + +dependencies { + implementation(project(":lib_analytics")) + implementation(project(":profile_data")) + implementation(project(":lib_ui")) + implementation(project(":lib_utils")) + implementation(project(":profile-parsers")) + implementation(project(":lib_service")) + implementation(project(":profile_manager")) + implementation(project(":lib_storage")) + implementation(project(":permissions-ranging")) + + implementation(libs.nordic.core) + implementation(libs.nordic.navigation) + implementation(libs.nordic.ui) + implementation(libs.nordic.theme) + implementation(libs.nordic.permissions.ble) + implementation(libs.nordic.permissions.notification) + implementation(libs.nordic.logger) + implementation(libs.nordic.log.timber) + implementation(libs.nordic.blek.client.android) + + implementation(libs.slf4j.timber) + implementation(libs.androidx.lifecycle.service) + implementation(libs.chart) + implementation(libs.androidx.compose.material.iconsExtended) + + // DataStore + implementation(libs.androidx.dataStore.core) + implementation(libs.androidx.dataStore.preferences) + + // coroutine core + implementation(libs.kotlinx.coroutines.core) + + // Simple XML + implementation("org.simpleframework:simple-xml:2.7.1") { + exclude(group = "stax", module = "stax-api") + exclude(group = "xpp3", module = "xpp3") + } +} \ No newline at end of file diff --git a/profile/module-rules.pro b/profile/module-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/profile/module-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/profile/src/main/AndroidManifest.xml b/profile/src/main/AndroidManifest.xml new file mode 100644 index 00000000..607518e4 --- /dev/null +++ b/profile/src/main/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileDestination.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileDestination.kt new file mode 100644 index 00000000..c27a0260 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileDestination.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile + +import no.nordicsemi.android.common.navigation.createDestination +import no.nordicsemi.android.common.navigation.defineDestination + +val ProfileDestinationId = createDestination("profile-destination") +val ProfileDestination = listOf( + defineDestination(ProfileDestinationId) { + ProfileScreen() + } +) diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt new file mode 100644 index 00000000..88eb3f93 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/ProfileScreen.kt @@ -0,0 +1,258 @@ +package no.nordicsemi.android.toolbox.profile + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.permissions.ble.RequireBluetooth +import no.nordicsemi.android.common.permissions.ble.RequireLocation +import no.nordicsemi.android.common.permissions.notification.RequestNotificationPermission +import no.nordicsemi.android.service.profile.CustomReason +import no.nordicsemi.android.service.profile.DeviceDisconnectionReason +import no.nordicsemi.android.service.profile.StateReason +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.data.toReason +import no.nordicsemi.android.toolbox.profile.view.battery.BatteryScreen +import no.nordicsemi.android.toolbox.profile.view.bps.BPSScreen +import no.nordicsemi.android.toolbox.profile.view.cgms.CGMScreen +import no.nordicsemi.android.toolbox.profile.view.channelSounding.ChannelSoundingScreen +import no.nordicsemi.android.toolbox.profile.view.cscs.CSCScreen +import no.nordicsemi.android.toolbox.profile.view.directionFinder.DFSScreen +import no.nordicsemi.android.toolbox.profile.view.gls.GLSScreen +import no.nordicsemi.android.toolbox.profile.view.hrs.HRSScreen +import no.nordicsemi.android.toolbox.profile.view.hts.HTSScreen +import no.nordicsemi.android.toolbox.profile.view.internal.ProfileAppBar +import no.nordicsemi.android.toolbox.profile.view.lbs.BlinkyScreen +import no.nordicsemi.android.toolbox.profile.view.rscs.RSCSScreen +import no.nordicsemi.android.toolbox.profile.view.throughput.ThroughputScreen +import no.nordicsemi.android.toolbox.profile.view.uart.UARTScreen +import no.nordicsemi.android.toolbox.profile.viewmodel.ConnectionEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.ProfileViewModel +import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceConnectionState +import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceData +import no.nordicsemi.android.ui.view.internal.DeviceConnectingView +import no.nordicsemi.android.ui.view.internal.DisconnectReason +import no.nordicsemi.android.ui.view.internal.LoadingView +import no.nordicsemi.android.ui.view.internal.ServiceDiscoveryView + +@Composable +internal fun ProfileScreen() { + val profileViewModel: ProfileViewModel = hiltViewModel() + val deviceAddress = profileViewModel.address + val deviceDataState by profileViewModel.deviceState.collectAsStateWithLifecycle() + val onClickEvent: (ConnectionEvent) -> Unit = { event -> + profileViewModel.onConnectionEvent(event) + } + + Scaffold( + topBar = { + ProfileAppBar( + deviceName = when (val state = deviceDataState) { + is DeviceConnectionState.Connected -> state.data.peripheral?.name + ?: deviceAddress + + is DeviceConnectionState.Disconnected -> state.device?.name ?: deviceAddress + + else -> deviceAddress + }, + title = deviceAddress, + connectionState = deviceDataState, + navigateUp = { onClickEvent(ConnectionEvent.NavigateUp) }, + disconnect = { onClickEvent(ConnectionEvent.DisconnectEvent(deviceAddress)) }, + openLogger = { onClickEvent(ConnectionEvent.OpenLoggerEvent) } + ) + }, + ) { paddingValues -> + // Get notch padding for devices with a display cutout (notch) + val notchPadding = WindowInsets.displayCutout + .only(WindowInsetsSides.Horizontal) + .asPaddingValues() + + RequireBluetooth { + RequireLocation { + RequestNotificationPermission { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(notchPadding) + .verticalScroll(rememberScrollState()) + .imePadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + when (val state = deviceDataState) { + is DeviceConnectionState.Connected -> { + DeviceConnectedView( + state.data, + onClickEvent + ) + } + + DeviceConnectionState.Connecting -> DeviceConnectingView( + modifier = Modifier + .padding(16.dp) + ) + + is DeviceConnectionState.Disconnected -> { + state.reason?.let { + DeviceDisconnectedView( + it, + deviceAddress, + onClickEvent + ) + } + } + + DeviceConnectionState.Idle, DeviceConnectionState.Disconnecting -> LoadingView() + } + } + } + } + } + } +} + +@Composable +internal fun DeviceDisconnectedView( + reason: DeviceDisconnectionReason, + deviceAddress: String, + onClickEvent: (ConnectionEvent) -> Unit +) { + when (reason) { + is CustomReason -> { + no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView( + reason = reason.reason, + modifier = Modifier + .padding(16.dp) + ) { + Button( + onClick = { onClickEvent(ConnectionEvent.OnRetryClicked(deviceAddress)) }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = stringResource(id = R.string.reconnect)) + } + } + } + + is StateReason -> { + no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView( + disconnectedReason = toReason(reason.reason), + modifier = Modifier + .padding(16.dp) + ) { + Button( + onClick = { onClickEvent(ConnectionEvent.OnRetryClicked(deviceAddress)) }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = stringResource(id = R.string.reconnect)) + } + } + } + } +} + +@Composable +internal fun DeviceConnectedView( + deviceData: DeviceData, + onClickEvent: (ConnectionEvent) -> Unit, +) { + // Is missing services? + deviceData.peripheral?.let { peripheral -> + when { + deviceData.isMissingServices -> { + no.nordicsemi.android.ui.view.internal.DeviceDisconnectedView( + reason = DisconnectReason.MISSING_SERVICE, + modifier = Modifier + .padding(16.dp) + ) + } + + else -> { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(16.dp) + .imePadding() + ) { + deviceData.peripheralProfileMap[deviceData.peripheral]?.forEach { profile -> + Column( + modifier = Modifier + .imePadding() + ) { + // Requires max value length to be set. + val needsMaxValueLength = profile.profile == Profile.CHANNEL_SOUNDING || + profile.profile == Profile.UART + if (needsMaxValueLength) { + LaunchedEffect(key1 = true) { + if (deviceData.maxValueLength == null) { + onClickEvent(ConnectionEvent.RequestMaxValueLength) + } + } + } + when (profile.profile) { + Profile.HTS -> HTSScreen() + Profile.CHANNEL_SOUNDING -> ChannelSoundingScreen() + Profile.BPS -> BPSScreen() + Profile.CSC -> CSCScreen() + Profile.CGM -> CGMScreen() + Profile.DFS -> DFSScreen() + Profile.GLS -> GLSScreen() + Profile.HRS -> HRSScreen() + Profile.LBS -> BlinkyScreen() + Profile.RSCS -> RSCSScreen() + Profile.THROUGHPUT -> ThroughputScreen(deviceData.maxValueLength) + Profile.UART -> UARTScreen(deviceData.maxValueLength) + + else -> { + // Do nothing. + } + } + if (profile.profile == Profile.BATTERY) { + // Battery level will be added at the end. + BatteryScreen() + } + } + } ?: run { + ServiceDiscoveryView( + modifier = Modifier + ) { + Button( + onClick = { + onClickEvent( + ConnectionEvent.DisconnectEvent( + peripheral.address + ) + ) + }, + modifier = Modifier.padding(16.dp) + ) { + Text(text = stringResource(id = R.string.cancel)) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/data/UiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/data/UiMapper.kt new file mode 100644 index 00000000..62f6ab0a --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/data/UiMapper.kt @@ -0,0 +1,15 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.kotlin.ble.core.ConnectionState.Disconnected.Reason + +fun toReason(reason: Reason): String = + when (reason) { + Reason.Cancelled -> "Connection was cancelled." + Reason.LinkLoss -> "Device signal has been lost." + Reason.Success -> "Device disconnected successfully." + Reason.TerminateLocalHost -> "Device disconnected by the local host." + Reason.TerminatePeerUser -> "Device disconnected by the peer user." + is Reason.Timeout -> "Connection attempt timed out with ${reason.duration}." + is Reason.Unknown -> "Oops...! Connection went on a coffee break." + Reason.UnsupportedAddress -> "Device disconnected due to unsupported address." + } \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/DeviceRepository.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/DeviceRepository.kt new file mode 100644 index 00000000..21a58041 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/DeviceRepository.kt @@ -0,0 +1,60 @@ +package no.nordicsemi.android.toolbox.profile.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.analytics.AppAnalytics +import no.nordicsemi.android.analytics.ProfileConnectedEvent +import no.nordicsemi.android.toolbox.profile.manager.ServiceManager +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DeviceRepository @Inject constructor( + private val analytics: AppAnalytics, +) { + private val _connectedDevices = + MutableStateFlow>>>(emptyMap()) + val connectedDevices = _connectedDevices.asStateFlow() + + private val _profilePeripheralPair = + MutableStateFlow>>(emptyMap()) + val profileHandlerFlow = _profilePeripheralPair.asSharedFlow() + + private val _loggedProfiles = mutableListOf>() + + fun updateConnectedDevices(devices: Map>>) { + _connectedDevices.update { devices } + } + + fun updateProfilePeripheralPair( + peripheral: Peripheral, + serviceManager: List + ) { + _profilePeripheralPair.update { + it.toMutableMap().apply { this[peripheral] = serviceManager } + } + + } + + /** + * Updates the analytics with the profile connected event if it has not been logged before. + */ + fun updateAnalytics(address: String, profile: Profile) { + if (!_loggedProfiles.any { it.first == address && it.second == profile.toString() }) { + analytics.logEvent(ProfileConnectedEvent(profile)) + _loggedProfiles.add(address to profile.toString()) + } + } + + /** + * Removes the logged profile for the given address. + */ + fun removeLoggedProfile(address: String) { + _loggedProfiles.removeAll { it.first == address } + } + +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/ChannelSoundingManager.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/ChannelSoundingManager.kt new file mode 100644 index 00000000..9765e97f --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/ChannelSoundingManager.kt @@ -0,0 +1,144 @@ +package no.nordicsemi.android.toolbox.profile.repository.channelSounding + +import android.content.Context +import android.os.Build +import android.ranging.RangingData +import android.ranging.RangingDevice +import android.ranging.RangingManager +import android.ranging.RangingPreference +import android.ranging.RangingPreference.DEVICE_ROLE_RESPONDER +import android.ranging.RangingSession +import android.ranging.SensorFusionParams +import android.ranging.SessionConfig +import android.ranging.ble.cs.BleCsRangingParams +import android.ranging.raw.RawRangingDevice +import android.ranging.raw.RawResponderRangingConfig +import androidx.annotation.RequiresApi +import no.nordicsemi.android.toolbox.profile.repository.channelSounding.RangingSessionStartTechnology.Companion.getTechnology +import timber.log.Timber + +object ChannelSoundingManager { + + private var rangingSession: RangingSession? = null + + private val rangingSessionCallback = @RequiresApi(Build.VERSION_CODES.BAKLAVA) + object : RangingSession.Callback { + override fun onClosed(reason: Int) { + Timber.d("closed, reason: ${RangingSessionCloseReason.getReason(reason)}") + } + + override fun onOpenFailed(reason: Int) { + Timber.d("Failed, reason: ${RangingSessionFailedReason.getReason(reason)}") + } + + override fun onOpened() { + Timber.d("Opened successfully.") + } + + override fun onResults( + peer: RangingDevice, + data: RangingData + ) { + val measurement = data.distance?.measurement + val confidence = data.distance?.confidence + Timber.d("RangingTechnology: ${data.rangingTechnology}") + Timber.d( + "Azimuth: ${data.azimuth}\televation: " + + "${data.elevation}\tpeer: ${peer.uuid} distance ${data.distance}\t" + + " rssi: ${data.rssi} \tmeasurement: $measurement\tconfidence: $confidence" + ) + } + + override fun onStarted( + peer: RangingDevice, + technology: Int + ) { + Timber.d( + "Session started with peer: ${peer.uuid}, \ntechnology: ${getTechnology(technology)}" + ) + } + + override fun onStopped( + peer: RangingDevice, + technology: Int + ) { + Timber.d("Session stopped with peer: ${peer.uuid}") + } + } + + @RequiresApi(Build.VERSION_CODES.BAKLAVA) + fun addDeviceToRangingSession( + context: Context, + device: String + ) { + val rangingManager = try { + context.getSystemService(RangingManager::class.java) + } catch (e: Exception) { + null + } + if (rangingManager == null) { + // RangingManager is not supported on this device + return + } + val rangingCapabilityCallback = RangingManager.RangingCapabilitiesCallback { capabilities -> + if (capabilities.csCapabilities != null) { + capabilities.csCapabilities!!.supportedSecurityLevels + .find { it == 1 } + ?.let { + Timber.d("Channel Sounding supported.") + } + } else { + Timber.d("Channel Sounding Capabilities is not supported") + } + + } + + rangingManager.registerCapabilitiesCallback( + context.mainExecutor, + rangingCapabilityCallback + ) + + val rangingDevice = RangingDevice.Builder() + .build() + + val csRangingParams = BleCsRangingParams.Builder(device) + .build() + + val rawRangingDevice = RawRangingDevice.Builder() + .setRangingDevice(rangingDevice) + .setCsRangingParams(csRangingParams) + .build() + + val rawRangingDeviceConfig = RawResponderRangingConfig.Builder() + .setRawRangingDevice(rawRangingDevice) + .build() + + val rangingPreference = RangingPreference.Builder( + DEVICE_ROLE_RESPONDER, + rawRangingDeviceConfig + ) + .setSessionConfig( + SessionConfig.Builder() + .setRangingMeasurementsLimit(1000) + .setAngleOfArrivalNeeded(true) + .setSensorFusionParams( + SensorFusionParams.Builder() + .setSensorFusionEnabled(false) + .build() + ) + .build() + ) + .build() + + rangingSession = rangingManager.createRangingSession( + context.mainExecutor, + rangingSessionCallback + ) + rangingSession?.let { + it.addDeviceToRangingSession(rawRangingDeviceConfig) + it.start(rangingPreference) + } + } + +} + diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionCloseReason.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionCloseReason.kt new file mode 100644 index 00000000..b2abeee9 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionCloseReason.kt @@ -0,0 +1,28 @@ +package no.nordicsemi.android.toolbox.profile.repository.channelSounding + +enum class RangingSessionCloseReason(val reason: Int) { + REASON_UNKNOWN(0), + REASON_LOCAL_REQUEST(1), + REASON_REMOTE_REQUEST(2), + REASON_UNSUPPORTED(3), + REASON_SYSTEM_POLICY(4), + REASON_NO_PEERS_FOUND(5), ; + + override fun toString(): String { + return when (reason) { + REASON_UNKNOWN.reason -> "Unknown" + REASON_LOCAL_REQUEST.reason -> "Local request" + REASON_NO_PEERS_FOUND.reason -> "No peers found" + REASON_REMOTE_REQUEST.reason -> "Remote request" + REASON_SYSTEM_POLICY.reason -> "System policy" + REASON_UNSUPPORTED.reason -> "Unsupported" + else -> "Unknown reason" + } + } + + companion object { + fun getReason(reason: Int): String { + return entries.firstOrNull { it.reason == reason }.toString() + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionFailedReason.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionFailedReason.kt new file mode 100644 index 00000000..71a1f0a3 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionFailedReason.kt @@ -0,0 +1,27 @@ +package no.nordicsemi.android.toolbox.profile.repository.channelSounding + +enum class RangingSessionFailedReason(val reason: Int) { + UNKNOWN(0), + LOCAL_REQUEST(1), + REMOTE_REQUEST(2), + UNSUPPORTED(3), + SYSTEM_POLICY(4), + NO_PEERS_FOUND(5), ; + + override fun toString(): String { + return when (this) { + UNKNOWN -> "Unknown" + LOCAL_REQUEST -> "Local request" // Indicates that the session was closed because AutoCloseable.close() or RangingSession.stop() was called. + REMOTE_REQUEST -> "Remote request" // Indicates that the session was closed at the request of a remote peer. + UNSUPPORTED -> "Unsupported" // Indicates that the session closed because the provided session parameters were not supported. + SYSTEM_POLICY -> "System policy" // Indicates that the local system policy forced the session to close, such as power management policy, airplane mode etc. + NO_PEERS_FOUND -> "No peers found" // Indicates that the session was closed because none of the specified peers were found. + } + } + + companion object { + fun getReason(value: Int): String { + return entries.firstOrNull { it.reason == value }.toString() + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionStartTechnology.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionStartTechnology.kt new file mode 100644 index 00000000..087e7a5c --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/channelSounding/RangingSessionStartTechnology.kt @@ -0,0 +1,24 @@ +package no.nordicsemi.android.toolbox.profile.repository.channelSounding + +enum class RangingSessionStartTechnology(val technology: Int) { + UWB(0), + BLE_CS(1), + WIFI_NAN_RTT(2), + BLE_RSSI(3), ; + + override fun toString(): String { + return when (technology) { + UWB.technology -> "UWB" + BLE_CS.technology -> "BLE CS" + WIFI_NAN_RTT.technology -> "WIFI NAN RTT" + BLE_RSSI.technology -> "BLE RSSI" + else -> "Unknown technology" + } + } + + companion object { + fun getTechnology(technology: Int): String { + return entries.firstOrNull { it.technology == technology }.toString() + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/CommentVisitor.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/CommentVisitor.kt new file mode 100644 index 00000000..7042f26e --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/CommentVisitor.kt @@ -0,0 +1,28 @@ +package no.nordicsemi.android.toolbox.profile.repository.uartXml + +import no.nordicsemi.android.toolbox.profile.data.uart.MacroIcon +import org.simpleframework.xml.strategy.Type +import org.simpleframework.xml.strategy.Visitor +import org.simpleframework.xml.stream.InputNode +import org.simpleframework.xml.stream.NodeMap +import org.simpleframework.xml.stream.OutputNode + +/** + * The comment visitor will add comments to the XML during saving. + */ +internal class CommentVisitor : Visitor { + override fun read(type: Type, node: NodeMap) { + // do nothing + } + + override fun write(type: Type, node: NodeMap) { + if (type.type == Array::class.java) { + val element = node.node + val builder = + StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:") + for (icon in MacroIcon.entries) builder.append("\n - ") + .append(icon.toString()) + element.comment = builder.toString() + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationDataStore.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationDataStore.kt new file mode 100644 index 00000000..118119ae --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationDataStore.kt @@ -0,0 +1,36 @@ +package no.nordicsemi.android.toolbox.profile.repository.uartXml + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private const val FILE = "UART_CONFIGURATION" +private const val LAST_CONFIGURATION_KEY = "LAST_CONFIGURATION" + +@Singleton +internal class UartConfigurationDataSource @Inject constructor( + @param:ApplicationContext private val context: Context +) { + private val Context.dataStore: DataStore by preferencesDataStore(name = FILE) + + val lastConfigurationName = context.dataStore.data.map { + it[LAST_CONFIGURATION] + } + + suspend fun saveConfigurationName(name: String) { + context.dataStore.edit { + it[LAST_CONFIGURATION] = name + } + } + + companion object { + private val LAST_CONFIGURATION = stringPreferencesKey(LAST_CONFIGURATION_KEY) + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationRepository.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationRepository.kt new file mode 100644 index 00000000..95ffb5c5 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/UartConfigurationRepository.kt @@ -0,0 +1,114 @@ +package no.nordicsemi.android.toolbox.profile.repository.uartXml + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import no.nordicsemi.android.toolbox.lib.storage.ConfigurationEntity +import no.nordicsemi.android.toolbox.lib.storage.ConfigurationsDao +import no.nordicsemi.android.toolbox.profile.data.uart.MacroIcon +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro +import org.simpleframework.xml.Serializer +import org.simpleframework.xml.core.Persister +import org.simpleframework.xml.strategy.Strategy +import org.simpleframework.xml.strategy.VisitorStrategy +import org.simpleframework.xml.stream.Format +import org.simpleframework.xml.stream.HyphenStyle +import timber.log.Timber +import java.io.StringWriter +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class UartConfigurationRepository @Inject constructor( + private val configurationDao: ConfigurationsDao, + private val uartDataStore: UartConfigurationDataSource +) { + // Get all uart configurations. + fun getAllConfigurations(): Flow> = + configurationDao.getAllConfigurations().map { configurations -> + configurations.mapNotNull { it.toDomain() } + } + + private fun ConfigurationEntity.toDomain(): UARTConfiguration? { + return try { + val xml: String = xml + val format = Format(HyphenStyle()) + val serializer: Serializer = Persister(format) + val configuration = serializer.read(XmlConfiguration::class.java, xml) + + UARTConfiguration( + _id, + configuration.name ?: "Unknown", + createMacro(configuration.commands) + ) + } catch (t: Throwable) { + t.printStackTrace() + null + } + } + + private fun createMacro(macros: Array): List { + return macros.map { + if (it == null) { + null + } else { + val icon = MacroIcon.create(it.iconIndex) + UARTMacro(icon, it.command, it.eol) + } + } + } + + suspend fun insertConfiguration(configuration: UARTConfiguration): Long? { + val configurationEntity = configuration.toConfigurationEntity() + return configurationEntity?.let { configurationDao.insertConfiguration(it) } + } + + suspend fun deleteConfiguration(configuration: UARTConfiguration) { + configurationDao.deleteConfiguration(configuration.name) + } + + private fun UARTConfiguration.toConfigurationEntity(): ConfigurationEntity? { + return try { + val format = Format(HyphenStyle()) + val strategy: Strategy = VisitorStrategy(CommentVisitor()) + val serializer: Serializer = Persister(strategy, format) + val writer = StringWriter() + serializer.write(this.toXmlConfiguration(), writer) + + return ConfigurationEntity( + _id = id, + name = name, + xml = writer.toString(), + deleted = 0 + ) + } catch (e: Exception) { + Timber.e(e, "Error converting to ConfigurationEntity") + null + } + + } + + private fun UARTConfiguration.toXmlConfiguration(): XmlConfiguration { + val xmlConfiguration = XmlConfiguration() + xmlConfiguration.name = name + val commands = macros.map { macro -> + macro?.let { + XmlMacro().apply { + setEol(it.newLineChar.ordinal) + command = it.command + iconIndex = it.icon.index + } + } + }.toTypedArray() + xmlConfiguration.commands = commands + return xmlConfiguration + } + + fun getLastConfigurationName(): Flow { + return uartDataStore.lastConfigurationName + } + + suspend fun saveLastConfigurationNameToDataSource(name: String) { + uartDataStore.saveConfigurationName(name) + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlConfiguration.java b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlConfiguration.java new file mode 100644 index 00000000..95cdb8b1 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlConfiguration.java @@ -0,0 +1,53 @@ +package no.nordicsemi.android.toolbox.profile.repository.uartXml; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.ElementArray; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.core.PersistenceException; +import org.simpleframework.xml.core.Validate; + +@Root +public class XmlConfiguration { + public static final int COMMANDS_COUNT = 9; + + @Attribute(required = false, empty = "Unnamed") + private String name; + + @ElementArray + private XmlMacro[] commands = new XmlMacro[COMMANDS_COUNT]; + + /** + * Returns the field name + * + * @return optional name + */ + public String getName() { + return name; + } + + /** + * Sets the name to specified value + * @param name the new name + */ + public void setName(final String name) { + this.name = name; + } + + /** + * Returns the array of commands. There is always 9 of them. + * @return the commands array + */ + public XmlMacro[] getCommands() { + return commands; + } + + public void setCommands(XmlMacro[] commands) { + this.commands = commands; + } + + @Validate + private void validate() throws PersistenceException { + if (commands == null || commands.length != COMMANDS_COUNT) + throw new PersistenceException("There must be always " + COMMANDS_COUNT + " commands in a configuration."); + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlMacro.java b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlMacro.java new file mode 100644 index 00000000..e9808aa9 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/repository/uartXml/XmlMacro.java @@ -0,0 +1,95 @@ +package no.nordicsemi.android.toolbox.profile.repository.uartXml; + +import org.simpleframework.xml.Attribute; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.Text; + +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol; +import no.nordicsemi.android.toolbox.profile.data.uart.MacroIcon; + +@Root +public class XmlMacro { + + @Text(required = false) + private String command; + + @Attribute(required = false) + private boolean active = false; + + @Attribute(required = false) + private MacroEol eol = MacroEol.LF; + + @Attribute(required = false) + private MacroIcon icon = MacroIcon.LEFT; + + /** + * Sets the command. + * @param command the command that will be sent to UART device + */ + public void setCommand(final String command) { + this.command = command; + } + + /** + * Sets whether the command is active. + * @param active true to make it active + */ + public void setActive(final boolean active) { + this.active = active; + } + + /** + * Sets the new line type. + * @param eol end of line terminator + */ + public void setEol(final int eol) { + this.eol = MacroEol.getEntries().get(eol); + } + + /** + * Sets the icon index. + * @param index index of the icon. + */ + public void setIconIndex(final int index) { + this.icon = MacroIcon.getEntries().get(index); + } + + /** + * Returns the command that will be sent to UART device. + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * Returns whether the icon is active. + * @return true if it's active + */ + public boolean isActive() { + return active; + } + + /** + * Returns the new line type. + * @return end of line terminator + */ + public MacroEol getEol() { + return eol; + } + + /** + * Returns the icon index. + * @return the icon index + */ + public int getIconIndex() { + return icon.getIndex(); + } + /** + * Returns the EOL index. + * @return the EOL index + */ + public int getEolIndex() { + return eol.ordinal(); + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/battery/BatteryLevelView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/battery/BatteryLevelView.kt new file mode 100644 index 00000000..c7051f0c --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/battery/BatteryLevelView.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022, Nordic Semiconductor + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * 3. Neither the name of the copyright holder nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A + * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package no.nordicsemi.android.toolbox.profile.view.battery + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BatteryChargingFull +import androidx.compose.material.icons.outlined.Battery0Bar +import androidx.compose.material.icons.outlined.Battery1Bar +import androidx.compose.material.icons.outlined.Battery2Bar +import androidx.compose.material.icons.outlined.Battery3Bar +import androidx.compose.material.icons.outlined.Battery4Bar +import androidx.compose.material.icons.outlined.Battery5Bar +import androidx.compose.material.icons.outlined.Battery6Bar +import androidx.compose.material.icons.outlined.BatteryAlert +import androidx.compose.material.icons.outlined.BatteryFull +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.common.theme.nordicGrass +import no.nordicsemi.android.common.theme.nordicGreen +import no.nordicsemi.android.toolbox.profile.viewmodel.BatteryViewModel +import no.nordicsemi.android.ui.R +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun BatteryScreen() { + val batteryViewModel = hiltViewModel() + val batteryServiceData by batteryViewModel.batteryServiceState.collectAsStateWithLifecycle() + + ScreenSection { + SectionTitle( + icon = Icons.Default.BatteryChargingFull, + title = stringResource(id = R.string.field_battery), + menu = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 8.dp) + ) { + batteryServiceData.batteryLevel?.let { batteryLevel -> + DynamicBatteryStatus(batteryLevel) + Text(text = "$batteryLevel %") + } + } + } + ) + } +} + +@Preview(showBackground = true) +@Composable +internal fun DynamicBatteryStatus(batteryLevel: Int = 40) { + val (batteryIcon: ImageVector, color: Color) = when { + batteryLevel > 95 -> { + Icons.Outlined.BatteryFull to nordicGreen + } // Full Battery + batteryLevel > 80 -> { + Icons.Outlined.Battery6Bar to nordicGreen + } + + batteryLevel > 70 -> { + Icons.Outlined.Battery5Bar to nordicGreen + } // Moderate Battery + batteryLevel > 55 -> { + Icons.Outlined.Battery4Bar to nordicGreen + } // Moderate Battery + + batteryLevel > 40 -> { + Icons.Outlined.Battery3Bar to nordicGreen + } // Moderate Battery + + batteryLevel > 25 -> { + Icons.Outlined.Battery2Bar to nordicGrass + } // Low Battery + + batteryLevel > 10 -> { + Icons.Outlined.Battery1Bar to MaterialTheme.colorScheme.error + } // Low Battery + + batteryLevel > 5 -> { + Icons.Outlined.Battery0Bar to MaterialTheme.colorScheme.error + } // Low Battery + + else -> { + Icons.Outlined.BatteryAlert to MaterialTheme.colorScheme.error + } // Critically Low Battery + } + + Icon( + imageVector = batteryIcon, + contentDescription = "Battery icon", + tint = color, + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/bps/BPSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/bps/BPSScreen.kt new file mode 100644 index 00000000..9e235cc9 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/bps/BPSScreen.kt @@ -0,0 +1,243 @@ +package no.nordicsemi.android.toolbox.profile.view.bps + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureData +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureType +import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.viewmodel.BPSViewModel +import no.nordicsemi.android.ui.view.FeatureSupported +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun BPSScreen() { + val bpsViewModel = hiltViewModel() + val serviceData by bpsViewModel.bpsServiceState.collectAsStateWithLifecycle() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection(modifier = Modifier.padding(bottom = 16.dp)) { + SectionTitle( + resId = R.drawable.ic_bps, + title = stringResource(id = R.string.bps_title), + modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp) + ) + serviceData.bloodPressureMeasurement?.let { + BloodPressureView(it) + } + serviceData.intermediateCuffPressure?.displayHeartRate()?.let { + HorizontalDivider() + SectionRow { + KeyValueColumn( + stringResource(id = R.string.bps_pulse), + it, + verticalSpacing = 4.dp, + modifier = Modifier.padding(start = 16.dp) + ) + } + } + + serviceData.bloodPressureFeature?.let { + HorizontalDivider() + Text( + "Blood pressure features", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.secondary + ) + BloodPressureFeatureView(it) + } + + if (serviceData.intermediateCuffPressure == null && + serviceData.bloodPressureMeasurement == null && + serviceData.bloodPressureFeature == null + ) { + WaitingForMeasurementView() + } + } + } +} + +@Composable +internal fun WaitingForMeasurementView() { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = stringResource(id = R.string.no_data_info_title), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + Text( + text = stringResource(id = R.string.no_data_info), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun WaitingForMeasurementViewPreview() { + WaitingForMeasurementView() +} + +@Composable +internal fun BloodPressureFeatureView(it: BloodPressureFeatureData) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + if (it.bodyMovementDetection) { + FeatureSupported(stringResource(id = R.string.body_movement_detected)) + } + if (it.cuffFitDetection) { + FeatureSupported(stringResource(id = R.string.cuff_fit_detected)) + } + if (it.irregularPulseDetection) { + FeatureSupported(stringResource(id = R.string.irregular_heart_rate_detected)) + } + if (it.pulseRateRangeDetection) { + FeatureSupported(stringResource(id = R.string.pulse_rate_detected)) + } + if (it.measurementPositionDetection) { + FeatureSupported(stringResource(id = R.string.measurement_position_detected)) + } + } +} + +@Composable +internal fun BloodPressureView(state: BloodPressureMeasurementData) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.bps_systolic), + state.displaySystolic() + ) + KeyValueColumnReverse( + stringResource(id = R.string.bps_diastolic), + state.displayDiastolic() + ) + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.bps_mean), + state.displayMeanArterialPressure() + ) + state.pulseRate?.let { + KeyValueColumnReverse( + "Heart rate", state.displayPulseRate() + ) + } + } + SectionRow { + state.calendar?.let { + stringResource(R.string.bps_timestamp, it) + }?.let { + KeyValueColumn( + "Date & Time", + it + ) + } + } + } + state.status?.let { + HorizontalDivider() + Text( + "BPM status", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.secondary + ) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + if (it.bodyMovementDetected) { + FeatureSupported(stringResource(id = R.string.body_movement_detected)) + + } + if (it.irregularPulseDetected) { + FeatureSupported(stringResource(id = R.string.irregular_heart_rate_detected)) + } + + if (it.cuffTooLose) { + FeatureSupported("Cuff Too Lose") + } + if (it.pulseRateExceedsUpperLimit) { + FeatureSupported(stringResource(id = R.string.pulse_rate_higher_limit)) + } + if (it.pulseRateInRange) { + FeatureSupported(stringResource(id = R.string.pulse_rate_detected)) + } + if (it.improperMeasurementPosition) { + FeatureSupported(stringResource(id = R.string.improper_measurement_position)) + } + + if (it.pulseRateIsLessThenLowerLimit) { + FeatureSupported(stringResource(id = R.string.pulse_rate_lower_limit)) + } + } + } +} + +@Composable +fun BloodPressureMeasurementData.displaySystolic(): String = + stringResource( + id = R.string.bps_blood_pressure, + systolic, displayUnit() + ) + +@Composable +fun BloodPressureMeasurementData.displayDiastolic(): String = + stringResource( + id = R.string.bps_blood_pressure, + diastolic, displayUnit() + ) + +@Composable +fun BloodPressureMeasurementData.displayMeanArterialPressure(): String = + stringResource( + id = R.string.bps_blood_pressure, + meanArterialPressure, displayUnit() + ) + +@Composable +fun IntermediateCuffPressureData.displayHeartRate(): String = pulseRate?.toString() + " bpm" + +@Composable +fun BloodPressureMeasurementData.displayPulseRate(): String = pulseRate?.toString() + " bpm" + +@Composable +fun BloodPressureMeasurementData.displayUnit(): String = + if (unit == BloodPressureType.UNIT_MMHG) "mmHg" else "kPA" diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMMapper.kt new file mode 100644 index 00000000..0f04a156 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMMapper.kt @@ -0,0 +1,19 @@ +package no.nordicsemi.android.toolbox.profile.view.cgms + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +internal fun CGMRecordWithSequenceNumber.formattedTime(): String { + val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) + return timeFormat.format(Date(timestamp)) +} + +@Composable +internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String { + return stringResource(id = R.string.cgms_value_unit, record.glucoseConcentration) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMScreen.kt new file mode 100644 index 00000000..41b89bcf --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cgms/CGMScreen.kt @@ -0,0 +1,350 @@ +package no.nordicsemi.android.toolbox.profile.view.cgms + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMRecord +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMStatus +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.toolbox.profile.data.CGMServiceData +import no.nordicsemi.android.toolbox.profile.view.gls.toDisplayString +import no.nordicsemi.android.toolbox.profile.viewmodel.CGMSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.CGMSViewModel +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle +import java.util.Calendar + +@Composable +internal fun CGMScreen() { + val cgmVm = hiltViewModel() + val serviceData by cgmVm.channelSoundingState.collectAsStateWithLifecycle() + var isWorkingModeClicked by rememberSaveable { mutableStateOf(false) } + val onClickEvent: (CGMSEvent) -> Unit = { cgmVm.onEvent(it) } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection { + SectionTitle( + resId = R.drawable.ic_cgm, + title = "Continuous glucose monitoring", + menu = { + WorkingModeDropDown( + cgmState = serviceData, + isWorkingModeSelected = isWorkingModeClicked, + onExpand = { isWorkingModeClicked = true }, + onDismiss = { isWorkingModeClicked = false }, + onClickEvent = { onClickEvent(it) } + ) + } + ) + } + RecordsView(serviceData) + } +} + +@Composable +private fun WorkingModeDropDown( + cgmState: CGMServiceData, + isWorkingModeSelected: Boolean, + onExpand: () -> Unit, + onDismiss: () -> Unit, + onClickEvent: (CGMSEvent) -> Unit +) { + if (cgmState.requestStatus == RequestStatus.PENDING) { + CircularProgressIndicator() + } else { + Column { + OutlinedButton(onClick = { onExpand() }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = if (cgmState.workingMode != null) { + cgmState.workingMode!!.toDisplayString() + } else { + "Request" + } + ) + Icon(Icons.Default.ArrowDropDown, contentDescription = "") + } + } + if (isWorkingModeSelected) + WorkingModeDialog( + cgmState = cgmState, + onDismiss = onDismiss, + ) { + onClickEvent(it) + onDismiss() + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WorkingModeDropDownPreview() { + WorkingModeDropDown(CGMServiceData(), false, {}, {}, {}) +} + +@Composable +private fun WorkingModeDialog( + cgmState: CGMServiceData, + onDismiss: () -> Unit, + onWorkingModeSelected: (CGMSEvent) -> Unit, +) { + val listState = rememberLazyListState() + val workingModeEntries = WorkingMode.entries.map { it } + val selectedIndex = workingModeEntries.indexOf(cgmState.workingMode) + + LaunchedEffect(selectedIndex) { + if (selectedIndex >= 0) { + listState.scrollToItem(selectedIndex) + } + } + + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Text( + text = "Request record", + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium + ) + HorizontalDivider() + LazyColumn( + state = listState + ) { + items(workingModeEntries.size) { index -> + val entry = workingModeEntries[index] + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { + onWorkingModeSelected( + CGMSEvent.OnWorkingModeSelected(entry) + ) + } + .padding(8.dp), + ) { + Text( + text = entry.toDisplayString(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + style = MaterialTheme.typography.titleLarge, + color = if ((cgmState.workingMode == entry) && cgmState.records.isNotEmpty()) { + MaterialTheme.colorScheme.primary + } else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + } + } + } +} + +@Composable +private fun RecordsView(state: CGMServiceData) { + ScreenSection { + if (state.records.isEmpty()) { + RecordsViewWithoutData() + } else { + RecordsViewWithData(state) + } + + } +} + +@Composable +private fun RecordsViewWithoutData() { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SectionTitle(icon = Icons.Default.Search, title = "No items") + + Text( + text = stringResource(R.string.cgms_no_records_info), + style = MaterialTheme.typography.bodyMedium + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun RecordsViewWithoutDataPreview() { + RecordsViewWithoutData() +} + +@Composable +private fun RecordsViewWithData(state: CGMServiceData) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + ) { + SectionTitle(resId = R.drawable.ic_records, title = "Records") + + val newRecord = when (state.workingMode) { + WorkingMode.ALL -> state.records + WorkingMode.LAST -> listOf(state.records.last()) + WorkingMode.FIRST -> listOf(state.records.first()) + null -> state.records + } + + // Max height for the scrollable section, adjust as needed (e.g. 300.dp) + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 500.dp) + .verticalScroll(rememberScrollState()) + ) { + Column { + newRecord.forEachIndexed { i, it -> + RecordItem(it) + if (i < newRecord.size - 1) { + HorizontalDivider() + } + } + } + } + } +} + +@Composable +private fun RecordItem(record: CGMRecordWithSequenceNumber) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .padding(8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + SectionRow { + KeyValueColumn( + value = stringResource(id = R.string.cgms_sequence_number), + key = record.sequenceNumber.toString() + ) + KeyValueColumnReverse( + "Glucose concentration", + record.glucoseConcentration(), + keyStyle = MaterialTheme.typography.titleMedium + ) + } + SectionRow { + KeyValueColumn( + value = "Date & Time", + key = record.formattedTime() + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun RecordsViewWithDataPreview() { + RecordsViewWithData( + state = CGMServiceData( + records = listOf( + CGMRecordWithSequenceNumber( + sequenceNumber = 12, + record = CGMRecord( + glucoseConcentration = 0.5f, + trend = 2.05f, + quality = 0.5f, + status = CGMStatus(2, 3, 5), + timeOffset = 2, + crcPresent = true + ), + timestamp = Calendar.TUESDAY.toLong() + ), + CGMRecordWithSequenceNumber( + sequenceNumber = 13, + record = CGMRecord( + glucoseConcentration = 0.5f, + trend = 2.05f, + quality = 0.5f, + status = CGMStatus(2, 3, 5), + timeOffset = 2, + crcPresent = true + ), + timestamp = Calendar.TUESDAY.toLong() + ) + ) + ) + + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/channelSounding/ChannelSoundingScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/channelSounding/ChannelSoundingScreen.kt new file mode 100644 index 00000000..1f4948cb --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/channelSounding/ChannelSoundingScreen.kt @@ -0,0 +1,62 @@ +package no.nordicsemi.android.toolbox.profile.view.channelSounding + +import android.content.pm.PackageManager +import android.os.Build +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SocialDistance +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import no.nordicsemi.android.permissions_ranging.RequestRangingPermission +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun ChannelSoundingScreen() { + RequestRangingPermission { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .fillMaxSize() + .padding(16.dp) + ) { + SectionTitle( + icon = Icons.Default.SocialDistance, + title = "Channel Sounding", + ) + val context = LocalContext.current + val rangingPermissionStatusMessage = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { + if (ContextCompat.checkSelfPermission( + context, + "android.permission.RANGING" + ) == PackageManager.PERMISSION_GRANTED + ) { + "Ranging permission is granted" + } else { + "Ranging permission is not granted" + } + } else { + "Channel Sounding Service is not available on this Android version." + } + + Box(contentAlignment = Alignment.Center) { + + Text( + text = rangingPermissionStatusMessage + ) + } + } + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCScreen.kt new file mode 100644 index 00000000..e68bfd88 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCScreen.kt @@ -0,0 +1,327 @@ +package no.nordicsemi.android.toolbox.profile.view.cscs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit +import no.nordicsemi.android.toolbox.profile.parser.csc.WheelSizes +import no.nordicsemi.android.toolbox.profile.parser.csc.WheelSizes.getWheelSizeByName +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.CSCServiceData +import no.nordicsemi.android.toolbox.profile.viewmodel.CSCEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.CSCViewModel +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow + +@Composable +internal fun CSCScreen() { + val csVM = hiltViewModel() + val onClickEvent: (CSCEvent) -> Unit = { csVM.onEvent(it) } + val serviceData by csVM.cscState.collectAsStateWithLifecycle() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "\uD83D\uDEB4" + " Cycling", + textAlign = TextAlign.Center, + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.weight(1f)) + CSCSettingView(serviceData, onClickEvent) + } + SensorsReadingView(state = serviceData, serviceData.speedUnit) + } + } +} + +@Composable +private fun CSCSettingView( + serviceData: CSCServiceData, + onClickEvent: (CSCEvent) -> Unit +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + var isWheelSizeClicked by rememberSaveable { mutableStateOf(false) } + var isDropdownExpanded by rememberSaveable { mutableStateOf(false) } + + WheelSizeDropDown( + state = serviceData, + isWheelSizeClicked = isWheelSizeClicked, + onExpand = { isWheelSizeClicked = true }, + onDismiss = { isWheelSizeClicked = false }, + onClickEvent = { onClickEvent(it) } + ) + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Speed unit settings", + modifier = Modifier + .clip(CircleShape) + .size(28.dp) + .clickable { isDropdownExpanded = true } + ) + + if (isDropdownExpanded) + CSCSpeedSettingsFilterDropdown( + serviceData, + onDismiss = { isDropdownExpanded = false }, + onClickEvent = { onClickEvent(it) } + ) + } +} + +@Composable +private fun WheelSizeDropDown( + state: CSCServiceData, + isWheelSizeClicked: Boolean, + onExpand: () -> Unit, + onDismiss: () -> Unit, + onClickEvent: (CSCEvent) -> Unit +) { + val wheelEntries = WheelSizes.data.map { it.name } + Column { + OutlinedButton(onClick = { onExpand() }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.csc_field_wheel_size), + ) + Icon(Icons.Default.ArrowDropDown, contentDescription = "") + } + } + if (isWheelSizeClicked) + WheelSizeDialog( + state = state, + wheelSizeEntries = wheelEntries, + onDismiss = onDismiss, + ) { + onClickEvent(CSCEvent.OnWheelSizeSelected(getWheelSizeByName(it))) + onDismiss() + } + } +} + +@Composable +private fun WheelSizeDialog( + state: CSCServiceData, + wheelSizeEntries: List, + onDismiss: () -> Unit, + onWheelSizeSelected: (String) -> Unit, +) { + val listState = rememberLazyListState() + val selectedIndex = wheelSizeEntries.indexOf(state.data.wheelSize.name) + + LaunchedEffect(selectedIndex) { + if (selectedIndex >= 0) { + listState.scrollToItem(selectedIndex) + } + } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { Text(text = stringResource(id = R.string.csc_dialog_title)) }, + text = { + LazyColumn( + state = listState + ) { + items(wheelSizeEntries.size) { index -> + val entry = wheelSizeEntries[index] + Row( + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .clickable { + onWheelSizeSelected(entry) + } + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = entry, + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.titleLarge, + color = if (state.data.wheelSize.name == entry) + MaterialTheme.colorScheme.primary else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + }, + confirmButton = { + TextButton(onClick = { onDismiss() }) { + Text( + text = stringResource(id = no.nordicsemi.android.ui.R.string.cancel), + ) + } + } + ) +} + +@Composable +private fun CSCSpeedSettingsFilterDropdown( + state: CSCServiceData, + onDismiss: () -> Unit, + onClickEvent: (CSCEvent) -> Unit +) { + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Text( + text = stringResource(R.string.csc_settings), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium + ) + HorizontalDivider() + Column( + modifier = Modifier + .padding(8.dp), + ) { + SpeedUnit.entries.forEach { entry -> + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { + onClickEvent(CSCEvent.OnSelectedSpeedUnitSelected(entry)) + onDismiss() + }, + ) { + Text( + text = entry.displayName, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + style = MaterialTheme.typography.titleLarge, + color = if (state.speedUnit == entry) + MaterialTheme.colorScheme.primary else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CSCSpeedSettingsFilterDropdownPreview() { + CSCSpeedSettingsFilterDropdown( + state = CSCServiceData(), + onDismiss = {}, + onClickEvent = {} + ) +} + +@Composable +private fun SensorsReadingView(state: CSCServiceData, speedUnit: SpeedUnit) { + val csc = state.data + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.csc_field_speed), + csc.displaySpeed(speedUnit) + ) + KeyValueColumnReverse( + stringResource(id = R.string.csc_field_cadence), + csc.displayCadence() + ) + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.csc_field_distance), + csc.displayDistance(speedUnit) + ) + KeyValueColumnReverse( + stringResource(id = R.string.csc_field_total_distance), + csc.displayTotalDistance(speedUnit) + ) + } + Row { + KeyValueColumn( + stringResource(id = R.string.csc_field_gear_ratio), + csc.displayGearRatio() + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun SensorsReadingViewPreview() { + SensorsReadingView(CSCServiceData(), SpeedUnit.KM_H) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCUiMapper.kt new file mode 100644 index 00000000..4d80e393 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/cscs/CSCUiMapper.kt @@ -0,0 +1,58 @@ +package no.nordicsemi.android.toolbox.profile.view.cscs + +import no.nordicsemi.android.toolbox.profile.parser.csc.CSCData +import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit +import java.util.Locale + +internal fun CSCData.speedWithSpeedUnit(speedUnit: SpeedUnit): Float { + return when (speedUnit) { + SpeedUnit.M_S -> speed + SpeedUnit.KM_H -> speed * 3.6f + SpeedUnit.MPH -> speed * 2.2369f + } +} + +internal fun CSCData.displaySpeed(speedUnit: SpeedUnit): String { + val speedWithUnit = speedWithSpeedUnit(speedUnit) + return when (speedUnit) { + SpeedUnit.M_S -> String.format(Locale.US, "%.1f m/s", speedWithUnit) + SpeedUnit.KM_H -> String.format(Locale.US, "%.1f km/h", speedWithUnit) + SpeedUnit.MPH -> String.format(Locale.US, "%.1f mph", speedWithUnit) + } +} + +internal fun CSCData.displayCadence(): String { + return String.format(Locale.US, "%.0f RPM", cadence) +} + +internal fun CSCData.displayDistance(speedUnit: SpeedUnit): String { + return when (speedUnit) { + SpeedUnit.M_S -> String.format(Locale.US, "%.0f m", distance) + SpeedUnit.KM_H -> String.format(Locale.US, "%.0f m", distance) + SpeedUnit.MPH -> String.format(Locale.US, "%.0f yd", distance.toYards()) + } +} + +internal fun CSCData.displayTotalDistance(speedUnit: SpeedUnit): String { + return when (speedUnit) { + SpeedUnit.M_S -> String.format(Locale.US, "%.2f m", totalDistance) + SpeedUnit.KM_H -> String.format(Locale.US, "%.2f km", totalDistance.toKilometers()) + SpeedUnit.MPH -> String.format(Locale.US, "%.2f mile", totalDistance.toMiles()) + } +} + +internal fun CSCData.displayGearRatio(): String { + return String.format(Locale.US, "%.1f", gearRatio) +} + +internal fun Float.toYards(): Float { + return this * 1.0936f +} + +private fun Float.toKilometers(): Float { + return this / 1000f +} + +private fun Float.toMiles(): Float { + return this * 0.0006f +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthAndElevationSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthAndElevationSection.kt new file mode 100644 index 00000000..5576f0fe --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthAndElevationSection.kt @@ -0,0 +1,106 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.data.directionFinder.displayAzimuth +import no.nordicsemi.android.toolbox.profile.data.directionFinder.elevationValue +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun AzimuthAndElevationSection(data: SensorData, range: Range) { + ScreenSection { + SectionTitle( + R.drawable.ic_azimuth, + stringResource(id = R.string.azimuth_section) + ) + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box { + Image( + painter = painterResource(id = R.drawable.ic_azimuth), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surface), + contentDescription = null, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = CircleShape + ) + .size(200.dp) + ) + AzimuthView(data, range) + } + data.displayAzimuth()?.let { + Text( + text = "Direction relative to North: $it", + style = MaterialTheme.typography.titleLarge, + ) + } + } + + SectionTitle( + R.drawable.ic_elevation, + stringResource(id = R.string.elevation_section) + ) + Box { + ElevationView(value = data.elevationValue()!!, data) + } + + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AzimuthAndElevationSectionPreview() { + val sensorData = SensorData( + azimuth = SensorValue( + values = listOf( + AzimuthMeasurementData( + azimuth = 50, + address = PeripheralBluetoothAddress.TEST + ) + ) + ), + elevation = SensorValue( + values = listOf( + ElevationMeasurementData( + address = PeripheralBluetoothAddress.TEST, + elevation = 30 + ) + ) + ) + ) + AzimuthAndElevationSection(sensorData, Range(0, 50)) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthSection.kt new file mode 100644 index 00000000..e00957f6 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthSection.kt @@ -0,0 +1,84 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.data.directionFinder.displayAzimuth +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun AzimuthSection(data: SensorData, distanceRange: Range) { + ScreenSection { + SectionTitle( + resId = R.drawable.ic_azimuth, stringResource(id = R.string.azimuth_section) + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_azimuth), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.surface), + contentDescription = null, + modifier = Modifier + .background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = CircleShape + ) + .height(200.dp) + .width(200.dp) + ) + AzimuthView(data, distanceRange) + } + + data.displayAzimuth()?.let { + Box( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = it, + style = MaterialTheme.typography.titleLarge, + ) + } + } + } +} + +@Preview +@Composable +private fun AzimuthSectionPreview() { + val sensorData = SensorData( + azimuth = SensorValue( + values = listOf( + AzimuthMeasurementData( + azimuth = 20, + address = PeripheralBluetoothAddress.TEST + ) + ) + ) + ) + AzimuthSection(sensorData, Range(0, 50)) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthView.kt new file mode 100644 index 00000000..38a5856b --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/AzimuthView.kt @@ -0,0 +1,266 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.animation.core.InfiniteTransition +import androidx.compose.animation.core.LinearOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.isActive +import kotlinx.coroutines.yield +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.data.directionFinder.azimuthValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.distanceValue +import no.nordicsemi.android.ui.view.CircleTransitionState +import no.nordicsemi.android.ui.view.createCircleTransition + +@Composable +internal fun AzimuthView( + sensorData: SensorData, + range: Range +) { + val azimuthValue = sensorData.azimuthValue() ?: return + val distance = sensorData.distanceValue() + + val radius = 100.dp + val duration = 1000 + + val isInAccessibilityMode = rememberSaveable { mutableStateOf(false) } + val transition = createCircleTransition(isInAccessibilityMode.value, duration) + val rotationValue = remember { mutableFloatStateOf(0f) } + + LaunchedEffect(Unit) { + var lastFrame = 0L + while (isActive) { + val nextFrame = awaitFrame() / 100_000L + if (lastFrame != 0L) { + val period = nextFrame - lastFrame + rotationValue.floatValue += period / 1000f + } + lastFrame = nextFrame + yield() //TOdo: verify this. + } + } + + Box { + // Render the main canvas + RenderAzimuthCanvas( + radius = radius, + circleBorderColor = MaterialTheme.colorScheme.secondary, + transition = transition, + distance = distance, + isClose = isClose(sensorData, range), + range = range, + duration = duration, + ) + + // Render arrow if not close + if (!isClose(sensorData, range) || distance == null) { + RenderArrow( + azimuthValue = azimuthValue, + dotColor = transition.dotColor.value, + modifier = Modifier.align(Alignment.TopCenter) + ) + } + } +} + +@Composable +private fun RenderAzimuthCanvas( + radius: Dp, + circleBorderColor: Color, + transition: CircleTransitionState, + distance: Int?, + isClose: Boolean, + range: Range, + duration: Int, +) { + val infiniteTransition = rememberInfiniteTransition(label = "InfiniteTransition") + val scale = infiniteTransition.createInfiniteFloatAnimation(1f, 1.25f, duration) + val alphaColor = infiniteTransition.createInfiniteFloatAnimation(1f, 0f, duration) + val rotationAnimatedValue = infiniteTransition.createInfiniteFloatAnimation(0f, 360f, 10_000) + + val progressWidth = calculateProgressWidth(range, distance) + val rotationOffset = 100.dp * (1f - progressWidth) + val alphaColorValue = transition.dotColor.value.copy(alpha = alphaColor) + + Canvas( + modifier = Modifier + .size(radius * 2) + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = {}, + onLongClick = { transition.toggleAccessibilityMode() } + ) + ) { + drawOuterCircles( + radius, + circleBorderColor, + transition.circleWidth.value, + transition.circleColor.value + ) + + if (distance != null) { + if (isClose) { + drawCloseDot(radius, transition.dotColor.value, alphaColorValue, scale) + } else { + drawRotatingDots( + radius, + rotationAnimatedValue, + rotationOffset, + transition.dotColor.value, + transition.dotRadius.value + ) + } + } + } +} + +private fun DrawScope.drawOuterCircles( + radius: Dp, + borderColor: Color, + width: Dp, + fillColor: Color +) { + drawCircle( + color = borderColor, + radius = radius.toPx(), + style = Stroke(width = width.toPx() * 2 + 1) + ) + drawCircle( + color = fillColor, + radius = radius.toPx(), + style = Stroke(width = width.toPx() * 2) + ) +} + +private fun DrawScope.drawCloseDot( + radius: Dp, + dotColor: Color, + alphaColorValue: Color, + scale: Float +) { + val center = center + drawCircle( + color = dotColor, + radius = (radius / 2).toPx(), + center = center + ) + drawCircle( + color = alphaColorValue, + radius = (radius / 2 * scale).toPx(), + center = center + ) +} + +private fun DrawScope.drawRotatingDots( + radius: Dp, + rotationValue: Float, + offset: Dp, + dotColor: Color, + dotRadius: Dp +) { + rotate(rotationValue) { + repeat(7) { i -> + rotate(360f / 7 * i) { + drawCircle( + color = dotColor, + radius = dotRadius.toPx(), + center = Offset(offset.toPx(), radius.toPx()) + ) + } + } + } +} + +@Composable +private fun RenderArrow(azimuthValue: Int, dotColor: Color, modifier: Modifier) { + Image( + painter = painterResource(id = R.drawable.ic_arrow), + contentDescription = null, + colorFilter = ColorFilter.tint(dotColor), + modifier = Modifier + .rotate(azimuthValue.toFloat()) + .then(modifier) + ) +} + +private fun calculateProgressWidth(range: Range, distance: Int?): Float { + return when { + distance == null -> 0f + distance <= range.from -> 0f + distance >= range.to -> 1f + else -> (distance.toFloat() - range.from) / (range.to - range.from) + } +} + +@Composable +private fun InfiniteTransition.createInfiniteFloatAnimation( + initialValue: Float, + targetValue: Float, + duration: Int +): Float { + return animateFloat( + initialValue = initialValue, + targetValue = targetValue, + animationSpec = infiniteRepeatable( + animation = tween(duration, easing = LinearOutSlowInEasing), + repeatMode = RepeatMode.Restart + ), label = "Infinite Animation" + ).value +} + +private fun isClose(sensorData: SensorData, range: Range): Boolean { + val validatedValue = sensorData.distanceValue()?.coerceIn(range.from, range.to) ?: 0 + return validatedValue <= range.from || (validatedValue - range.from) < 10 +} + +@Preview(showBackground = true) +@Composable +private fun AzimuthViewPreview() { + val sensorData = SensorData( + azimuth = SensorValue( + values = listOf( + AzimuthMeasurementData( + azimuth = 20, + address = PeripheralBluetoothAddress.TEST + ) + ) + ) + ) + AzimuthView(sensorData, Range(0, 50)) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ControlView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ControlView.kt new file mode 100644 index 00000000..457f2201 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ControlView.kt @@ -0,0 +1,171 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.DFSServiceData +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol +import no.nordicsemi.android.toolbox.profile.viewmodel.DFSEvent + +@Composable +internal fun ControlView( + viewEntity: DFSServiceData, + sensorData: SensorData, + onEvent: (DFSEvent) -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + when { + !viewEntity.isDistanceAvailabilityChecked() -> { + DistanceCheckView { + onEvent(DFSEvent.OnAvailableDistanceModeRequest) + } + } + + !viewEntity.isDistanceAvailable() -> { + DistanceNotAvailableView() + } + + viewEntity.isDoubleModeAvailable() -> { + CurrentModeView( + distanceMode = sensorData.distanceMode, + onCheckMode = { onEvent(DFSEvent.OnCheckDistanceModeRequest) }, + onSwitchMode = { newMode -> onEvent(DFSEvent.OnDistanceModeSelected(newMode)) } + ) + } + + viewEntity.ddfFeature?.isMcpdAvailable == true -> { + SingleModeAvailableView( + isMcpdAvailable = true, + ) + } + + viewEntity.ddfFeature?.isRttAvailable == true -> { + SingleModeAvailableView( + isRttAvailable = true, + ) + } + } + } +} + +@Composable +private fun DistanceCheckView(onCheckAvailability: () -> Unit) { + Text(stringResource(id = R.string.check_distance_mode)) + Button(onClick = onCheckAvailability) { + Text(stringResource(id = R.string.check_availability)) + } +} + +@Preview(showBackground = true) +@Composable +private fun DistanceCheckViewPreview() { + DistanceCheckView {} +} + +@Composable +private fun DistanceNotAvailableView() { + Text(stringResource(id = R.string.distance_not_available)) +} + +@Composable +private fun CurrentModeView( + distanceMode: DistanceMode?, + onCheckMode: () -> Unit, + onSwitchMode: (DistanceMode) -> Unit +) { + if (distanceMode == null) { + Button(onClick = onCheckMode) { + Text(stringResource(id = R.string.check_mode)) + } + } else { + Box( + modifier = Modifier.clip(RoundedCornerShape(8.dp)), + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + DistanceMode.entries.forEachIndexed { index, it -> + val selected = it == distanceMode + val clip = if (selected) RoundedCornerShape(8.dp) else RoundedCornerShape(0.dp) + val (color, textColor) = if (selected) { + MaterialTheme.colorScheme.primary to MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.surfaceContainer to MaterialTheme.colorScheme.onSurface + } + + Box( + modifier = Modifier + .weight(1f) + .clip(clip) + .background(color = color) + .clickable { onSwitchMode(it) }, + contentAlignment = Alignment.Center, + ) { + Text( + it.toString(), + modifier = Modifier.padding(8.dp), + color = textColor, + ) + } + if ((index < MacroEol.entries.size - 1) && !selected) + VerticalDivider( + modifier = Modifier + .height(IntrinsicSize.Max) + .background(MaterialTheme.colorScheme.onSurface) + ) + } + } + } + } +} + +@Composable +private fun SingleModeAvailableView( + isMcpdAvailable: Boolean? = null, + isRttAvailable: Boolean? = null +) { + val messageId = when { + isMcpdAvailable == true -> R.string.only_mcpd_available + isRttAvailable == true -> R.string.only_rtt_available + else -> null + } + messageId?.let { Text(stringResource(id = it)) } +} + +@Preview(showBackground = true) +@Composable +private fun SingleModeAvailableViewPreview() { + SingleModeAvailableView(false, true) +} + + +@Preview(showBackground = true) +@Composable +private fun ControlViewPreview() { + CurrentModeView(DistanceMode.MCPD, {}) { } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DFSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DFSScreen.kt new file mode 100644 index 00000000..cb7f9902 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DFSScreen.kt @@ -0,0 +1,77 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.data.directionFinder.azimuthValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.distanceValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.elevationValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.isDistanceSettingsAvailable +import no.nordicsemi.android.toolbox.profile.viewmodel.DFSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.DirectionFinderViewModel + +@Composable +internal fun DFSScreen() { + val dfsVM = hiltViewModel() + val onClick: (DFSEvent) -> Unit = { dfsVM.onEvent(it) } + val serviceData by dfsVM.dfsState.collectAsStateWithLifecycle() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + when (serviceData.requestStatus) { + RequestStatus.PENDING -> CircularProgressIndicator() + RequestStatus.SUCCESS -> { + SectionBluetoothDeviceComponent( + serviceData, + selectedDevice = serviceData.selectedDevice, + ) { onClick(it) } + + if (serviceData.selectedDevice != null) { + val data = serviceData.data[serviceData.selectedDevice] + val isAzimuthAndElevationDataAvailable = + (data?.azimuthValue() != null) && (data.elevationValue() != null) + + if (data != null) { + data.distanceValue()?.let { + DistanceSection(it, serviceData.distanceRange, onClick) + } + when { + isAzimuthAndElevationDataAvailable -> AzimuthAndElevationSection( + data, + serviceData.distanceRange + ) + + !isAzimuthAndElevationDataAvailable && (data.azimuth != null) -> AzimuthSection( + data, + serviceData.distanceRange + ) + + !isAzimuthAndElevationDataAvailable && data.elevation != null -> ElevationSection( + data + ) + } + if (data.isDistanceSettingsAvailable()) { + MeasurementDetailsView(serviceData, data) + } + } + } + } + + else -> { + CircularProgressIndicator() + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceSection.kt new file mode 100644 index 00000000..0aeeb73a --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceSection.kt @@ -0,0 +1,58 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.viewmodel.DFSEvent +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun DistanceSection( + distanceValue: Int, + range: Range, + onClick: (DFSEvent) -> Unit, +) { + ScreenSection { + SectionTitle( + R.drawable.ic_distance, + stringResource(id = R.string.distance_section) + ) + DistanceView(value = distanceValue, range = range) + + Box( + modifier = Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "$distanceValue dm", + style = MaterialTheme.typography.titleLarge, + ) + } + Column { + Text( + stringResource(R.string.distance_range), + style = MaterialTheme.typography.titleSmall + ) + RangeSlider(range) { + onClick(DFSEvent.OnRangeChangedEvent(it)) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun DistanceSectionPreview() { + DistanceSection(15, Range(0, 50)) {} +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceView.kt new file mode 100644 index 00000000..39516d8e --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceView.kt @@ -0,0 +1,46 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import java.util.Locale + +@Composable +internal fun DistanceView(value: Int, range: Range) { + Column { + DistanceChartView(value = value, range = range) + + Spacer(modifier = Modifier.padding(4.dp)) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = String.format(Locale.US, "%d dm", range.from)) + + val diff = range.to - range.from + val part = (diff / 4) + if (part > 0) { + Text(text = String.format(Locale.US, "% ddm", range.from + part)) + Text(text = String.format(Locale.US, "%d dm", range.from + 2 * part)) + } + + Text(text = String.format(Locale.US, "%d dm", range.to)) + } + } +} + +@Preview +@Composable +private fun DistanceViewPreview() { + DistanceView(20, Range(0, 50)) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceViewChart.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceViewChart.kt new file mode 100644 index 00000000..94ad87b4 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/DistanceViewChart.kt @@ -0,0 +1,79 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.ui.view.createLinearTransition + +@Composable +internal fun DistanceChartView(value: Int, range: Range) { + val duration = 1000 + val isInAccessibilityMode = rememberSaveable { mutableStateOf(false) } + val transition = createLinearTransition(isInAccessibilityMode.value, duration) + + BoxWithConstraints { + Canvas( + modifier = Modifier + .height(transition.height.value) + .fillMaxWidth() + .border( + transition.border.value, + transition.color.value, + RoundedCornerShape(transition.radius.value) + ) + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = { + }, + onLongClick = { isInAccessibilityMode.value = !isInAccessibilityMode.value } + ) + ) { + drawRoundRect( + color = transition.inactiveColor.value, + size = Size(maxWidth.toPx(), transition.height.value.toPx()), + cornerRadius = CornerRadius( + transition.radius.value.toPx(), + transition.radius.value.toPx() + ) + ) + + val min = range.from + val max = range.to + val progressWidth = when { + value <= min -> 0f + value >= max -> 1f + else -> (value - min).toFloat() / (max - min).toFloat() + } + + drawRoundRect( + color = transition.color.value, + size = Size(progressWidth * size.width, transition.height.value.toPx()), + cornerRadius = CornerRadius( + transition.radius.value.toPx(), + transition.radius.value.toPx() + ) + ) + } + } +} + +@Preview +@Composable +private fun DistanceChartViewPreview() { + DistanceChartView(20, Range(0, 50)) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationSection.kt new file mode 100644 index 00000000..7ba03962 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationSection.kt @@ -0,0 +1,52 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.medianValue +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun ElevationSection(data: SensorData) { + ScreenSection { + SectionTitle( + resId = R.drawable.ic_elevation, stringResource(id = R.string.elevation_section) + ) + + Row( + modifier = Modifier.padding(end = 50.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + data.elevation.medianValue { it.elevation }?.let { ElevationView(it, data) } + } + } +} + +@Preview +@Composable +private fun ElevationSectionPreview() { + val sensorData = SensorData( + elevation = SensorValue( + values = listOf( + ElevationMeasurementData( + address = PeripheralBluetoothAddress.TEST, + elevation = 30 + ) + ) + ) + ) + ElevationSection(sensorData) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationView.kt new file mode 100644 index 00000000..91e62c18 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/ElevationView.kt @@ -0,0 +1,175 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.displayElevation +import no.nordicsemi.android.ui.view.CircleTransitionState +import no.nordicsemi.android.ui.view.createCircleTransition + +@Composable +internal fun ElevationView( + value: Int, + data: SensorData +) { + val duration = 1000 + val radius = 100.dp + val isInAccessibilityMode = rememberSaveable { mutableStateOf(false) } + val transition = createCircleTransition(isInAccessibilityMode.value, duration) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(8.dp) + ) { + BoxWithConstraints( + modifier = Modifier.padding(8.dp) + ) { + ElevationLabels(Modifier.padding(8.dp)) + Box( + modifier = Modifier + .size(radius * 2) + .align(Alignment.Center) // force centering + ) { + ElevationCanvas( + radius = radius, + circleBorderColor = MaterialTheme.colorScheme.secondary, + transition = transition, + value = value + ) { + isInAccessibilityMode.value = !isInAccessibilityMode.value + } + } + } + data.displayElevation()?.let { + Box( + modifier = Modifier, + contentAlignment = Alignment.BottomCenter + ) { + Text( + text = "Tilt Angle: $it", + style = MaterialTheme.typography.titleLarge, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ElevationViewPreview() { + val value = 15 // Example elevation value + val sensorData = SensorData( + elevation = SensorValue( + values = listOf( + ElevationMeasurementData( + address = PeripheralBluetoothAddress.TEST, + elevation = 30 + ) + ) + ) + ) + ElevationView(value = value, sensorData) +} + +@Composable +private fun BoxScope.ElevationLabels( + modifier: Modifier = Modifier +) { + Text( + text = stringResource(id = R.string.elevation_max), + modifier = modifier.align(Alignment.TopCenter) + ) + Text( + text = stringResource(id = R.string.elevation_medium), + modifier = modifier.align(Alignment.CenterEnd) + ) + Text( + text = stringResource(id = R.string.elevation_min), + modifier = modifier.align(Alignment.BottomCenter) + ) +} + +@Composable +private fun ElevationCanvas( + radius: Dp, + circleBorderColor: Color, + transition: CircleTransitionState, + value: Int, + onLongClick: () -> Unit +) { + Canvas( + modifier = Modifier + .requiredSize(radius * 2) + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onClick = {}, + onLongClick = onLongClick + ) + ) { + drawArc( + color = circleBorderColor, + startAngle = -90f, + sweepAngle = 180f, + useCenter = false, + style = Stroke( + width = (transition.circleWidth.value * 2 + 1.dp).toPx(), + join = StrokeJoin.Round, + cap = StrokeCap.Round + ) + ) + + drawArc( + color = transition.circleColor.value, + startAngle = -90f, + sweepAngle = 180f, + useCenter = false, + style = Stroke( + width = (transition.circleWidth.value * 2).toPx(), + join = StrokeJoin.Round, + cap = StrokeCap.Round + ) + ) + + rotate(90f - value.toFloat()) { + drawCircle( + color = transition.dotColor.value, + radius = transition.dotRadius.value.toPx(), + center = Offset(x = size.width / 2, y = 0f) + ) + } + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/LinearDataView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/LinearDataView.kt new file mode 100644 index 00000000..cf0cb59c --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/LinearDataView.kt @@ -0,0 +1,147 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.data.directionFinder.bestEffortValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.ifftValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.isMcpdSectionAvailable +import no.nordicsemi.android.toolbox.profile.data.directionFinder.phaseSlopeValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.rssiValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.rttValue + +@Composable +internal fun LinearDataView( + data: SensorData, + range: Range +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + data.rttValue()?.let { + Text(stringResource(id = R.string.rtt), + style = MaterialTheme.typography.titleSmall) + + LinearDataItemView(name = stringResource(id = R.string.rtt), range, it) + } + + if (data.isMcpdSectionAvailable()) { + Text( + stringResource(id = R.string.mcpd), + style = MaterialTheme.typography.titleSmall + ) + } + + data.ifftValue()?.let { + LinearDataItemView(name = stringResource(id = R.string.ifft_label), range, it) + } + + data.phaseSlopeValue()?.let { + LinearDataItemView(name = stringResource(id = R.string.phase_label), range, it) + } + + data.rssiValue()?.let { + LinearDataItemView(name = stringResource(id = R.string.rssi_label), range, it) + } + + data.bestEffortValue()?.let { + LinearDataItemView(name = stringResource(id = R.string.best_label), range, it) + } + + Spacer(modifier = Modifier.height(8.dp)) + + data.ifftValue()?.let { + IfftFullForm() + } + } +} + +@Composable +private fun LinearDataItemView(name: String, range: Range, item: Int) { + val labelWidth = 48.dp + + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Column { + Text( + modifier = Modifier.width(labelWidth), + text = name, + style = MaterialTheme.typography.labelSmall + ) + Text( + text = "($item dm)", + style = MaterialTheme.typography.labelMedium, + ) + } + DistanceChartView(value = item, range = range) + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(start = labelWidth) + ) { + Text( + text = stringResource(R.string.dm_value, range.from), + style = MaterialTheme.typography.labelSmall + ) + + val diff = range.to - range.from + val part = (diff / 4) + if (part > 0) { + Text( + text = stringResource(R.string.dm_value, range.from + part), + style = MaterialTheme.typography.labelSmall + ) + Text( + text = stringResource(R.string.dm_value, range.from + 2 * part), + style = MaterialTheme.typography.labelSmall + ) + } + + Text( + text = stringResource(R.string.dm_value, range.to), + style = MaterialTheme.typography.labelSmall + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LinearDataItemViewPreview() { + LinearDataItemView( + name = "RSSI", + range = Range(0, 50), + item = 49 + ) +} + +@Preview(showBackground = true) +@Composable +private fun IfftFullForm(){ + Text( + text = "ifft - Inverse Fast Fourier Transform", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth().alpha(0.5f) + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/MeasurementDetailsView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/MeasurementDetailsView.kt new file mode 100644 index 00000000..5d7b226e --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/MeasurementDetailsView.kt @@ -0,0 +1,33 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.DFSServiceData +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.ui.view.ScreenSection + +@Composable +internal fun MeasurementDetailsView( + serviceData: DFSServiceData, + data: SensorData +) { + ScreenSection { + Text( + text = stringResource(R.string.distance_settings), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + ) + LinearDataView(data, serviceData.distanceRange) + } +} + +@Preview +@Composable +private fun MeasurementDetailsViewPreview() { + MeasurementDetailsView(DFSServiceData(), SensorData()) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/RangeSlider.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/RangeSlider.kt new file mode 100644 index 00000000..36405472 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/RangeSlider.kt @@ -0,0 +1,81 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RangeSlider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import java.util.Locale + +@Composable +internal fun RangeSlider(range: Range, onChange: (Range) -> Unit) { + Column { + RangeSliderView(range, onChange) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.from), + style = MaterialTheme.typography.bodySmall + ) + Text( + text = String.format(Locale.US, "%d", range.from), + style = MaterialTheme.typography.titleSmall + ) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = stringResource(R.string.to), style = MaterialTheme.typography.bodySmall) + Text( + text = String.format(Locale.US, "%d", range.to), + style = MaterialTheme.typography.titleSmall + ) + } + } + } +} + +@Composable +fun RangeSliderView( + range: Range, + onChange: (Range) -> Unit, + valueRange: ClosedFloatingPointRange = 0f..500f, + step: Int = 1 +) { + val currentOnChange = rememberUpdatedState(onChange) + val sliderValues = remember(range) { range.toFloatRange() } + + RangeSlider( + modifier = Modifier.fillMaxWidth(), + value = sliderValues, + onValueChange = { newValues -> + currentOnChange.value( + Range(newValues.start.toInt(), newValues.endInclusive.toInt()) + ) + }, + valueRange = valueRange, + steps = ((valueRange.endInclusive - valueRange.start) / step).toInt() - 1, + ) +} + +private fun Range.toFloatRange(): ClosedFloatingPointRange = + from.toFloat()..to.toFloat() + +@Preview +@Composable +private fun RangeSliderViewPreview() { + RangeSlider(Range(0, 50)) {} +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/SectionBluetoothDeviceComponent.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/SectionBluetoothDeviceComponent.kt new file mode 100644 index 00000000..db09198e --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/directionFinder/SectionBluetoothDeviceComponent.kt @@ -0,0 +1,276 @@ +package no.nordicsemi.android.toolbox.profile.view.directionFinder + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.DFSServiceData +import no.nordicsemi.android.toolbox.profile.viewmodel.DFSEvent +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.internal.EmptyView + +@Composable +internal fun SectionBluetoothDeviceComponent( + data: DFSServiceData, + selectedDevice: PeripheralBluetoothAddress?, + onEvent: (DFSEvent) -> Unit +) { + val devices = data.data.keys.toList() + .filter { it.address.lowercase() != PeripheralBluetoothAddress.TEST.address.lowercase() } // ignore case with TEST address + + when { + selectedDevice == null && devices.isNotEmpty() -> ScreenSection { + NotSelectedView(devices) { + onEvent(DFSEvent.OnBluetoothDeviceSelected(it)) + } + } + + selectedDevice == null -> { + EmptyView( + R.string.device_no_devices, + R.string.device_no_devices_hint, + ) + } + + else -> { + SelectedDevices(selectedDevice, devices, onEvent) + } + } + +} + +@Composable +private fun SelectedDevices( + selectedDevice: PeripheralBluetoothAddress, + devices: List, + onEvent: (DFSEvent) -> Unit +) { + var showDropdownMenu by rememberSaveable { mutableStateOf(false) } + var width by rememberSaveable { mutableIntStateOf(0) } + val icon = if (showDropdownMenu) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown + + OutlinedCard( + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { if (devices.size > 1) showDropdownMenu = true } + .onSizeChanged { width = it.width } + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.ic_elevation), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary), + modifier = Modifier.size(28.dp) + ) + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = stringResource(id = R.string.selected_device), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = selectedDevice.address.uppercase(), + style = MaterialTheme.typography.titleSmall + ) + Text( + text = "(${selectedDevice.type.name})", + style = MaterialTheme.typography.labelSmall + ) + } + } + + // Don't show icon if only one device is available + if (devices.size > 1) { + Spacer(Modifier.weight(1f)) + Icon(icon, contentDescription = "") + } + } + } + + DropdownMenu( + expanded = showDropdownMenu, + onDismissRequest = { showDropdownMenu = false }, + modifier = Modifier.width(with(LocalDensity.current) { width.toDp() }), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(id = R.string.devices), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(horizontal = 16.dp) + ) + HorizontalDivider() + + devices.forEach { + Text( + text = it.address, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth() + .clickable { + onEvent(DFSEvent.OnBluetoothDeviceSelected(it)) + showDropdownMenu = false + } + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MeasuredDevicesPreview() { + SelectedDevices( + PeripheralBluetoothAddress.TEST, + devices = listOf( + PeripheralBluetoothAddress.TEST, + PeripheralBluetoothAddress.TEST, + PeripheralBluetoothAddress.TEST + ) + ) {} +} + +@Preview(showBackground = true) +@Composable +private fun SectionBluetoothDeviceComponentPreview() { + SectionBluetoothDeviceComponent( + DFSServiceData(), null + ) {} +} + +@Composable +internal fun NotSelectedView( + devices: List, + onClick: (PeripheralBluetoothAddress) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(id = R.string.devices), + style = MaterialTheme.typography.titleLarge + ) + + Column { + devices.forEach { address -> + BluetoothDeviceView( + device = address, + title = stringResource(id = R.string.device_address), + modifier = Modifier.fillMaxWidth() + ) { onClick(address) } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun NotSelectedViewPreview() { + NotSelectedView( + listOf( + PeripheralBluetoothAddress.TEST, + PeripheralBluetoothAddress.TEST, + PeripheralBluetoothAddress.TEST + ) + ) { } +} + +@Preview(showBackground = true) +@Composable +private fun EmptyItemPreview() { + EmptyView(R.string.device_no_devices, R.string.device_no_devices_hint) +} + +@Composable +internal fun BluetoothDeviceView( + device: PeripheralBluetoothAddress, + title: String, + modifier: Modifier = Modifier, + onClick: (PeripheralBluetoothAddress) -> Unit +) { + Row( + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable { onClick(device) } + .padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_elevation), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.secondary), + modifier = Modifier + .size(28.dp) + ) + + Column(modifier) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + Text(text = device.address, style = MaterialTheme.typography.bodyMedium) + } + + } +} + +@Preview(showBackground = true) +@Composable +private fun BluetoothDeviceViewPreview() { + BluetoothDeviceView( + PeripheralBluetoothAddress.TEST, + "Bluetooth Device - Test" + ) {} +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSScreen.kt new file mode 100644 index 00000000..321d3471 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSScreen.kt @@ -0,0 +1,379 @@ +package no.nordicsemi.android.toolbox.profile.view.gls + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Carbohydrate +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ConcentrationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Health +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Meal +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Medication +import no.nordicsemi.android.toolbox.profile.parser.gls.data.MedicationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordType +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Tester +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.GLSServiceData +import no.nordicsemi.android.toolbox.profile.view.gls.details.GLSDetails +import no.nordicsemi.android.toolbox.profile.viewmodel.GLSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.GLSEvent.OnWorkingModeSelected +import no.nordicsemi.android.toolbox.profile.viewmodel.GLSViewModel +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle +import java.util.Calendar + +@Composable +internal fun GLSScreen() { + val glsViewModel = hiltViewModel() + val glsServiceData by glsViewModel.glsState.collectAsStateWithLifecycle() + val onClickEvent: (GLSEvent) -> Unit = { glsViewModel.onEvent(it) } + var isWorkingModeClicked by rememberSaveable { mutableStateOf(false) } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection { + SectionTitle( + resId = R.drawable.ic_gls, + title = "Glucose level", + menu = { + WorkingModeDropDown( + glsState = glsServiceData, + isWorkingModeSelected = isWorkingModeClicked, + onExpand = { isWorkingModeClicked = true }, + onDismiss = { isWorkingModeClicked = false }, + onClickEvent = { onClickEvent(it) } + ) + } + ) + } + RecordsView(glsServiceData) + } +} + +@Composable +private fun WorkingModeDropDown( + glsState: GLSServiceData, + isWorkingModeSelected: Boolean, + onExpand: () -> Unit, + onDismiss: () -> Unit, + onClickEvent: (GLSEvent) -> Unit +) { + if (glsState.requestStatus == RequestStatus.PENDING) { + CircularProgressIndicator() + } else { + Column { + OutlinedButton(onClick = { onExpand() }) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = if (glsState.workingMode != null) glsState.workingMode!!.toDisplayString() else "Request") + Icon(Icons.Default.ArrowDropDown, contentDescription = "") + } + } + if (isWorkingModeSelected) + WorkingModeDialog( + glsState = glsState, + onDismiss = onDismiss, + ) { + onClickEvent(it) + onDismiss() + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WorkingModeDropDownPreview() { + WorkingModeDropDown(GLSServiceData(), false, {}, {}, {}) +} + +@Composable +private fun WorkingModeDialog( + glsState: GLSServiceData, + onDismiss: () -> Unit, + onWorkingModeSelected: (GLSEvent) -> Unit, +) { + val listState = rememberLazyListState() + val workingModeEntries = WorkingMode.entries.map { it } + val selectedIndex = workingModeEntries.indexOf(glsState.workingMode) + + LaunchedEffect(selectedIndex) { + if (selectedIndex >= 0) { + listState.scrollToItem(selectedIndex) + } + } + + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Text( + text = "Request record", + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium + ) + HorizontalDivider() + LazyColumn( + state = listState + ) { + items(workingModeEntries.size) { index -> + val entry = workingModeEntries[index] + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { + onWorkingModeSelected(OnWorkingModeSelected(entry)) + } + .padding(8.dp), + ) { + Text( + text = entry.toDisplayString(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + style = MaterialTheme.typography.titleLarge, + color = if ((glsState.workingMode == entry) && glsState.records.isNotEmpty()) { + MaterialTheme.colorScheme.primary + } else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WorkingModeDialogPreview() { + WorkingModeDialog(GLSServiceData(workingMode = WorkingMode.ALL), {}) {} +} + +@Composable +private fun RecordsView( + state: GLSServiceData +) { + ScreenSection { + if (state.records.isEmpty()) { + RecordsViewWithoutData() + } else { + RecordsViewWithData(state) + } + } +} + +@Composable +private fun RecordsViewWithData( + state: GLSServiceData +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + SectionTitle(resId = R.drawable.ic_records, title = "Records") + + state.records.keys.forEachIndexed { i, it -> + RecordItem(it, state.records[it]) + + if (i < state.records.size - 1) { + HorizontalDivider() + } + } + } +} + +@Composable +private fun RecordItem( + record: GLSRecord, + gleContext: GLSMeasurementContext? +) { + var showBottomSheet by rememberSaveable { mutableStateOf(false) } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(10.dp)) + .clickable { + showBottomSheet = true + } + .padding(8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + SectionRow { + record.glucoseConcentration?.let { glucoseConcentration -> + record.unit?.let { unit -> + glucoseConcentrationDisplayValue(glucoseConcentration, unit) + } + }?.let { + KeyValueColumn( + record.type.toDisplayString(), + it, + keyStyle = MaterialTheme.typography.titleMedium + ) + } + record.time?.let { + KeyValueColumnReverse( + value = stringResource(id = R.string.gls_details_date_and_time), + key = stringResource(R.string.gls_timestamp, it) + ) + } + } + } + } + + if (showBottomSheet) { + GLSDetailsBottomSheet(record, gleContext) { showBottomSheet = false } + } +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun GLSDetailsBottomSheet( + record: GLSRecord, + context: GLSMeasurementContext?, + onDismiss: () -> Unit +) { + val sheetState = rememberModalBottomSheetState() + ModalBottomSheet( + onDismissRequest = { + onDismiss() + }, + sheetState = sheetState, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + containerColor = MaterialTheme.colorScheme.surface, + tonalElevation = 16.dp, + dragHandle = { + Box( + modifier = Modifier + .padding(8.dp) + .width(50.dp) + .height(6.dp) + .clip(RoundedCornerShape(50)) + .background(MaterialTheme.colorScheme.primary) + ) + } + ) { + GLSDetails(record, context) + } +} + + +@Preview(showBackground = true) +@Composable +private fun RecordItemPreview() { + RecordItem( + record = GLSRecord( + sequenceNumber = 1, + time = Calendar.getInstance(), + glucoseConcentration = 0.5f, + unit = ConcentrationUnit.UNIT_KGPL, + type = RecordType.VENOUS_PLASMA, + status = null, + sampleLocation = null, + contextInformationFollows = true + ), + gleContext = GLSMeasurementContext( + sequenceNumber = 20, + carbohydrate = Carbohydrate.LUNCH, + carbohydrateAmount = 12.5f, + meal = Meal.CASUAL, + tester = Tester.SELF, + health = Health.NO_HEALTH_ISSUES, + exerciseDuration = 2, + exerciseIntensity = 1, + medication = Medication.PRE_MIXED_INSULIN, + medicationQuantity = .5f, + medicationUnit = MedicationUnit.UNIT_KG, + HbA1c = 0.5f + ), + ) +} + +@Composable +private fun RecordsViewWithoutData() { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + SectionTitle(icon = Icons.Default.Search, title = "No items") + + Text( + text = stringResource(id = R.string.gls_no_records_info), + style = MaterialTheme.typography.bodyMedium + ) + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSUiMapper.kt new file mode 100644 index 00000000..31c3c106 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/GLSUiMapper.kt @@ -0,0 +1,49 @@ +package no.nordicsemi.android.toolbox.profile.view.gls + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ConcentrationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordType +import no.nordicsemi.android.toolbox.profile.R + +@Composable +internal fun RecordType?.toDisplayString(): String { + return when (this) { + RecordType.CAPILLARY_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_capillary_whole_blood) + RecordType.CAPILLARY_PLASMA -> stringResource(id = R.string.gls_type_capillary_plasma) + RecordType.VENOUS_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_venous_whole_blood) + RecordType.VENOUS_PLASMA -> stringResource(id = R.string.gls_type_venous_plasma) + RecordType.ARTERIAL_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_arterial_whole_blood) + RecordType.ARTERIAL_PLASMA -> stringResource(id = R.string.gls_type_arterial_plasma) + RecordType.UNDETERMINED_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_undetermined_whole_blood) + RecordType.UNDETERMINED_PLASMA -> stringResource(id = R.string.gls_type_undetermined_plasma) + RecordType.INTERSTITIAL_FLUID -> stringResource(id = R.string.gls_type_interstitial_fluid) + RecordType.CONTROL_SOLUTION -> stringResource(id = R.string.gls_type_control_solution) + null -> stringResource(id = R.string.gls_type_reserved) + } +} + +@Composable +internal fun ConcentrationUnit.toDisplayString(): String { + return when (this) { + ConcentrationUnit.UNIT_KGPL -> stringResource(id = R.string.gls_unit_kg_l) + ConcentrationUnit.UNIT_MOLPL -> stringResource(id = R.string.gls_unit_mol_dl) + } +} + +@Composable +internal fun WorkingMode.toDisplayString(): String { + return when (this) { + WorkingMode.ALL -> stringResource(id = R.string.gls__working_mode__all) + WorkingMode.LAST -> stringResource(id = R.string.gls__working_mode__last) + WorkingMode.FIRST -> stringResource(id = R.string.gls__working_mode__first) + } +} + +@SuppressLint("DefaultLocale") +@Composable +internal fun glucoseConcentrationDisplayValue(value: Float, unit: ConcentrationUnit): String { + return String.format("%.2f %s", value, unit.toDisplayString()) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetails.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetails.kt new file mode 100644 index 00000000..278b5eae --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetails.kt @@ -0,0 +1,310 @@ +package no.nordicsemi.android.toolbox.profile.view.gls.details + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Carbohydrate +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ConcentrationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GlucoseStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Health +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Meal +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Medication +import no.nordicsemi.android.toolbox.profile.parser.gls.data.MedicationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordType +import no.nordicsemi.android.toolbox.profile.parser.gls.data.SampleLocation +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Tester +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.view.gls.glucoseConcentrationDisplayValue +import no.nordicsemi.android.toolbox.profile.view.gls.toDisplayString +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.KeyValueField +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.dialog.toBooleanText +import java.util.Calendar + +@Composable +internal fun GLSDetails(record: GLSRecord, context: GLSMeasurementContext?) { + Column( + modifier = Modifier + .padding(16.dp) + .verticalScroll(rememberScrollState()), + ) { + ScreenSection { + Column { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_sequence_number), + record.sequenceNumber.toString() + ) + record.time?.let { + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_date_and_time), + stringResource(R.string.gls_timestamp, it) + ) + } + } + + } + HorizontalDivider( + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(vertical = 8.dp) + ) + + SectionRow { + record.type?.let { + KeyValueColumn( + stringResource(id = R.string.gls_details_type), it.toDisplayString() + ) + } + record.sampleLocation?.let { + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_location), + it.toDisplayString() + ) + } + + } + SectionRow { + record.glucoseConcentration?.let { glucoseConcentration -> + record.unit?.let { unit -> + KeyValueColumn( + stringResource(id = R.string.gls_details_glucose_condensation_title), + glucoseConcentrationDisplayValue(glucoseConcentration, unit), + keyStyle = MaterialTheme.typography.titleMedium + ) + } + } + } + + record.status?.let { + HorizontalDivider( + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(vertical = 8.dp) + ) + Text( + "Glucose status", + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.secondary + ) + + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_battery_low), + it.deviceBatteryLow.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_sensor_malfunction), + it.sensorMalfunction.toBooleanText(), + verticalSpacing = 4.dp + ) + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_insufficient_sample), + it.sampleSizeInsufficient.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_strip_insertion_error), + it.stripInsertionError.toBooleanText(), + verticalSpacing = 4.dp + ) + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_strip_type_incorrect), + it.stripTypeIncorrect.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_sensor_result_too_high), + it.sensorResultHigherThenDeviceCanProcess.toBooleanText(), + verticalSpacing = 4.dp + ) + } + + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_sensor_result_too_low), + it.sensorResultLowerThenDeviceCanProcess.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_temperature_too_high), + it.sensorTemperatureTooHigh.toBooleanText(), + verticalSpacing = 4.dp + ) + } + + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_temperature_too_low), + it.sensorTemperatureTooLow.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_strip_pulled_too_soon), + it.sensorReadInterrupted.toBooleanText(), + verticalSpacing = 4.dp + ) + } + + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_general_device_fault), + it.generalDeviceFault.toBooleanText(), + verticalSpacing = 4.dp + ) + KeyValueColumnReverse( + stringResource(id = R.string.gls_details_time_fault), + it.timeFault.toBooleanText(), + verticalSpacing = 4.dp + ) + } + } + + HorizontalDivider( + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(vertical = 8.dp) + ) + context?.let { glsMeasurementContext -> + Text( + stringResource(id = R.string.gls_context_title), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.secondary + ) + SectionRow { + KeyValueColumn( + stringResource(id = R.string.gls_details_sequence_number), + glsMeasurementContext.sequenceNumber.toString(), + verticalSpacing = 4.dp + ) + glsMeasurementContext.carbohydrate?.let { + val carbohydrateAmount = glsMeasurementContext.carbohydrateAmount + KeyValueColumnReverse( + stringResource(id = R.string.gls_context_carbohydrate), + it.toDisplayString() + " ($carbohydrateAmount g)", + verticalSpacing = 4.dp + ) + } + } + SectionRow { + glsMeasurementContext.meal?.let { + KeyValueColumn( + stringResource(id = R.string.gls_context_meal), + it.toDisplayString(), + verticalSpacing = 4.dp + ) + } + glsMeasurementContext.tester?.let { + KeyValueColumnReverse( + stringResource(id = R.string.gls_context_tester), + it.toDisplayString(), + verticalSpacing = 4.dp + ) + } + } + SectionRow { + glsMeasurementContext.health?.let { + KeyValueColumn( + stringResource(id = R.string.gls_context_health), + it.toDisplayString(), + verticalSpacing = 4.dp + ) + } + glsMeasurementContext.exerciseDuration?.let { duration -> + glsMeasurementContext.exerciseIntensity?.let { exerciseIntensity -> + KeyValueColumnReverse( + stringResource(id = R.string.gls_context_exercise_title), + stringResource( + id = R.string.gls_context_exercise_field, + getExerciseDuration(duration), + exerciseIntensity + ), + verticalSpacing = 4.dp + ) + } + } + } + SectionRow { + glsMeasurementContext.medicationUnit?.let { medicationUnit -> + val medicationField = String.format( + stringResource(id = R.string.gls_context_medication_field), + glsMeasurementContext.medication?.toDisplayString(), + glsMeasurementContext.medicationQuantity, + medicationUnit.toDisplayString() + ) + KeyValueColumn( + stringResource(id = R.string.gls_context_medication_title), + medicationField, + verticalSpacing = 4.dp + ) + } + + glsMeasurementContext.HbA1c?.let { hbA1c -> + KeyValueColumnReverse( + stringResource(id = R.string.gls_context_hba1c_title), + stringResource(id = R.string.gls_context_hba1c_field, hbA1c), + verticalSpacing = 4.dp + ) + } + } + + } ?: KeyValueField( + stringResource(id = R.string.gls_context_title), + stringResource(id = R.string.gls_unavailable) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun GlsDetailsPreview() { + GLSDetails( + record = GLSRecord( + sequenceNumber = 1, + time = Calendar.getInstance(), + glucoseConcentration = 0.5f, + unit = ConcentrationUnit.UNIT_KGPL, + type = RecordType.VENOUS_PLASMA, + status = GlucoseStatus(212), + sampleLocation = SampleLocation.FINGER, + contextInformationFollows = true + ), + context = GLSMeasurementContext( + sequenceNumber = 20, + carbohydrate = Carbohydrate.LUNCH, + carbohydrateAmount = 12.5f, + meal = Meal.CASUAL, + tester = Tester.SELF, + health = Health.NO_HEALTH_ISSUES, + exerciseDuration = 4520, // 1 hour, 15 minutes and 20 seconds + exerciseIntensity = 1, + medication = Medication.PRE_MIXED_INSULIN, + medicationQuantity = .5f, + medicationUnit = MedicationUnit.UNIT_KG, + HbA1c = 0.5f + ) + ) +} \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetailsUiMapper.kt similarity index 60% rename from profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt rename to profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetailsUiMapper.kt index 63a6ddbb..48254575 100644 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsMappers.kt +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/gls/details/GLSDetailsUiMapper.kt @@ -1,47 +1,15 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.details.view +package no.nordicsemi.android.toolbox.profile.view.gls.details import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.kotlin.ble.profile.gls.data.Carbohydrate -import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit -import no.nordicsemi.android.kotlin.ble.profile.gls.data.Health -import no.nordicsemi.android.kotlin.ble.profile.gls.data.Meal -import no.nordicsemi.android.kotlin.ble.profile.gls.data.Medication -import no.nordicsemi.android.kotlin.ble.profile.gls.data.MedicationUnit -import no.nordicsemi.android.kotlin.ble.profile.gls.data.SampleLocation -import no.nordicsemi.android.kotlin.ble.profile.gls.data.Tester +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Carbohydrate +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Health +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Meal +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Medication +import no.nordicsemi.android.toolbox.profile.parser.gls.data.MedicationUnit +import no.nordicsemi.android.toolbox.profile.parser.gls.data.SampleLocation +import no.nordicsemi.android.toolbox.profile.parser.gls.data.Tester +import no.nordicsemi.android.toolbox.profile.R @Composable internal fun SampleLocation.toDisplayString(): String { @@ -54,19 +22,11 @@ internal fun SampleLocation.toDisplayString(): String { } } -@Composable -internal fun ConcentrationUnit.toDisplayString(): String { - return when (this) { - ConcentrationUnit.UNIT_KGPL -> stringResource(id = R.string.gls_sample_location_kg_l) - ConcentrationUnit.UNIT_MOLPL -> stringResource(id = R.string.gls_sample_location_mol_l) - } -} - @Composable internal fun MedicationUnit.toDisplayString(): String { return when (this) { - MedicationUnit.UNIT_MG -> stringResource(id = R.string.gls_sample_location_kg) - MedicationUnit.UNIT_ML -> stringResource(id = R.string.gls_sample_location_l) + MedicationUnit.UNIT_KG -> stringResource(id = R.string.gls_sample_location_kg) + MedicationUnit.UNIT_LITER -> stringResource(id = R.string.gls_sample_location_l) } } @@ -131,3 +91,22 @@ internal fun Meal.toDisplayString(): String { Meal.BEDTIME -> stringResource(id = R.string.gls_meal_bedtime) } } + +internal fun getExerciseDuration(duration: Int): String { + // Given duration is in seconds. If duration is more than 60 seconds, convert it to minutes. If minutes is more than 60, convert it to hours, and so on. + val hours = duration / 3600 + val minutes = (duration % 3600) / 60 + val seconds = duration % 60 + + return when { + hours > 0 -> "$hours h ${minutes}m ${seconds}s" + minutes > 0 -> "${minutes}m ${seconds}s" + else -> "${seconds}s" + } +} + +fun main() { + // This is just a placeholder main function to avoid compilation errors. + // The actual code does not require a main function as it is intended for use in a Compose UI context. + println("Exercise Duration Example: ${getExerciseDuration(4520)}") // Example usage // 1 hour, 15 minutes and 20 seconds +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSScreen.kt new file mode 100644 index 00000000..44848eb3 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSScreen.kt @@ -0,0 +1,104 @@ +package no.nordicsemi.android.toolbox.profile.view.hrs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MonitorHeart +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.viewmodel.HRSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.HRSEvent.SwitchZoomEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.HRSViewModel +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle +import no.nordicsemi.android.ui.view.animate.AnimatedHeart + +@Composable +internal fun HRSScreen() { + val hrsViewModel = hiltViewModel() + val hrsServiceData by hrsViewModel.hrsState.collectAsStateWithLifecycle() + val onClickEvent: (HRSEvent) -> Unit = { hrsViewModel.onEvent(it) } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection(modifier = Modifier.padding(0.dp) /* No padding */) { + Column(modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp)) { + SectionTitle( + icon = Icons.Default.MonitorHeart, + title = stringResource(id = R.string.hrs_section_data), + menu = { + MagnifyingGlass(hrsServiceData.zoomIn) { onClickEvent(it) } + } + ) + + LineChartView(hrsServiceData, hrsServiceData.zoomIn) + hrsServiceData.heartRate?.let { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, top = 16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedHeart(modifier = Modifier.padding(8.dp)) + Text( + text = hrsServiceData.displayHeartRate(), + style = MaterialTheme.typography.titleLarge, + ) + } + } + } + + HorizontalDivider() + + hrsServiceData.bodySensorLocation?.let { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.body_sensor_location), + hrsServiceData.displayBodySensorLocation(), + modifier = Modifier.padding(16.dp) + ) + } + } + } + } + +} + +@Composable +private fun MagnifyingGlass(zoomIn: Boolean, onEvent: (HRSEvent) -> Unit) { + val icon = when (zoomIn) { + true -> R.drawable.ic_zoom_out + false -> R.drawable.ic_zoom_in + } + Icon( + painter = painterResource(id = icon), + contentDescription = stringResource(id = R.string.hrs_zoom_icon), + modifier = Modifier + .clip(CircleShape) + .clickable { onEvent(SwitchZoomEvent) } + .padding(8.dp) + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSUiMapper.kt new file mode 100644 index 00000000..fe82b355 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/HRSUiMapper.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.toolbox.profile.view.hrs + +import no.nordicsemi.android.toolbox.profile.data.HRSServiceData + +fun HRSServiceData.displayHeartRate(): String { + return "${this.heartRate} BPM" +} + +fun HRSServiceData.displayBodySensorLocation(): String { + return when (bodySensorLocation) { + 0 -> "Other" + 1 -> "Chest" + 2 -> "Wrist" + 3 -> "Finger" + 4 -> "Hand" + 5 -> "Ear Lobe" + 6 -> "Foot" + else -> "Unknown" + } +} \ No newline at end of file diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/LineChart.kt similarity index 59% rename from profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt rename to profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/LineChart.kt index aa185af2..2fc52fe3 100644 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/LineChartView.kt +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hrs/LineChart.kt @@ -1,39 +1,7 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.view +package no.nordicsemi.android.toolbox.profile.view.hrs import android.content.Context import android.graphics.Color -import android.graphics.DashPathEffect import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -42,31 +10,37 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.github.mikephil.charting.charts.LineChart +import com.github.mikephil.charting.components.Legend import com.github.mikephil.charting.components.XAxis import com.github.mikephil.charting.data.Entry import com.github.mikephil.charting.data.LineData import com.github.mikephil.charting.data.LineDataSet import com.github.mikephil.charting.interfaces.datasets.ILineDataSet -import no.nordicsemi.android.hrs.data.HRSServiceData - -private const val X_AXIS_ELEMENTS_COUNT = 40f - -private const val AXIS_MIN = 0 -private const val AXIS_MAX = 300 +import no.nordicsemi.android.toolbox.profile.data.HRSServiceData @Composable -internal fun LineChartView(state: HRSServiceData, zoomIn: Boolean,) { - val items = state.heartRates.takeLast(X_AXIS_ELEMENTS_COUNT.toInt()).reversed() +internal fun LineChartView(state: HRSServiceData, zoomIn: Boolean) { + val items = state.heartRates.takeLast(state.heartRates.size) val isSystemInDarkTheme = isSystemInDarkTheme() + AndroidView( modifier = Modifier .fillMaxWidth() .height(300.dp), factory = { createLineChartView(isSystemInDarkTheme, it, items, zoomIn) }, - update = { updateData(items, it, zoomIn) } + update = { updateData(isSystemInDarkTheme, items, it, zoomIn) } ) } +/** The number of elements to display on the X axis. */ +private const val X_AXIS_ELEMENTS_COUNT = 40f + +/** The minimum value for the Y axis. */ +private const val AXIS_MIN = 0 + +/** The maximum value for the Y axis. */ +private const val AXIS_MAX = 300 + internal fun createLineChartView( isDarkTheme: Boolean, context: Context, @@ -75,16 +49,12 @@ internal fun createLineChartView( ): LineChart { return LineChart(context).apply { description.isEnabled = false - - legend.isEnabled = false - - setTouchEnabled(false) - + legend.isEnabled = true + setTouchEnabled(true) // Enable touch gestures setDrawGridBackground(false) - isDragEnabled = true - setScaleEnabled(false) - setPinchZoom(false) + setScaleEnabled(false) // Enable scaling + setPinchZoom(true) // Enable pinch zoom if (isDarkTheme) { setBackgroundColor(Color.TRANSPARENT) @@ -101,25 +71,33 @@ internal fun createLineChartView( } xAxis.apply { - xAxis.enableGridDashedLine(10f, 10f, 0f) - - axisMinimum = -X_AXIS_ELEMENTS_COUNT - axisMaximum = 0f - setAvoidFirstLastClipping(true) + enableGridDashedLine(10f, 10f, 0f) + axisMinimum = 0f + axisMaximum = X_AXIS_ELEMENTS_COUNT + setAvoidFirstLastClipping(false) position = XAxis.XAxisPosition.BOTTOM + setDrawLabels(false) // Hide X-axis labels + setDrawGridLines(false) // Hide vertical grid lines } axisLeft.apply { enableGridDashedLine(10f, 10f, 0f) - axisMaximum = points.getMax(zoomIn) axisMinimum = points.getMin(zoomIn) +// setDrawGridLines(true) // Show horizontal grid lines } axisRight.isEnabled = false val entries = points.mapIndexed { i, v -> - Entry(-i.toFloat(), v.toFloat()) - }.reversed() + Entry(i.toFloat(), v.toFloat()) + } + legend.apply { + isEnabled = true + textColor = if (isDarkTheme) Color.WHITE else Color.RED + form = Legend.LegendForm.LINE + horizontalAlignment = Legend.LegendHorizontalAlignment.CENTER + verticalAlignment = Legend.LegendVerticalAlignment.TOP + } // create a dataset and give it a type if (data != null && data.dataSetCount > 0) { val set1 = data!!.getDataSetByIndex(0) as LineDataSet @@ -128,41 +106,31 @@ internal fun createLineChartView( data!!.notifyDataChanged() notifyDataSetChanged() } else { - val set1 = LineDataSet(entries, "DataSet 1") + val set1 = LineDataSet(entries, "Heart Rate") set1.setDrawIcons(false) set1.setDrawValues(false) - // draw dashed line - set1.enableDashedLine(10f, 5f, 0f) + // solid line + set1.enableDashedLine(0f, 0f, 0f) - // black lines and points - if (isDarkTheme) { - set1.color = Color.WHITE - set1.setCircleColor(Color.WHITE) - } else { - set1.color = Color.BLACK - set1.setCircleColor(Color.BLACK) - } + // red line and points + set1.color = Color.RED + set1.setDrawCircles(false) // line thickness and point size - set1.lineWidth = 1f - set1.circleRadius = 3f + set1.lineWidth = 2f // draw points as solid circles set1.setDrawCircleHole(false) // customize legend entry set1.formLineWidth = 1f - set1.formLineDashEffect = DashPathEffect(floatArrayOf(10f, 5f), 0f) set1.formSize = 15f // text size of values set1.valueTextSize = 9f - // draw selection line as dashed - set1.enableDashedHighlightLine(10f, 5f, 0f) - val dataSets = ArrayList() dataSets.add(set1) // add the data sets @@ -171,20 +139,28 @@ internal fun createLineChartView( // set data setData(data) + setVisibleXRangeMaximum(10f) + moveViewToX(0f) } } } -private fun updateData(points: List, chart: LineChart, zoomIn: Boolean) { +internal fun updateData( + isDarkTheme: Boolean, + points: List, + chart: LineChart, + zoomIn: Boolean +) { val entries = points.mapIndexed { i, v -> - Entry(-i.toFloat(), v.toFloat()) - }.reversed() + Entry(i.toFloat(), v.toFloat()) + } with(chart) { axisLeft.apply { axisMaximum = points.getMax(zoomIn) axisMinimum = points.getMin(zoomIn) } + xAxis.axisMaximum = points.size.toFloat() // Update axisMaximum to the size of heart rates if (data != null && data.dataSetCount > 0) { val set1 = data!!.getDataSetByIndex(0) as LineDataSet set1.values = entries @@ -192,7 +168,25 @@ private fun updateData(points: List, chart: LineChart, zoomIn: Boolean) { data!!.notifyDataChanged() notifyDataSetChanged() invalidate() + } else { + val set1 = LineDataSet(entries, "DataSet 1") + set1.setDrawIcons(false) + set1.setDrawValues(false) + set1.enableDashedLine(10f, 5f, 0f) + set1.color = if (isDarkTheme) Color.WHITE else Color.BLACK + set1.setCircleColor(if (isDarkTheme) Color.WHITE else Color.BLACK) + set1.lineWidth = 1f + set1.setDrawCircleHole(false) + set1.formLineWidth = 1f + set1.formSize = 15f + set1.valueTextSize = 9f + + val dataSets = ArrayList().apply { add(set1) } + val data = LineData(dataSets) + setData(data) } + setVisibleXRangeMaximum(40f) + moveViewToX(entries.size.toFloat()) } } diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSScreen.kt new file mode 100644 index 00000000..405c181d --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSScreen.kt @@ -0,0 +1,213 @@ +package no.nordicsemi.android.toolbox.profile.view.hts + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.hts.HTSMeasurementType +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.HTSServiceData +import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit +import no.nordicsemi.android.toolbox.profile.viewmodel.HTSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.HTSViewModel +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle +import no.nordicsemi.android.ui.view.TextWithAnimatedDots + +@Composable +internal fun HTSScreen() { + val htsViewModel = hiltViewModel() + val onClickEvent: (HTSEvent) -> Unit = { htsViewModel.onEvent(it) } + val htsServiceData by htsViewModel.htsServiceState.collectAsStateWithLifecycle() + + HTSContent(htsServiceData, onClickEvent) +} + +@Composable +private fun HTSContent( + htsServiceData: HTSServiceData, + onClickEvent: (HTSEvent) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection { + SectionTitle( + resId = R.drawable.ic_hts, + title = stringResource(id = R.string.hts_temperature), + menu = { + TemperatureUnitSettings( + state = htsServiceData, + onClickEvent = { onClickEvent(it) }, + ) + } + ) + SectionRow { + htsServiceData.data?.temperature?.let { temperature -> + KeyValueColumn( + value = stringResource(id = R.string.temperature_title), + key = htsServiceData.temperatureUnit.displayTemperature(temperature), + keyStyle = MaterialTheme.typography.titleMedium + ) + } ?: run { + TextWithAnimatedDots(text = stringResource(id = R.string.reading_temperature_placeholder)) + } + } + if (htsServiceData.data?.type != null) { + SectionRow { + KeyValueColumn( + value = stringResource(id = R.string.temp_measurement_location), + key = htsServiceData.data!!.type?.let { + HTSMeasurementType.fromValue(it).toString() + } ?: "Unknown", + keyStyle = MaterialTheme.typography.titleMedium + ) + } + } + htsServiceData.data?.timestamp?.let { + SectionRow { + KeyValueColumn( + value = stringResource(R.string.temp_measurement_time), + key = it.toFormattedString(), + keyStyle = MaterialTheme.typography.titleMedium + ) + } + } + } + } +} + +@Composable +private fun TemperatureUnitSettings( + state: HTSServiceData, + onClickEvent: (HTSEvent) -> Unit +) { + var openSettingsDialog by rememberSaveable { mutableStateOf(false) } + + Column { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(id = R.string.hts_temperature_unit_des), + modifier = Modifier + .clip(CircleShape) + .clickable { openSettingsDialog = true } + ) + if (openSettingsDialog) { + TemperatureUnitSettingsDialog( + state, + { openSettingsDialog = false } + ) { onClickEvent(it) } + } + } +} + +@Composable +private fun TemperatureUnitSettingsDialog( + state: HTSServiceData, + onDismiss: () -> Unit, + onClickEvent: (HTSEvent) -> Unit, +) { + val listState = rememberLazyListState() + val entries = TemperatureUnit.entries.map { it } + + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Text( + text = stringResource(id = R.string.hts_temperature_unit), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium + ) + HorizontalDivider() + LazyColumn( + state = listState + ) { + items(entries.size) { index -> + val entry = entries[index] + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { + onClickEvent( + HTSEvent.OnTemperatureUnitSelected(entry) + ) + onDismiss() + } + .padding(8.dp), + ) { + Text( + text = entry.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + style = MaterialTheme.typography.titleLarge, + color = if (state.temperatureUnit == entry) + MaterialTheme.colorScheme.primary else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun TemperatureUnitSettingsDialogPreview() { + TemperatureUnitSettingsDialog( + state = HTSServiceData(), + onDismiss = {}, + onClickEvent = {} + ) +} \ No newline at end of file diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSUiMapper.kt similarity index 68% rename from profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt rename to profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSUiMapper.kt index aab7d8d7..e3678286 100644 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMMapper.kt +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/hts/HTSUiMapper.kt @@ -29,22 +29,22 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.cgms.view +package no.nordicsemi.android.toolbox.profile.view.hts -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit import java.text.SimpleDateFormat -import java.util.Date +import java.util.Calendar import java.util.Locale -internal fun CGMRecordWithSequenceNumber.formattedTime(): String { - val timeFormat = SimpleDateFormat("dd.MM.yyyy HH:mm", Locale.US) - return timeFormat.format(Date(timestamp)) -} - -@Composable -internal fun CGMRecordWithSequenceNumber.glucoseConcentration(): String { - return stringResource(id = R.string.cgms_value_unit, record.glucoseConcentration) +internal fun TemperatureUnit.displayTemperature(value: Float): String { + return when (this) { + TemperatureUnit.CELSIUS -> String.format(Locale.US, "%.1f °C", value) + TemperatureUnit.FAHRENHEIT -> String.format(Locale.US, "%.1f °F", value * 1.8f + 32f) + TemperatureUnit.KELVIN -> String.format(Locale.US, "%.1f K", value + 273.15f) + } } +// Extension function to convert Calendar to a formatted String +fun Calendar.toFormattedString(pattern: String = "dd MMM yyyy, HH:mm"): String { + val formatter = SimpleDateFormat(pattern, Locale.getDefault()) + return formatter.format(this.time) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/internal/ProfileAppBar.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/internal/ProfileAppBar.kt new file mode 100644 index 00000000..7c103acc --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/internal/ProfileAppBar.kt @@ -0,0 +1,35 @@ +package no.nordicsemi.android.toolbox.profile.view.internal + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.toolbox.profile.viewmodel.DeviceConnectionState +import no.nordicsemi.android.ui.view.BackIconAppBar +import no.nordicsemi.android.ui.view.LoggerBackIconAppBar +import no.nordicsemi.android.ui.view.LoggerIconAppBar + +@Composable +internal fun ProfileAppBar( + deviceName: String?, + title: String, + connectionState: DeviceConnectionState, + navigateUp: () -> Unit, + disconnect: () -> Unit, + openLogger: () -> Unit +) { + if (deviceName?.isNotBlank() == true) { + if (connectionState !is DeviceConnectionState.Disconnected) + LoggerIconAppBar(deviceName, navigateUp, disconnect, openLogger) + else LoggerBackIconAppBar(deviceName, navigateUp) { openLogger() } + } else { + BackIconAppBar(title, navigateUp) + } +} + +@Preview +@Composable +private fun ProfileAppBarPreview() { + NordicTheme { + ProfileAppBar("DE", "nRF Toolbox", DeviceConnectionState.Connecting, {}, {}) {} + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/lbs/BlinkyScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/lbs/BlinkyScreen.kt new file mode 100644 index 00000000..3b9f4eab --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/lbs/BlinkyScreen.kt @@ -0,0 +1,168 @@ +package no.nordicsemi.android.toolbox.profile.view.lbs + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Lightbulb +import androidx.compose.material.icons.filled.RadioButtonChecked +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.viewmodel.LBSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.LBSViewModel + +@Composable +internal fun BlinkyScreen() { + val lbsViewModel = hiltViewModel() + val onClickEvent: (LBSEvent) -> Unit = { lbsViewModel.onEvent(it) } + val serviceData by lbsViewModel.lbsState.collectAsStateWithLifecycle() + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + LedControlView( + ledState = serviceData.data.ledState, + onStateChanged = { onClickEvent(LBSEvent.OnLedStateChanged(it)) }, + ) + + ButtonControlView( + buttonState = serviceData.data.buttonState, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun BlinkyScreenPreview() { + BlinkyScreen() +} + +@Composable +private fun ButtonControlView( + buttonState: Boolean +) { + val (text, textColor) = if (buttonState) { + stringResource(id = R.string.button_pressed) to MaterialTheme.colorScheme.primary + } else { + stringResource(id = R.string.button_released) to MaterialTheme.colorScheme.onSurface + } + OutlinedCard { + Column( + modifier = Modifier + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = Icons.Default.RadioButtonChecked, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + colorFilter = ColorFilter.tint(textColor) + ) + Text( + text = stringResource(id = R.string.button), + style = MaterialTheme.typography.headlineMedium, + ) + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = text, + color = textColor, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun ButtonControlViewPreview() { + ButtonControlView( + buttonState = true, + ) +} + +@Composable +private fun LedControlView( + ledState: Boolean, + onStateChanged: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val colorFilter = if (ledState) { + ColorFilter.tint(MaterialTheme.colorScheme.primary) + } else { + ColorFilter.tint(MaterialTheme.colorScheme.onSurface) + } + OutlinedCard( + modifier = modifier + ) { + Column( + modifier = Modifier + .clickable { onStateChanged(!ledState) } + .padding(16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + imageVector = Icons.Default.Lightbulb, + contentDescription = null, + modifier = Modifier.padding(end = 16.dp), + colorFilter = colorFilter + ) + Text( + text = stringResource(id = R.string.light), + style = MaterialTheme.typography.headlineMedium, + ) + } + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(id = R.string.led_guide), + modifier = Modifier.weight(1f) + ) + Switch(checked = ledState, onCheckedChange = onStateChanged) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun LecControlViewPreview() { + LedControlView( + ledState = true, + onStateChanged = {}, + modifier = Modifier.padding(16.dp), + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSScreen.kt new file mode 100644 index 00000000..8c1d66f3 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSScreen.kt @@ -0,0 +1,226 @@ +package no.nordicsemi.android.toolbox.profile.view.rscs + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.RSCSServiceData +import no.nordicsemi.android.toolbox.profile.viewmodel.RSCSEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.RSCSViewModel +import no.nordicsemi.android.ui.view.FeatureSupported +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun RSCSScreen() { + val rscsViewModel = hiltViewModel() + val serviceData by rscsViewModel.rscsState.collectAsStateWithLifecycle() + val onClickEvent: (RSCSEvent) -> Unit = { rscsViewModel.onEvent(it) } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection(modifier = Modifier.padding(bottom = 16.dp)) { + SectionTitle( + resId = R.drawable.ic_rscs, + title = if (serviceData.data.running) "Running" else "Walking", + modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp), + menu = { RSCSSettingsDropdown(serviceData, onClickEvent) } + ) + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.rscs_cadence), + serviceData.displayPace() + ) + KeyValueColumnReverse( + value = stringResource(id = R.string.rscs_activity), + key = if (serviceData.data.running) + "\uD83C\uDFC3 ${serviceData.displayActivity()}" else + "\uD83D\uDEB6 ${serviceData.displayActivity()}" + ) + } + SectionRow { + KeyValueColumn("Speed", "${serviceData.displaySpeed()}") + serviceData.displayStrideLength()?.let { + KeyValueColumnReverse(stringResource(id = R.string.stride_length), it) + } ?: serviceData.displayNumberOfSteps()?.let { + KeyValueColumnReverse( + stringResource(id = R.string.rscs_number_of_steps), + it + ) + } + } + serviceData.data.totalDistance?.let { + SectionRow { + KeyValueColumn( + "Total distance", + serviceData.data.displayDistance( + serviceData.unit ?: RSCSSettingsUnit.UNIT_M + ) + ) + } + } + } + serviceData.feature?.let { + HorizontalDivider() + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(start = 16.dp, end = 16.dp) + ) { + Text("Supported features", style = MaterialTheme.typography.titleMedium) + if (it.instantaneousStrideLengthMeasurementSupported) { + FeatureSupported( + stringResource(id = R.string.instantaneous_stride_length_measurement) + ) + } + if (it.totalDistanceMeasurementSupported) { + FeatureSupported( + stringResource(id = R.string.total_distance_measurement) + ) + } + if (it.walkingOrRunningStatusSupported) { + FeatureSupported( + stringResource(id = R.string.walking_or_running_status) + ) + } + if (it.calibrationSupported) { + FeatureSupported(stringResource(id = R.string.calibration)) + } + if (it.multipleSensorLocationsSupported) { + FeatureSupported(stringResource(id = R.string.multiple_sensor_location)) + } + } + } + } + } +} + +@Composable +private fun RSCSSettingsDropdown( + state: RSCSServiceData, + onClickEvent: (RSCSEvent) -> Unit +) { + var openSettingsDialog by rememberSaveable { mutableStateOf(false) } + + Column { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "display settings", + modifier = Modifier + .clip(CircleShape) + .clickable { openSettingsDialog = true } + ) + + if (openSettingsDialog) { + RSCSSettingsDialog(state, { openSettingsDialog = false }, onClickEvent) + } + } +} + +@Composable +private fun RSCSSettingsDialog( + state: RSCSServiceData, + onDismiss: () -> Unit, + onSpeedUnitSelected: (RSCSEvent) -> Unit +) { + Dialog( + onDismissRequest = { onDismiss() }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + ) { + Text( + text = stringResource(R.string.csc_settings), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium + ) + HorizontalDivider() + Column { + RSCSSettingsUnit.entries.forEach { entry -> + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(4.dp)) + .clickable { + onSpeedUnitSelected( + RSCSEvent.OnSelectedSpeedUnitSelected(entry) + ) + onDismiss() + }, + ) { + Text( + text = entry.toString(), + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + color = if (state.unit == entry) + MaterialTheme.colorScheme.primary else + MaterialTheme.colorScheme.onBackground + ) + } + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun RSCSSettingsDialogPreview() { + RSCSSettingsDialog( + state = RSCSServiceData(), + onDismiss = {}, + onSpeedUnitSelected = {} + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSUiMapper.kt new file mode 100644 index 00000000..659aa603 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/rscs/RSCSUiMapper.kt @@ -0,0 +1,128 @@ +package no.nordicsemi.android.toolbox.profile.view.rscs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSData +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.RSCSServiceData +import java.util.Locale + +@Composable +fun RSCSServiceData.displayActivity(): String = + stringResource(id = if (data.running) R.string.rscs_running else R.string.rscs_walking) + +@Composable +fun RSCSServiceData.displayCadence(): String = + stringResource(id = R.string.rscs_speed, data.instantaneousSpeed) + +@Composable +fun RSCSServiceData.displayPace(): String = + stringResource(id = R.string.rscs_rpm, data.instantaneousCadence) + + +@Composable +fun RSCSServiceData.displayNumberOfSteps(): String? { + if (data.totalDistance == null || data.strideLength == null) { + return null + } + val numberOfSteps = data.totalDistance!! / data.strideLength!!.toLong() + return numberOfSteps.toString() +} + +internal fun RSCSData.speedWithSpeedUnit(speedUnit: RSCSSettingsUnit): Float { + return when (speedUnit) { + RSCSSettingsUnit.UNIT_M -> instantaneousSpeed + RSCSSettingsUnit.UNIT_KM -> instantaneousSpeed * 3.6f + RSCSSettingsUnit.UNIT_MPH -> instantaneousSpeed * 2.2369f + RSCSSettingsUnit.UNIT_CM -> instantaneousSpeed * 100 + } +} + +internal fun RSCSServiceData.displaySpeed(): String? { + val speedWithUnit = unit?.let { data.speedWithSpeedUnit(it) } + return when (unit) { + RSCSSettingsUnit.UNIT_M -> String.format(Locale.US, "%.1f m/s", speedWithUnit) + RSCSSettingsUnit.UNIT_KM -> String.format(Locale.US, "%.1f km/h", speedWithUnit) + RSCSSettingsUnit.UNIT_MPH -> String.format(Locale.US, "%.1f mph", speedWithUnit) + RSCSSettingsUnit.UNIT_CM -> String.format(Locale.US, "%.1f cm/s", speedWithUnit) + null -> null + } +} + +/** + * Returns the total distance in a formatted string based on the provided speed unit. + * + * @param speedUnit The unit to display the distance in. + * @return A formatted string representing the total distance. + */ +internal fun RSCSData.displayDistance(speedUnit: RSCSSettingsUnit): String { + if (totalDistance == null) return "" + return when (speedUnit) { + RSCSSettingsUnit.UNIT_M -> String.format( + Locale.US, + "%.0f m", + totalDistance!!.toFloat() + ) + + RSCSSettingsUnit.UNIT_KM -> String.format( + Locale.US, + "%.0f m", + totalDistance!!.toFloat().toKilometers() + ) + + RSCSSettingsUnit.UNIT_MPH -> String.format( + Locale.US, + "%.2f mile", + totalDistance!!.toFloat().toMiles() + ) + + RSCSSettingsUnit.UNIT_CM -> String.format( + Locale.US, + "%.2f cm", + totalDistance!!.toFloat().toCentimeter() + ) + } +} + +private fun Float.toCentimeter(): Float = this * 100 + +@Composable +internal fun RSCSServiceData.displayStrideLength(): String? { + if (data.strideLength == null) return null + return when (unit) { + RSCSSettingsUnit.UNIT_M -> String.format( + Locale.US, + "%.2f m", + data.strideLength!! / 100.0f + ) + + RSCSSettingsUnit.UNIT_KM -> String.format( + Locale.US, + "%.4f km", + data.strideLength!! / 100000.0f + ) + + RSCSSettingsUnit.UNIT_MPH -> String.format( + Locale.US, + "%.4f mile", + data.strideLength!! / 160931.23f + ) + + RSCSSettingsUnit.UNIT_CM -> String.format( + Locale.US, + "%.1f cm", + data.strideLength!!.toFloat() + ) + + null -> null + } +} + +private fun Float.toKilometers(): Float { + return this / 1000f +} + +private fun Float.toMiles(): Float { + return this * 0.0006f +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputScreen.kt new file mode 100644 index 00000000..574fbb62 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputScreen.kt @@ -0,0 +1,274 @@ +package no.nordicsemi.android.toolbox.profile.view.throughput + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.SyncAlt +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.NumberOfBytes +import no.nordicsemi.android.toolbox.profile.data.NumberOfSeconds +import no.nordicsemi.android.toolbox.profile.data.ThroughputServiceData +import no.nordicsemi.android.toolbox.profile.data.WritingStatus +import no.nordicsemi.android.toolbox.profile.viewmodel.ThroughputEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.ThroughputViewModel +import no.nordicsemi.android.ui.view.AnimatedThreeDots +import no.nordicsemi.android.ui.view.KeyValueColumn +import no.nordicsemi.android.ui.view.KeyValueColumnReverse +import no.nordicsemi.android.ui.view.ScreenSection +import no.nordicsemi.android.ui.view.SectionRow +import no.nordicsemi.android.ui.view.SectionTitle +import no.nordicsemi.android.ui.view.TextInputField + +@Composable +internal fun ThroughputScreen( + maxWriteValueLength: Int? +) { + val throughputViewModel = hiltViewModel() + val serviceData by throughputViewModel.throughputState.collectAsStateWithLifecycle() + val onClickEvent: (ThroughputEvent) -> Unit = { throughputViewModel.onEvent(it) } + + // Update the max write value length in the ViewModel. + LaunchedEffect(maxWriteValueLength != null) { + onClickEvent(ThroughputEvent.UpdateMaxWriteValueLength(maxWriteValueLength)) + } + + ThroughputContent(serviceData, onClickEvent) +} + +@Composable +private fun ThroughputContent( + serviceData: ThroughputServiceData, + onClickEvent: (ThroughputEvent) -> Unit +) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + ScreenSection { + SectionTitle( + icon = Icons.Default.SyncAlt, + title = stringResource(id = R.string.throughput_service_name), + menu = { + var expanded by rememberSaveable { mutableStateOf(false) } + var number by rememberSaveable { mutableIntStateOf(0) } + var writeDataType by rememberSaveable { mutableStateOf("") } + + if (serviceData.writingStatus == WritingStatus.IN_PROGRESS) { + Box(modifier = Modifier.padding(8.dp)) { + CircularProgressIndicator( + modifier = Modifier.size(40.dp), + color = MaterialTheme.colorScheme.secondary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + ) + } + } else + WriteDropdown( + expanded = expanded, + writeDataType = writeDataType, + number = number, + onDropdownMenuSelected = { writeDataType = it }, + onNumberUpdate = { number = it }, + onDismiss = { + expanded = false + writeDataType = "" + number = 0 + }, + onExpand = { expanded = true }, + onClickEvent = onClickEvent + ) + } + ) + // Show throughput data. + when (serviceData.writingStatus) { + WritingStatus.IN_PROGRESS -> ThroughputInProgress(serviceData.maxWriteValueLength) { AnimatedThreeDots() } + WritingStatus.IDEAL, WritingStatus.COMPLETED -> ThroughputData(serviceData) + } + } + } +} + +@Composable +fun ThroughputInProgress( + maxWriteValueLength: Int?, + animatedThreeDots: @Composable () -> Unit +) { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.total_bytes_received), + ) { animatedThreeDots() } + KeyValueColumnReverse( + stringResource(id = R.string.gatt_write_number) + ) { animatedThreeDots() } + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.measured_throughput) + ) { animatedThreeDots() } + // Show mtu size + maxWriteValueLength?.let { + KeyValueColumnReverse( + stringResource(id = R.string.max_write_value), + "$it" + ) + } + } +} + +@Composable +private fun ThroughputData(serviceData: ThroughputServiceData) { + serviceData.throughputData.let { + SectionRow { + KeyValueColumn( + stringResource(id = R.string.total_bytes_received), + it.throughputDataReceived() + ) + KeyValueColumnReverse( + stringResource(id = R.string.gatt_write_number), + it.gattWritesReceived.toString() + ) + } + SectionRow { + KeyValueColumn( + stringResource(id = R.string.measured_throughput), + it.displayThroughput() + ) + // Show mtu size + serviceData.maxWriteValueLength?.let { + KeyValueColumnReverse( + stringResource(id = R.string.max_write_value), + "$it" + ) + } + } + } +} + +@Composable +private fun WriteDropdown( + expanded: Boolean, + number: Int, + writeDataType: String, + onDismiss: () -> Unit, + onExpand: () -> Unit, + onDropdownMenuSelected: (String) -> Unit, + onNumberUpdate: (Int) -> Unit, + onClickEvent: (ThroughputEvent) -> Unit +) { + Box { + Button(onClick = { onExpand() }) { + Text(stringResource(id = R.string.throughput_write)) + } + // Animated dropdown menu + DropdownMenu( + expanded = expanded, + onDismissRequest = { onDismiss() }, + modifier = Modifier.padding(8.dp) + ) { + when (writeDataType) { + NumberOfBytes.getString() -> { + // Show bytes input + TextInputField( + input = number.toString(), + label = stringResource(id = R.string.throughput_bytes), + placeholder = stringResource(id = R.string.throughput_bytes_description), + errorState = number < 0, + errorMessage = stringResource(id = R.string.throughput_bytes_error), + onUpdate = { + onNumberUpdate(it.toIntOrNull() ?: 0) + + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + } + + NumberOfSeconds.getString() -> { + // Show time input + TextInputField( + input = number.toString(), + label = stringResource(id = R.string.throughput_time), + placeholder = stringResource(id = R.string.throughput_time_description), + errorState = number < 0, + errorMessage = stringResource(id = R.string.throughput_time_error), + onUpdate = { + onNumberUpdate(it.toIntOrNull() ?: 0) + }, + keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number) + ) + } + + else -> { + // Show throughput input type + getThroughputInputTypes().forEach { + DropdownMenuItem( + text = { Text(it) }, + onClick = { + onDropdownMenuSelected(it) + when (it) { + NumberOfBytes.getString() -> onNumberUpdate(100) + NumberOfSeconds.getString() -> onNumberUpdate(20) + } + } + ) + } + } + } + // Run button. + if (writeDataType.isNotEmpty() && number > 0) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Button( + colors = ButtonDefaults.buttonColors(), + onClick = { + onClickEvent( + ThroughputEvent.OnWriteData( + when (writeDataType) { + NumberOfBytes.getString() -> NumberOfBytes(number * 1024) + NumberOfSeconds.getString() -> NumberOfSeconds(number) + else -> throw IllegalArgumentException("Invalid throughput input type") + } + ) + ) + onDismiss() + } + ) { Text(text = stringResource(id = R.string.throughput_start)) } + } + } + } + } +} + +@Preview +@Composable +private fun ThroughputScreenPreview() { + ThroughputContent(ThroughputServiceData()) {} +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputUiMapper.kt new file mode 100644 index 00000000..070a7c71 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/throughput/ThroughputUiMapper.kt @@ -0,0 +1,44 @@ +package no.nordicsemi.android.toolbox.profile.view.throughput + +import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputMetrics +import no.nordicsemi.android.toolbox.profile.data.NumberOfBytes +import no.nordicsemi.android.toolbox.profile.data.NumberOfSeconds +import java.util.Locale + +internal fun ThroughputMetrics.throughputDataReceived(): String { + val kilobytes = this.totalBytesReceived / 1024f + val megabytes = kilobytes / 1024f + + return when { + megabytes >= 1 -> { + "${String.format(Locale.US, "%.2f", megabytes)} MB" + } + + kilobytes > 0 -> { + "${String.format(Locale.US, "%.2f", kilobytes)} KB" + } + + else -> { + "${this.totalBytesReceived} bytes" + } + } +} + +internal fun ThroughputMetrics.displayThroughput(): String { + val kbps = (this.throughputBitsPerSecond / 8f) / 1024f + return if (kbps > 0) { + "${String.format(Locale.US, "%.2f", kbps)} KBps" + } else { + "${this.throughputBitsPerSecond} bps" + } +} + +/** + * This function returns a list of Strings from the Throughput input type. + */ +fun getThroughputInputTypes(): List { + return listOf( + NumberOfBytes.getString(), + NumberOfSeconds.getString() + ) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/InputSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/InputSection.kt new file mode 100644 index 00000000..84201b44 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/InputSection.kt @@ -0,0 +1,115 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent + +@Composable +internal fun InputSection( + onEvent: (UARTEvent) -> Unit, + modifier: Modifier = Modifier, +) { + var text by rememberSaveable { mutableStateOf("") } + val checkedItem by rememberSaveable { mutableStateOf(MacroEol.entries[0]) } + val focusRequester = remember { FocusRequester() } + val bringIntoViewRequester = remember { BringIntoViewRequester() } + val scope = rememberCoroutineScope() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp) + ) { + Box(modifier = Modifier.weight(1f)) { + BasicTextField( + modifier = Modifier + .fillMaxWidth() + .bringIntoViewRequester(bringIntoViewRequester) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + scope.launch { + bringIntoViewRequester.bringIntoView() + } + } + } + .padding(16.dp), + value = text, + textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current), + onValueChange = { newValue -> + text = newValue + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant) + ) + if (text.isEmpty()) { + Text( + modifier = Modifier + .padding(16.dp) + .align(Alignment.CenterStart) + .alpha(0.5f), + text = stringResource(id = R.string.uart_input_hint), + ) + } + } + Icon( + Icons.AutoMirrored.Filled.Send, + contentDescription = stringResource(id = R.string.uart_input_macro), + modifier = Modifier + .clip(CircleShape) + .clickable { + onEvent(UARTEvent.OnRunInput(text, checkedItem)) + text = "" + } + .padding(8.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun InputSectionPreview() { + InputSection( + onEvent = {}) +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/MacroSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/MacroSection.kt new file mode 100644 index 00000000..88e78719 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/MacroSection.kt @@ -0,0 +1,260 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.EditOff +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.UARTViewState +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent +import no.nordicsemi.android.ui.view.SectionTitle + +@Composable +internal fun MacroSection( + viewState: UARTViewState = UARTViewState(), + onEvent: (UARTEvent) -> Unit +) { + var showAddDialog by rememberSaveable { mutableStateOf(false) } + var showDeleteDialog by rememberSaveable { mutableStateOf(false) } + + // Dialogs + MacroDialogs( + viewState = viewState, + showAddDialog = showAddDialog, + showDeleteDialog = showDeleteDialog, + onDismissAdd = { showAddDialog = false }, + onDismissDelete = { showDeleteDialog = false }, + onEvent = onEvent + ) + + Column { + OutlinedCard { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + MacroSectionTitle( + onAddClick = { showAddDialog = true } + ) + + if (viewState.configurations.isNotEmpty()) { + MacroConfigControls( + viewState = viewState, + onDeleteClick = { showDeleteDialog = true }, + onEvent = onEvent + ) + + viewState.selectedConfiguration?.let { + UARTMacroView(it, viewState.isConfigurationEdited, onEvent) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MacroSectionPreview() { + MacroSection( + viewState = UARTViewState( + configurations = listOf( + UARTConfiguration(1, "Config 1"), + UARTConfiguration(2, "Config 2") + ), + selectedConfigurationName = "Config 1", + isConfigurationEdited = false + ) + ) {} +} + +@Composable +private fun MacroDialogs( + viewState: UARTViewState, + showAddDialog: Boolean, + showDeleteDialog: Boolean, + onDismissAdd: () -> Unit, + onDismissDelete: () -> Unit, + onEvent: (UARTEvent) -> Unit +) { + if (showAddDialog) { + UARTAddConfigurationDialog(viewState, onEvent, onDismissAdd) + } + + if (showDeleteDialog) { + viewState.selectedConfiguration?.let { + DeleteConfigurationDialog(it, onEvent, onDismissDelete) + } + } + + if (viewState.showEditDialog) { + UARTAddMacroDialog(viewState.selectedMacro) { onEvent(it) } + } +} + +@Composable +private fun MacroSectionTitle( + onAddClick: () -> Unit, +) { + SectionTitle( + resId = R.drawable.ic_macro, + title = stringResource(id = R.string.uart_macros), + menu = { + CircleIcon(Icons.Default.Add, R.string.uart_configuration_add) { onAddClick() } + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun MacroSectionTitlePreview() { + MacroSectionTitle(onAddClick = {}) +} + +@Composable +private fun MacroConfigControls( + viewState: UARTViewState, + onDeleteClick: () -> Unit, + onEvent: (UARTEvent) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Box(modifier = Modifier.weight(1f)) { + UARTConfigurationPicker(viewState, onEvent) + } + + viewState.selectedConfiguration?.let { + val editIcon = + if (viewState.isConfigurationEdited) Icons.Default.EditOff else Icons.Default.Edit + val editDesc = R.string.uart_configuration_edit + + CircleIcon(editIcon, editDesc) { + onEvent(UARTEvent.OnEditConfiguration) + } + + CircleIcon( + Icons.Default.Delete, + R.string.uart_configuration_delete, + MaterialTheme.colorScheme.error + ) { onDeleteClick() } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MacroConfigControlsPreview() { + MacroConfigControls( + viewState = UARTViewState( + configurations = listOf( + UARTConfiguration(1, "Config 1"), + UARTConfiguration(2, "Config 2") + ), + selectedConfigurationName = "Config 1", + isConfigurationEdited = false + ), + onDeleteClick = {}, + onEvent = {} + ) +} + +@Composable +private fun CircleIcon( + imageVector: ImageVector, + @StringRes contentDescription: Int, + tintColor: Color = LocalContentColor.current, + onClick: () -> Unit +) { + Icon( + imageVector = imageVector, + contentDescription = stringResource(id = contentDescription), + modifier = Modifier + .clip(CircleShape) + .clickable(onClick = onClick) + .padding(8.dp), + tint = tintColor, + ) +} + +@Composable +private fun DeleteConfigurationDialog( + selectedConfiguration: UARTConfiguration, + onEvent: (UARTEvent) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + Text( + text = stringResource(id = R.string.uart_delete_dialog_title), + style = MaterialTheme.typography.headlineSmall + ) + } + }, + text = { + Text(text = stringResource(id = R.string.uart_delete_dialog_info)) + }, + confirmButton = { + TextButton(onClick = { + onEvent(UARTEvent.OnDeleteConfiguration(selectedConfiguration)) + onDismiss() + }) { + Text(text = stringResource(id = R.string.uart_delete_dialog_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = stringResource(id = R.string.uart_delete_dialog_cancel)) + } + } + ) +} + +@Preview +@Composable +private fun DeleteConfigurationDialogPreview() { + DeleteConfigurationDialog(UARTConfiguration(null, "Config 1"), {}, {}) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/OutputSection.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/OutputSection.kt new file mode 100644 index 00000000..3dee07d5 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/OutputSection.kt @@ -0,0 +1,227 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Chat +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.UARTRecord +import no.nordicsemi.android.toolbox.profile.data.UARTRecordType +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent +import no.nordicsemi.android.ui.view.SectionTitle +import java.text.SimpleDateFormat +import java.util.Locale + +@Composable +internal fun OutputSection( + records: List, + onEvent: (UARTEvent) -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + ) { + // Scrollable message area + OutlinedCard( + modifier = Modifier + .fillMaxSize() + .imePadding(), // Set a fixed height for the message area + ) { + SectionTitle( + icon = Icons.AutoMirrored.Filled.Chat, + title = "Messages", + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + menu = { Menu(onEvent) } + ) + + Spacer(modifier = Modifier.height(8.dp)) + + val listState = rememberLazyListState() + LaunchedEffect(records.size) { + listState.animateScrollToItem(records.lastIndex.coerceAtLeast(0)) + } + + LazyColumn( + state = listState, + modifier = Modifier + .padding(8.dp) + .heightIn(max = 500.dp), // Set a fixed height for the message area + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (records.isEmpty()) { + item { + Text( + text = stringResource(id = R.string.uart_output_placeholder), + modifier = Modifier.padding(8.dp) + ) + } + } else { + items(records) { record -> + when (record.type) { + UARTRecordType.INPUT -> MessageItemInput(record) + UARTRecordType.OUTPUT -> MessageItemOutput(record) + } + } + } + } + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + Spacer(modifier = Modifier.weight(1f)) + HorizontalDivider() + InputSection( + onEvent = onEvent, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun OutputSectionPreview() { + OutputSection( + records = emptyList() + ) { } +} + +@Composable +private fun MessageItemInput(record: UARTRecord) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.End + ) { + Text( + text = record.timeToString(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + Column( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp)) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(8.dp), + horizontalAlignment = Alignment.End + ) { + Text( + text = record.text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MessageItemInputPreview() { + MessageItemInput( + record = UARTRecord( + text = "Hello, World!", + type = UARTRecordType.INPUT + ) + ) +} + +@Composable +private fun MessageItemOutput(record: UARTRecord) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = record.timeToString(), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(4.dp)) + Column( + modifier = Modifier + .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp)) + .background(MaterialTheme.colorScheme.primary) + .padding(8.dp), + horizontalAlignment = Alignment.Start + ) { + Text( + text = record.text, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MessageItemOutputPreview() { + MessageItemOutput( + record = UARTRecord( + text = "Hello, World!", + type = UARTRecordType.OUTPUT + ) + ) +} + +@Composable +private fun Menu(onEvent: (UARTEvent) -> Unit) { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(id = R.string.uart_clear_items), + modifier = Modifier + .clip(CircleShape) + .clickable { onEvent(UARTEvent.ClearOutputItems) } + .padding(8.dp), + tint = MaterialTheme.colorScheme.error, + ) +} + +@Preview(showBackground = true) +@Composable +private fun MenuPreview() { + Menu(onEvent = {}) +} + +private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH) + +private fun UARTRecord.timeToString(): String { + return datFormatter.format(timestamp) +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddConfigurationDialog.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddConfigurationDialog.kt new file mode 100644 index 00000000..b799c0fb --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddConfigurationDialog.kt @@ -0,0 +1,92 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.UARTViewState +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent +import no.nordicsemi.android.ui.view.TextInputField + +@Composable +internal fun UARTAddConfigurationDialog( + viewState: UARTViewState, + onEvent: (UARTEvent) -> Unit, + onDismiss: () -> Unit +) { + var name by rememberSaveable { mutableStateOf("") } + var isError by rememberSaveable { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = { onDismiss() }, + title = { Text(stringResource(id = R.string.uart_configuration_dialog_title)) }, + text = { + TextInputField( + input = name, + label = stringResource(id = R.string.uart_configuration_hint), + placeholder = "Enter preset configuration name", + errorMessage = stringResource(id = R.string.uart_name_empty), + errorState = isError, + ) { + name = it + isError = !isNameValid(it) + } + }, + confirmButton = { + TextButton(onClick = { + if (isNameValid(name) && viewState.isNameUnique(name)) { + onDismiss() + onEvent(UARTEvent.OnAddConfiguration(name.trim())) + } else { + isError = true + } + }) { + Text(stringResource(id = R.string.uart_macro_dialog_confirm)) + } + }, + dismissButton = { + TextButton(onClick = { onDismiss() }) { + Text(stringResource(id = R.string.uart_macro_dialog_dismiss)) + } + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun UARTAddConfigurationDialogPreview() { + UARTAddConfigurationDialog( + viewState = UARTViewState( + configurations = emptyList(), + ), + onEvent = {}, + onDismiss = {} + ) +} + +/** + * Check if the name is unique. + * A name is unique if it does not exist in the list of configurations. + * + * @param name The name to check. + * @return True if the name is unique, false otherwise. + */ +private fun UARTViewState.isNameUnique(name: String): Boolean { + return configurations.none { it.name == name.trim() } +} + +/** + * Check if the name is valid. + * A name is valid if it is not empty or blank. + * + * @param name The name to check. + * @return True if the name is valid, false otherwise. + */ +private fun isNameValid(name: String): Boolean = name.trim().isNotBlank() diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddMacroDialog.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddMacroDialog.kt new file mode 100644 index 00000000..f597405f --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTAddMacroDialog.kt @@ -0,0 +1,259 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.GridItemSpan +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol +import no.nordicsemi.android.toolbox.profile.data.uart.MacroIcon +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent + +private const val GRID_SIZE = 5 + +@Composable +internal fun UARTAddMacroDialog( + macro: UARTMacro?, + onEvent: (UARTEvent) -> Unit +) { + val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) } + val command = rememberSaveable { mutableStateOf(macro?.command ?: "") } + val selectedIcon = + rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.entries.toTypedArray()[0]) } + + AlertDialog( + onDismissRequest = { onEvent(UARTEvent.OnEditFinished) }, + dismissButton = { + TextButton(onClick = { onEvent(UARTEvent.OnDeleteMacro) }) { + Text( + stringResource(id = R.string.uart_macro_dialog_delete), + color = MaterialTheme.colorScheme.error + ) + } + }, + confirmButton = { + TextButton(onClick = { + onEvent( + UARTEvent.OnCreateMacro( + UARTMacro( + selectedIcon.value, + command.value, + newLineChar.value + ) + ) + ) + }) { + Text(stringResource(id = R.string.uart_macro_dialog_confirm)) + } + }, + title = { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Text( + text = macro?.command + ?.let { stringResource(id = R.string.uart_macro_dialog_title_edit) } + ?: stringResource(id = R.string.uart_macro_dialog_title), + style = MaterialTheme.typography.headlineSmall + ) + } + }, + text = { + LazyVerticalGrid( + columns = GridCells.Fixed(GRID_SIZE), + modifier = Modifier.wrapContentHeight() + ) { + item(span = { GridItemSpan(GRID_SIZE) }) { + Column { + NewLineCharSection(newLineChar.value) { newLineChar.value = it } + + Spacer(modifier = Modifier.size(16.dp)) + } + } + + item(span = { GridItemSpan(GRID_SIZE) }) { + CommandInput(command) + } + + items(20) { item -> + val icon = MacroIcon.create(item) + val background = if (selectedIcon.value == icon) { + MaterialTheme.colorScheme.primaryContainer + } else { + Color.Transparent + } + + Image( + painter = painterResource(id = icon.toResId()), + contentDescription = stringResource(id = R.string.uart_macro_icon), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(10.dp)) + .clickable { selectedIcon.value = icon } + .background(background) + ) + } + } + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun UARTAddMacroDialogPreview() { + UARTAddMacroDialog( + UARTMacro( + MacroIcon.entries[0], + "AT+", + MacroEol.LF + ) + ) {} +} + +@Composable +private fun CommandInput(command: MutableState) { + Column { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth(), + value = command.value, + label = { Text(stringResource(id = R.string.uart_macro_dialog_command)) }, + onValueChange = { + command.value = it + } + ) + + Spacer(modifier = Modifier.size(16.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun CommandInputPreview() { + val command = rememberSaveable { mutableStateOf("AT+") } + CommandInput(command) +} + +@Composable +private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) -> Unit) { + + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "End of Line (EOL)", + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.alpha(0.5f) + ) + EolTab( + checkedItem = checkedItem, + onItemClick = onItemClick + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun NewLineCharSectionPreview() { + val newLineChar = rememberSaveable { mutableStateOf(MacroEol.LF) } + NewLineCharSection(newLineChar.value) {} +} + +@Composable +private fun EolTab( + checkedItem: MacroEol = MacroEol.LF, + onItemClick: (MacroEol) -> Unit, +) { + Box( + modifier = Modifier.clip(RoundedCornerShape(8.dp)) + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + MacroEol.entries.forEachIndexed { index, it -> + val selected = it == checkedItem + val clip = if (selected) RoundedCornerShape(8.dp) else RoundedCornerShape(0.dp) + val textColor = if (selected) { + MaterialTheme.colorScheme.onPrimary + } else { + MaterialTheme.colorScheme.onSurface + } + val backgroundColor by animateColorAsState( + if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surface, + label = "BackgroundAnimation" + ) + + Box( + modifier = Modifier + .weight(1f) + .clip(clip) + .background(color = backgroundColor) + .clickable { onItemClick(it) }, + contentAlignment = Alignment.Center, + ) { + Text( + it.toString(), + modifier = Modifier.padding(8.dp), + color = textColor, + ) + } + if ((index < MacroEol.entries.size - 1) && !selected) + VerticalDivider( + modifier = Modifier + .height(IntrinsicSize.Max) + .background(MaterialTheme.colorScheme.onSurface) + ) + } + } + } +} + +@Preview +@Composable +private fun EOLTabPreview() { + NordicTheme { + EolTab {} + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTConfigurationPicker.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTConfigurationPicker.kt new file mode 100644 index 00000000..dfc06952 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTConfigurationPicker.kt @@ -0,0 +1,96 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType.Companion.PrimaryNotEditable +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.UARTViewState +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun UARTConfigurationPicker( + state: UARTViewState, + onEvent: (UARTEvent) -> Unit +) { + var expanded by rememberSaveable { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + + ) { + OutlinedButton( + onClick = { }, + modifier = Modifier.menuAnchor(PrimaryNotEditable) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + val title = + state.selectedConfigurationName + ?: stringResource(id = R.string.uart_configuration_picker_hint) + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + Icon(Icons.Default.ArrowDropDown, contentDescription = "") + } + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + state.configurations.forEach { configuration -> + DropdownMenuItem( + text = { Text(text = configuration.name) }, + onClick = { + onEvent(UARTEvent.OnConfigurationSelected(configuration)) + expanded = false + }, + ) + } + } + + } +} + +@Preview(showBackground = true) +@Composable +private fun UARTConfigurationPickerPreview() { + UARTConfigurationPicker( + state = UARTViewState( + configurations = listOf( + UARTConfiguration(1, "Config 1"), + UARTConfiguration(2, "Config 2"), + UARTConfiguration(3, "Config 3"), + ), + ) + ) {} +} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMacroView.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTMacroView.kt similarity index 51% rename from profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMacroView.kt rename to profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTMacroView.kt index ebb24275..859c7da8 100644 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMacroView.kt +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTMacroView.kt @@ -1,60 +1,33 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view +package no.nordicsemi.android.toolbox.profile.view.uart import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.uart.data.UARTMacro +import no.nordicsemi.android.common.theme.NordicTheme +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent private val divider = 4.dp @@ -62,55 +35,64 @@ private val divider = 4.dp internal fun UARTMacroView( configuration: UARTConfiguration, isEdited: Boolean, - onEvent: (UARTViewEvent) -> Unit + onEvent: (UARTEvent) -> Unit ) { BoxWithConstraints { val buttonSize = if (maxWidth < 260.dp) { 48.dp //Minimum touch area - } else { + } else { 80.dp } - Column(modifier = Modifier.padding(horizontal = 16.dp)) { - - Row { + Column( + verticalArrangement = Arrangement.spacedBy(divider), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(divider) + ) { Item(configuration, isEdited, 0, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 1, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 2, buttonSize, onEvent) } - Spacer(modifier = Modifier.size(divider)) - - Row { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(divider) + ) { Item(configuration, isEdited, 3, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 4, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 5, buttonSize, onEvent) } - - Spacer(modifier = Modifier.size(divider)) - - Row { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(divider) + ) { Item(configuration, isEdited, 6, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 7, buttonSize, onEvent) - Spacer(modifier = Modifier.size(divider)) Item(configuration, isEdited, 8, buttonSize, onEvent) } } } } +@Preview(showBackground = true) +@Composable +private fun UARTMacroViewPreview() { + UARTMacroView( + configuration = UARTConfiguration(1, "Config 1"), + isEdited = false + ) {} +} + @Composable private fun Item( configuration: UARTConfiguration, isEdited: Boolean, position: Int, buttonSize: Dp, - onEvent: (UARTViewEvent) -> Unit + onEvent: (UARTEvent) -> Unit ) { val macro = configuration.macros.getOrNull(position) @@ -127,7 +109,7 @@ private fun MacroButton( position: Int, isEdited: Boolean, buttonSize: Dp, - onEvent: (UARTViewEvent) -> Unit + onEvent: (UARTEvent) -> Unit ) { Image( painter = painterResource(id = macro.icon.toResId()), @@ -138,12 +120,12 @@ private fun MacroButton( .clip(RoundedCornerShape(10.dp)) .clickable { if (isEdited) { - onEvent(OnEditMacro(position)) + onEvent(UARTEvent.OnEditMacro(position)) } else { - onEvent(OnRunMacro(macro)) + onEvent(UARTEvent.OnRunMacro(macro)) } } - .background(getBackground(isEdited)) + .background(getBackground(isEdited, macro)) ) } @@ -152,7 +134,7 @@ private fun EmptyButton( isEdited: Boolean, position: Int, buttonSize: Dp, - onEvent: (UARTViewEvent) -> Unit + onEvent: (UARTEvent) -> Unit ) { Box( modifier = Modifier @@ -160,18 +142,38 @@ private fun EmptyButton( .clip(RoundedCornerShape(10.dp)) .clickable { if (isEdited) { - onEvent(OnEditMacro(position)) + onEvent(UARTEvent.OnEditMacro(position)) } } .background(getBackground(isEdited)) ) } +@Preview(showBackground = true) @Composable -private fun getBackground(isEdited: Boolean): Color { - return if (!isEdited) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.tertiary +private fun EmptyButtonPreview() { + NordicTheme { + EmptyButton(false, 0, 80.dp) {} } } + +@Composable +private fun getBackground(isEdited: Boolean, macro: UARTMacro? = null): Color { + return when { + !isEdited && macro != null -> { + MaterialTheme.colorScheme.primary + } + + isEdited && macro == null -> { + MaterialTheme.colorScheme.tertiary.copy(alpha = 0.5f) + } + + macro == null -> { + ButtonDefaults.buttonColors().disabledContainerColor + } + + else -> { + MaterialTheme.colorScheme.tertiary + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTScreen.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTScreen.kt new file mode 100644 index 00000000..8389965c --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UARTScreen.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import no.nordicsemi.android.toolbox.profile.viewmodel.UARTEvent +import no.nordicsemi.android.toolbox.profile.viewmodel.UartViewModel + +@Composable +internal fun UARTScreen(maxValueLength: Int?) { + val uartViewModel = hiltViewModel() + val state by uartViewModel.uartState.collectAsStateWithLifecycle() + val onEvent: (UARTEvent) -> Unit = { uartViewModel.onEvent(it) } + + LaunchedEffect(key1 = maxValueLength != null) { + if (maxValueLength != null) + onEvent(UARTEvent.SetMaxValueLength(maxValueLength)) + } + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + MacroSection(state.uartViewState, onEvent) + OutputSection( + records = state.messages, + onEvent = onEvent + ) + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UartUiMapper.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UartUiMapper.kt new file mode 100644 index 00000000..2d1dd6c1 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/UartUiMapper.kt @@ -0,0 +1,31 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import androidx.annotation.DrawableRes +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.data.uart.MacroIcon + +@DrawableRes +fun MacroIcon.toResId(): Int { + return when (this) { + MacroIcon.LEFT -> R.drawable.ic_uart_left + MacroIcon.UP -> R.drawable.ic_uart_up + MacroIcon.RIGHT -> R.drawable.ic_uart_right + MacroIcon.DOWN -> R.drawable.ic_uart_down + MacroIcon.SETTINGS -> R.drawable.ic_uart_settings + MacroIcon.REW -> R.drawable.ic_uart_rewind + MacroIcon.PLAY -> R.drawable.ic_uart_play + MacroIcon.PAUSE -> R.drawable.ic_uart_pause + MacroIcon.STOP -> R.drawable.ic_uart_stop + MacroIcon.FWD -> R.drawable.ic_uart_forward + MacroIcon.INFO -> R.drawable.ic_uart_about + MacroIcon.NUMBER_1 -> R.drawable.ic_uart_1 + MacroIcon.NUMBER_2 -> R.drawable.ic_uart_2 + MacroIcon.NUMBER_3 -> R.drawable.ic_uart_3 + MacroIcon.NUMBER_4 -> R.drawable.ic_uart_4 + MacroIcon.NUMBER_5 -> R.drawable.ic_uart_5 + MacroIcon.NUMBER_6 -> R.drawable.ic_uart_6 + MacroIcon.NUMBER_7 -> R.drawable.ic_uart_7 + MacroIcon.NUMBER_8 -> R.drawable.ic_uart_8 + MacroIcon.NUMBER_9 -> R.drawable.ic_uart_9 + } +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/rememberImeState.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/rememberImeState.kt new file mode 100644 index 00000000..89a247bc --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/view/uart/rememberImeState.kt @@ -0,0 +1,33 @@ +package no.nordicsemi.android.toolbox.profile.view.uart + +import android.view.ViewTreeObserver +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalView +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat + +@Composable +fun rememberImeState(): State { + val imeState = remember { + mutableStateOf(false) + } + + val view = LocalView.current + DisposableEffect(view) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val isKeyboardOpen = ViewCompat.getRootWindowInsets(view) + ?.isVisible(WindowInsetsCompat.Type.ime()) ?: true + imeState.value = isKeyboardOpen + } + + view.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + view.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + return imeState +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BPSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BPSViewModel.kt new file mode 100644 index 00000000..c8b30143 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BPSViewModel.kt @@ -0,0 +1,66 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.BPSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.BPSServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +@HiltViewModel +internal class BPSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + // StateFlow to hold the selected temperature unit + private val _bpsServiceState = MutableStateFlow(BPSServiceData()) + val bpsServiceState = _bpsServiceState.asStateFlow() + private val address = parameterOf(ProfileDestinationId) + + init { + observeBPSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.BPS]. + */ + private fun observeBPSProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.BPS } + .forEach { _ -> + startBPSService(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the BPS service for the given address and updates the state with the received data. + */ + private fun startBPSService(address: String) = + BPSRepository.getData(address).onEach { + _bpsServiceState.value = _bpsServiceState.value.copy( + profile = it.profile, + bloodPressureMeasurement = it.bloodPressureMeasurement, + intermediateCuffPressure = it.intermediateCuffPressure, + bloodPressureFeature = it.bloodPressureFeature, + ) + }.launchIn(viewModelScope) + +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BatteryViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BatteryViewModel.kt new file mode 100644 index 00000000..23e07f54 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/BatteryViewModel.kt @@ -0,0 +1,65 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.BatteryRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.BatteryServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +@HiltViewModel +internal class BatteryViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + val address = parameterOf(ProfileDestinationId) + + // StateFlow to hold the selected temperature unit + private val _batteryServiceState = MutableStateFlow(BatteryServiceData()) + val batteryServiceState = _batteryServiceState.asStateFlow() + + init { + observeBatteryProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.BATTERY]. + */ + private fun observeBatteryProfile() = viewModelScope.launch { + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.BATTERY } + .forEach { _ -> + startBatteryService(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the Battery Service and observes battery level changes. + * + * @param address The address of the peripheral device. + */ + private fun startBatteryService(address: String) = BatteryRepository.getData(address) + .onEach { batteryServiceState -> + // Handle the temperature data, e.g., update UI or state + // This is where you would emit the temperature data to your UI + _batteryServiceState.value = batteryServiceState + }.launchIn(viewModelScope) + +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CGMSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CGMSViewModel.kt new file mode 100644 index 00000000..ee5b396d --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CGMSViewModel.kt @@ -0,0 +1,90 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.manager.repository.CGMRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.CGMServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// CGMS Profile Events +internal sealed interface CGMSEvent { + data class OnWorkingModeSelected( + val workingMode: WorkingMode + ) : CGMSEvent +} + +@HiltViewModel +internal class CGMSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + // StateFlow to hold the selected temperature unit + private val _cgmsServiceState = MutableStateFlow(CGMServiceData()) + val channelSoundingState = _cgmsServiceState.asStateFlow() + + private val address = parameterOf(ProfileDestinationId) + + init { + observeCGMSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.CGM]. + */ + private fun observeCGMSProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.CGM } + .forEach { _ -> + startCGMSService(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the CGMS service and observes CGMS profile data changes. + */ + private fun startCGMSService(address: String) = + CGMRepository.getData(address).onEach { + _cgmsServiceState.value = _cgmsServiceState.value.copy( + profile = it.profile, + records = it.records, + requestStatus = it.requestStatus, + workingMode = it.workingMode, + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the CGMS profile. + */ + fun onEvent(event: CGMSEvent) { + when (event) { + is CGMSEvent.OnWorkingModeSelected -> viewModelScope.launch { + // Handle the working mode selection event + CGMRepository.requestRecord( + deviceId = address, + workingMode = event.workingMode + ) + } + } + } + +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CSCViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CSCViewModel.kt new file mode 100644 index 00000000..b0474e6a --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/CSCViewModel.kt @@ -0,0 +1,76 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit +import no.nordicsemi.android.toolbox.profile.parser.csc.WheelSize +import no.nordicsemi.android.toolbox.profile.manager.repository.CSCRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.CSCServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// CSC Profile Events +internal sealed interface CSCEvent { + data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCEvent + data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCEvent +} + +@HiltViewModel +internal class CSCViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _cscState = MutableStateFlow(CSCServiceData()) + val cscState = _cscState.asStateFlow() + val address = parameterOf(ProfileDestinationId) + + init { + observeCSCProfile() + } + + private fun observeCSCProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.CSC } + .forEach { _ -> + startCSCService(peripheral.address) + } + } + } + }.launchIn(this) + } + + private fun startCSCService(address: String) = + // Start the LBS service and observe location changes + CSCRepository.getData(address).onEach { + _cscState.value = _cscState.value.copy( + profile = it.profile, + data = it.data, + speedUnit = it.speedUnit + ) + }.launchIn(viewModelScope) + + fun onEvent(event: CSCEvent) { + when (event) { + is CSCEvent.OnWheelSizeSelected -> + CSCRepository.setWheelSize(address, event.wheelSize) + + is CSCEvent.OnSelectedSpeedUnitSelected -> + CSCRepository.setSpeedUnit(address, event.selectedSpeedUnit) + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ChannelSoundingViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ChannelSoundingViewModel.kt new file mode 100644 index 00000000..3c82e192 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ChannelSoundingViewModel.kt @@ -0,0 +1,83 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import android.content.Context +import android.os.Build +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.ChannelSoundingRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.ChannelSoundingServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import no.nordicsemi.android.toolbox.profile.repository.channelSounding.ChannelSoundingManager +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +internal class ChannelSoundingViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + @param:ApplicationContext private val context: Context, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + // StateFlow to hold the selected temperature unit + private val _channelSoundingServiceState = MutableStateFlow(ChannelSoundingServiceData()) + val channelSoundingState = _channelSoundingServiceState.asStateFlow() + + private val address = parameterOf(ProfileDestinationId) + + init { + observeChannelSoundingProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.CHANNEL_SOUNDING]. + */ + private fun observeChannelSoundingProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.CHANNEL_SOUNDING } + .forEach { _ -> + startChannelSounding(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the Channel Sounding service and observes channel sounding profile data changes. + */ + private fun startChannelSounding(address: String) { + ChannelSoundingRepository.getData(address).onEach { + _channelSoundingServiceState.value = _channelSoundingServiceState.value.copy( + profile = it.profile + ) + }.launchIn(viewModelScope) + if (Build.VERSION.SDK_INT >= 36) { + viewModelScope.launch { + try { + ChannelSoundingManager.addDeviceToRangingSession(context, address) + } catch (e: Exception) { + Timber.e(" ${e.message}") + } + } + } else { + Timber.tag("Channel_Sounding") + .d("Channel Sounding is not available in this Android version.") + } + } + +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DeviceConnectionState.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DeviceConnectionState.kt new file mode 100644 index 00000000..aa626305 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DeviceConnectionState.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import no.nordicsemi.android.service.profile.DeviceDisconnectionReason +import no.nordicsemi.android.toolbox.profile.manager.ServiceManager +import no.nordicsemi.kotlin.ble.client.android.Peripheral + +internal data class DeviceData( + val peripheral: Peripheral? = null, + val peripheralProfileMap: Map> = emptyMap(), + val isMissingServices: Boolean = false, + val maxValueLength: Int? = null, +) + +internal sealed class DeviceConnectionState { + data object Idle : DeviceConnectionState() + data object Connecting : DeviceConnectionState() + data object Disconnecting : DeviceConnectionState() + data class Connected(val data: DeviceData) : DeviceConnectionState() + data class Disconnected( + val device: Peripheral? = null, + val reason: DeviceDisconnectionReason? + ) : DeviceConnectionState() +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DirectionFinderViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DirectionFinderViewModel.kt new file mode 100644 index 00000000..7f1d92b7 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/DirectionFinderViewModel.kt @@ -0,0 +1,110 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode +import no.nordicsemi.android.toolbox.profile.manager.repository.DFSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.DFSServiceData +import no.nordicsemi.android.toolbox.profile.data.directionFinder.MeasurementSection +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +internal sealed interface DFSEvent { + data object OnAvailableDistanceModeRequest : DFSEvent + data object OnCheckDistanceModeRequest : DFSEvent + data class OnRangeChangedEvent(val range: Range) : DFSEvent + data class OnDistanceModeSelected(val mode: DistanceMode) : DFSEvent + data class OnDetailsSectionParamsSelected(val section: MeasurementSection) : DFSEvent + data class OnBluetoothDeviceSelected(val device: PeripheralBluetoothAddress) : DFSEvent +} + +@HiltViewModel +internal class DirectionFinderViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _dfsState = MutableStateFlow(DFSServiceData()) + val dfsState = _dfsState.asStateFlow() + private val address = parameterOf(ProfileDestinationId) + + init { + observeDFSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.DFS]. + */ + private fun observeDFSProfile() = + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.DFS } + .forEach { _ -> + startDFSService() + } + } + } + }.launchIn(viewModelScope) + + /** + * Starts the DFS service and observes direction finder profile data changes. + */ + private fun startDFSService() = + DFSRepository.getData(address).onEach { + _dfsState.value = _dfsState.value.copy( + requestStatus = it.requestStatus, + data = it.data, + ddfFeature = it.ddfFeature, + selectedDevice = it.selectedDevice, + distanceRange = it.distanceRange, + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the Direction Finder Service (DFS). + */ + fun onEvent(event: DFSEvent) { + when (event) { + DFSEvent.OnAvailableDistanceModeRequest -> viewModelScope.launch { + DFSRepository.checkAvailableFeatures(address) + } + + DFSEvent.OnCheckDistanceModeRequest -> viewModelScope.launch { + DFSRepository.checkCurrentDistanceMode(address) + } + + is DFSEvent.OnRangeChangedEvent -> { + DFSRepository.updateDistanceRange(address, event.range) + } + + is DFSEvent.OnDistanceModeSelected -> { + viewModelScope.launch { + DFSRepository.enableDistanceMode(address, event.mode) + } + } + + is DFSEvent.OnDetailsSectionParamsSelected -> { + DFSRepository.updateDetailsSection(address, event.section) + } + + is DFSEvent.OnBluetoothDeviceSelected -> DFSRepository.updateSelectedDevice( + address, + event.device + ) + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/GLSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/GLSViewModel.kt new file mode 100644 index 00000000..dec36c6d --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/GLSViewModel.kt @@ -0,0 +1,81 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.manager.repository.GLSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.GLSServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// GLSProfile Events +internal sealed interface GLSEvent { + data class OnWorkingModeSelected( + val workingMode: WorkingMode + ) : GLSEvent + +} + +@HiltViewModel +internal class GLSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + val address = parameterOf(ProfileDestinationId) + private val _glsState = MutableStateFlow(GLSServiceData()) + val glsState = _glsState.asStateFlow() + + init { + observeGLSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.GLS]. + */ + private fun observeGLSProfile() = deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.GLS } + .forEach { _ -> + startGLSService(peripheral.address) + } + } + } + }.launchIn(viewModelScope) + + /** + * Starts the GLS service and observes glucose profile data changes. + */ + private fun startGLSService(address: String) = + GLSRepository.getData(address).onEach { + _glsState.value = _glsState.value.copy( + profile = it.profile, + records = it.records, + requestStatus = it.requestStatus, + workingMode = it.workingMode, + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the GLS profile. + */ + fun onEvent(event: GLSEvent) { + when (event) { + is GLSEvent.OnWorkingModeSelected -> viewModelScope.launch { + GLSRepository.requestRecord(address, event.workingMode) + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HRSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HRSViewModel.kt new file mode 100644 index 00000000..3a83d50f --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HRSViewModel.kt @@ -0,0 +1,78 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.HRSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.HRSServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// Heart Rate Service (HRS) Profile Events +internal sealed interface HRSEvent { + data object SwitchZoomEvent : HRSEvent +} + +@HiltViewModel +internal class HRSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _hrsState = MutableStateFlow(HRSServiceData()) + val hrsState = _hrsState.asStateFlow() + + private val address = parameterOf(ProfileDestinationId) + + init { + observeHRSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.HRS]. + */ + private fun observeHRSProfile() = + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.HRS } + .forEach { _ -> + startHRSService(peripheral.address) + } + } + } + }.launchIn(viewModelScope) + + /** + * Starts the HRS service and observes heart rate data changes. + */ + private fun startHRSService(address: String) = + HRSRepository.getData(address).onEach { + _hrsState.value = _hrsState.value.copy( + profile = it.profile, + heartRate = it.heartRate, + data = it.data, + bodySensorLocation = it.bodySensorLocation, + zoomIn = it.zoomIn, + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the HRS profile. + */ + fun onEvent(event: HRSEvent) { + when (event) { + HRSEvent.SwitchZoomEvent -> HRSRepository.updateZoomIn(address) + } + } + +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HTSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HTSViewModel.kt new file mode 100644 index 00000000..be37b490 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/HTSViewModel.kt @@ -0,0 +1,93 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.HTSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.HTSServiceData +import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// HTS Profile Events +internal sealed interface HTSEvent { + data class OnTemperatureUnitSelected( + val value: TemperatureUnit + ) : HTSEvent +} + +@HiltViewModel +internal class HTSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + // StateFlow to hold the selected temperature unit + private val _htsServiceState = MutableStateFlow(HTSServiceData()) + val htsServiceState = _htsServiceState.asStateFlow() + val address = parameterOf(ProfileDestinationId) + + init { + observeHtsProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.HTS]. + */ + private fun observeHtsProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.HTS } + .forEach { _ -> + startHTSService(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the HTS service and observes temperature changes. + * + * @param address The address of the peripheral device. + */ + private fun startHTSService(address: String) { + // Start the HTS service and observe temperature changes + HTSRepository.getData(address) + .onEach { htsServiceData -> + _htsServiceState.value = _htsServiceState.value.copy( + data = htsServiceData.data, + temperatureUnit = htsServiceData.temperatureUnit, + ) + }.launchIn(viewModelScope) + } + + /** + * Handles events related to the HTS profile. + * + * @param event The event to handle. + */ + fun onEvent(event: HTSEvent) { + when (event) { + is HTSEvent.OnTemperatureUnitSelected -> { + // Handle the temperature unit selection event + HTSRepository.onTemperatureUnitChange( + deviceId = address, + unit = event.value + ) + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/LBSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/LBSViewModel.kt new file mode 100644 index 00000000..79bbbb90 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/LBSViewModel.kt @@ -0,0 +1,97 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.LBSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.LBSServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// LBS Profile Events +internal sealed interface LBSEvent { + data class OnLedStateChanged( + val value: Boolean + ) : LBSEvent + + data class OnButtonStateChanged( + val value: Boolean + ) : LBSEvent +} + +@HiltViewModel +internal class LBSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _lbsState = MutableStateFlow(LBSServiceData()) + val lbsState = _lbsState.asStateFlow() + val address = parameterOf(ProfileDestinationId) + + init { + observeLbsProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.LBS]. + */ + private fun observeLbsProfile() = viewModelScope.launch { + // update state or emit to UI + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.LBS } + .forEach { _ -> + startLBSService(peripheral.address) + } + } + } + }.launchIn(this) + } + + /** + * Starts the LBS service and observes location changes. + */ + private fun startLBSService(address: String) { + // Start the LBS service and observe location changes + LBSRepository.getData(address).onEach { + _lbsState.value = _lbsState.value.copy( + profile = it.profile, + data = it.data, + ) + }.launchIn(viewModelScope) + } + + /** + * Handles events related to the LBS profile. + */ + fun onEvent(event: LBSEvent) { + when (event) { + is LBSEvent.OnLedStateChanged -> { + // Handle LED state change + viewModelScope.launch { + LBSRepository.writeToBlinkyLED(address, event.value) + } + } + + is LBSEvent.OnButtonStateChanged -> { + // Handle button state change + LBSRepository.updateButtonState( + deviceId = address, + buttonState = event.value + ) + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ProfileViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ProfileViewModel.kt new file mode 100644 index 00000000..be6c9529 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ProfileViewModel.kt @@ -0,0 +1,308 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import no.nordicsemi.android.analytics.AppAnalytics +import no.nordicsemi.android.analytics.Link +import no.nordicsemi.android.analytics.ProfileOpenEvent +import no.nordicsemi.android.common.logger.LoggerLauncher +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.log.LogSession +import no.nordicsemi.android.log.timber.nRFLoggerTree +import no.nordicsemi.android.service.profile.CustomReason +import no.nordicsemi.android.service.profile.ProfileServiceManager +import no.nordicsemi.android.service.profile.ServiceApi +import no.nordicsemi.android.service.profile.StateReason +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.R +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import no.nordicsemi.android.ui.view.internal.DisconnectReason +import no.nordicsemi.kotlin.ble.client.android.Peripheral +import no.nordicsemi.kotlin.ble.core.ConnectionState +import timber.log.Timber +import java.lang.ref.WeakReference +import javax.inject.Inject + +internal sealed interface ConnectionEvent { + + data class OnRetryClicked(val device: String) : ConnectionEvent + + data object NavigateUp : ConnectionEvent + + data class DisconnectEvent(val device: String) : ConnectionEvent + + data object OpenLoggerEvent : ConnectionEvent + + data object RequestMaxValueLength : ConnectionEvent +} + +@HiltViewModel +internal class ProfileViewModel @Inject constructor( + private val profileServiceManager: ProfileServiceManager, + private val navigator: Navigator, + private val deviceRepository: DeviceRepository, + private val analytics: AppAnalytics, + @param:ApplicationContext private val context: Context, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + val address: String = parameterOf(ProfileDestinationId) + private val _deviceState = MutableStateFlow(DeviceConnectionState.Idle) + val deviceState = _deviceState.asStateFlow() + + private var logger: nRFLoggerTree? = null + private var serviceApi: WeakReference? = null + private var peripheral: Peripheral? = null + private var job: Job? = null + + init { + connectToPeripheral(address) + observeConnectedDevices() + initLogger() + } + + private suspend fun getServiceApi(): ServiceApi? { + if (serviceApi == null) { + serviceApi = WeakReference(profileServiceManager.bindService()) + } + return serviceApi?.get() + } + + private fun initLogger() { + logger = nRFLoggerTree(context, address, context.getString(R.string.app_name)).also { + Timber.plant(it) + } + } + + private fun observeConnectedDevices() = viewModelScope.launch { + getServiceApi()?.let { api -> + peripheral = api.getPeripheralById(address) + + api.connectedDevices + .onEach { peripheralProfileMap -> + deviceRepository.updateConnectedDevices(peripheralProfileMap) + + peripheralProfileMap[peripheral?.address]?.let { pair -> + deviceRepository.updateProfilePeripheralPair(pair.first, pair.second) + _deviceState.update { + DeviceConnectionState.Connected( + DeviceData( + peripheral = pair.first, + peripheralProfileMap = mapOf(pair.first to pair.second), + ) + ) + + } + } + + // Send each profile handler to a shared flow that profile ViewModels can observe + peripheralProfileMap[peripheral?.address]?.second?.forEach { handler -> + deviceRepository.updateAnalytics(address, handler.profile) + + } + }.launchIn(viewModelScope) + + updateConnectionState(api, address, peripheral?.isConnected == true) + } + } + + + /** + * Connect to the peripheral with the given address. Before connecting, the service must be bound. + * The service will be started if not already running. + * @param deviceAddress the address of the peripheral to connect to. + */ + private fun connectToPeripheral(deviceAddress: String) = viewModelScope.launch { + // Connect to the peripheral + getServiceApi()?.let { + if (peripheral == null) peripheral = it.getPeripheralById(address) + if (peripheral?.isConnected != true) { + profileServiceManager.connectToPeripheral(deviceAddress) + } + } + } + + + /** + * Update the service data, including connection state and peripheral data. + * @param api the service API. + * @param deviceAddress the address of the connected device. + */ + private fun updateConnectionState( + api: ServiceApi, + deviceAddress: String, + isAlreadyConnected: Boolean + ) { + // Drop the first default state (Closed) before connection. + job = api.getConnectionState(deviceAddress) + ?.drop(if (isAlreadyConnected) 0 else 1) + ?.onEach { connectionState -> + if (peripheral == null) peripheral = api.getPeripheralById(address) + when (connectionState) { + ConnectionState.Connected -> { + _deviceState.update { currentState -> + val currentData = + (currentState as? DeviceConnectionState.Connected)?.data + DeviceConnectionState.Connected( + currentData?.copy( + peripheral = peripheral + ) ?: DeviceData(peripheral = peripheral) + ) + + }.apply { checkForMissingServices(api) } + } + + is ConnectionState.Disconnected -> { + _deviceState.update { + DeviceConnectionState.Disconnected( + peripheral, + StateReason(connectionState.reason) + ) + }.also { + // Remove the analytics logged profiles for the disconnected device. + deviceRepository.removeLoggedProfile(deviceAddress) + } + } + + ConnectionState.Closed -> { + unbindService() + api.disconnectionReason.onEach { reason -> + if (reason != null) { + _deviceState.update { + DeviceConnectionState.Disconnected(peripheral, reason) + } + } else { + _deviceState.update { + DeviceConnectionState.Disconnected( + peripheral, + CustomReason(DisconnectReason.UNKNOWN) + ) + } + } + }.launchIn(viewModelScope) + job?.cancel() + } + + ConnectionState.Connecting -> { + _deviceState.update { + DeviceConnectionState.Connecting + } + } + + ConnectionState.Disconnecting -> { + // Update the state to disconnecting. + _deviceState.update { + DeviceConnectionState.Disconnecting + } + } + } + } + ?.onCompletion { + job?.cancel() + job = null + }?.launchIn(viewModelScope) + } + + /** + * Check for missing services. + */ + private fun checkForMissingServices(api: ServiceApi) = + api.isMissingServices.onEach { isMissing -> + (_deviceState.value as? DeviceConnectionState.Connected)?.let { connectedState -> + _deviceState.update { + connectedState.copy( + data = connectedState.data.copy(isMissingServices = isMissing) + ) + } + } + }.launchIn(viewModelScope) + + + /** + * Unbind the service. + */ + private fun unbindService() { + serviceApi?.let { profileServiceManager.unbindService() } + serviceApi = null + } + + fun onConnectionEvent(event: ConnectionEvent) { + when (event) { + is ConnectionEvent.DisconnectEvent -> disconnect(event.device) + ConnectionEvent.NavigateUp -> { + // If the device is connected and missing services, disconnect it before navigating up. + if ((_deviceState.value as? DeviceConnectionState.Connected)?.data?.isMissingServices == true) { + disconnect(address) + } + navigator.navigateUp() + } + + is ConnectionEvent.OnRetryClicked -> reconnectDevice(event.device) + ConnectionEvent.OpenLoggerEvent -> openLogger() + ConnectionEvent.RequestMaxValueLength -> viewModelScope.launch(Dispatchers.IO) { + // Request maximum MTU size if it is not already set. + val mtuSize = getServiceApi()?.getMaxWriteValue(address) + _deviceState.update { currentState -> + val currentData = + (currentState as? DeviceConnectionState.Connected)?.data + if (currentData != null && currentData.maxValueLength == mtuSize) { + // No need to update if the max value length is already set. + return@update currentState + } + DeviceConnectionState.Connected( + currentData?.copy( + maxValueLength = mtuSize + ) ?: DeviceData( + peripheral = peripheral, + maxValueLength = mtuSize + ) + ) + } + } + } + } + + /** + * Disconnect the device with the given address and navigate back. + * @param device the address of the device to disconnect. + */ + private fun disconnect(device: String) = viewModelScope.launch { + getServiceApi()?.disconnect(device) + unbindService() + } + + /** + * Launch the logger activity. + */ + private fun openLogger() { + // Log the event in the analytics. + analytics.logEvent(ProfileOpenEvent(Link.LOGGER)) + LoggerLauncher.launch(context, logger?.session as? LogSession) + } + + /** + * Reconnect to the device with the given address. + * + * @param deviceAddress the address of the device to reconnect to. + */ + private fun reconnectDevice(deviceAddress: String) = viewModelScope.launch { + getServiceApi()?.let { + connectToPeripheral(deviceAddress) + updateConnectionState(it, deviceAddress, false) + } + } + +} diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/RSCSViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/RSCSViewModel.kt new file mode 100644 index 00000000..9a668ac9 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/RSCSViewModel.kt @@ -0,0 +1,78 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit +import no.nordicsemi.android.toolbox.profile.manager.repository.RSCSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.RSCSServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// RSCS Profile Events +internal sealed interface RSCSEvent { + data class OnSelectedSpeedUnitSelected(val rscsSettingsUnit: RSCSSettingsUnit) : RSCSEvent +} + +@HiltViewModel +internal class RSCSViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _rscsState = MutableStateFlow(RSCSServiceData()) + val rscsState = _rscsState.asStateFlow() + private val address = parameterOf(ProfileDestinationId) + + init { + observeRSCSProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.RSCS]. + */ + private fun observeRSCSProfile() = + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.RSCS } + .forEach { _ -> + startRSCSService(peripheral.address) + } + } + } + }.launchIn(viewModelScope) + + /** + * Starts the RSCS service and observes running speed, cadence, and other data changes. + */ + private fun startRSCSService(address: String) = + RSCSRepository.getData(address).onEach { + _rscsState.value = _rscsState.value.copy( + profile = it.profile, + data = it.data, + unit = it.unit, + feature = it.feature, + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the RSCS profile. + */ + fun onEvent(event: RSCSEvent) { + when (event) { + is RSCSEvent.OnSelectedSpeedUnitSelected -> { + RSCSRepository.updateUnitSettings(address, event.rscsSettingsUnit) + } + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ThroughputViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ThroughputViewModel.kt new file mode 100644 index 00000000..f4793e60 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/ThroughputViewModel.kt @@ -0,0 +1,92 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.profile.manager.repository.ThroughputRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.ThroughputInputType +import no.nordicsemi.android.toolbox.profile.data.ThroughputServiceData +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import javax.inject.Inject + +// Throughput events. +internal sealed interface ThroughputEvent { + data class OnWriteData( + val writeType: ThroughputInputType, + ) : ThroughputEvent + + data class UpdateMaxWriteValueLength( + val maxWriteValueLength: Int? = null, + ) : ThroughputEvent + +} + +@HiltViewModel +internal class ThroughputViewModel @Inject constructor( + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _throughputState = MutableStateFlow(ThroughputServiceData()) + val throughputState = _throughputState.asStateFlow() + private val address = parameterOf(ProfileDestinationId) + + init { + observeThroughputProfile() + } + + /** + * Observes the [DeviceRepository.profileHandlerFlow] from the [deviceRepository] that contains [Profile.THROUGHPUT]. + */ + private fun observeThroughputProfile() = + deviceRepository.profileHandlerFlow + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.THROUGHPUT } + .forEach { _ -> + startThroughputService(peripheral.address) + } + } + } + }.launchIn(viewModelScope) + + /** + * Starts the Throughput service and observes throughput data changes. + */ + private fun startThroughputService(address: String) = + ThroughputRepository.getData(address).onEach { + _throughputState.value = _throughputState.value.copy( + profile = it.profile, + throughputData = it.throughputData, + writingStatus = it.writingStatus, + maxWriteValueLength = it.maxWriteValueLength + ) + }.launchIn(viewModelScope) + + /** + * Handles events related to the Throughput profile. + */ + fun onEvent(event: ThroughputEvent) { + when (event) { + is ThroughputEvent.OnWriteData -> viewModelScope.launch { + ThroughputRepository.sendDataToDK(address, event.writeType) + } + + is ThroughputEvent.UpdateMaxWriteValueLength -> + ThroughputRepository.updateMaxWriteValueLength( + deviceId = address, + mtuSize = event.maxWriteValueLength + ) + } + } +} \ No newline at end of file diff --git a/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/UartViewModel.kt b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/UartViewModel.kt new file mode 100644 index 00000000..c24d1f27 --- /dev/null +++ b/profile/src/main/java/no/nordicsemi/android/toolbox/profile/viewmodel/UartViewModel.kt @@ -0,0 +1,276 @@ +package no.nordicsemi.android.toolbox.profile.viewmodel + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import jakarta.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import no.nordicsemi.android.analytics.AppAnalytics +import no.nordicsemi.android.analytics.UARTChangeConfiguration +import no.nordicsemi.android.analytics.UARTCreateConfiguration +import no.nordicsemi.android.analytics.UARTMode +import no.nordicsemi.android.analytics.UARTSendAnalyticsEvent +import no.nordicsemi.android.common.navigation.Navigator +import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.ProfileDestinationId +import no.nordicsemi.android.toolbox.profile.data.UARTServiceData +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro +import no.nordicsemi.android.toolbox.profile.manager.repository.UartRepository +import no.nordicsemi.android.toolbox.profile.repository.DeviceRepository +import no.nordicsemi.android.toolbox.profile.repository.uartXml.UartConfigurationRepository + +// UART Profile events. +internal sealed interface UARTEvent { + data class OnCreateMacro( + val macroName: UARTMacro, + ) : UARTEvent + + data class OnEditMacro( + val position: Int, + ) : UARTEvent + + data object OnEditFinished : UARTEvent + data object OnDeleteMacro : UARTEvent + + data class OnRunMacro( + val macro: UARTMacro, + ) : UARTEvent + + data class OnConfigurationSelected( + val configuration: UARTConfiguration, + ) : UARTEvent + + data class OnAddConfiguration( + val name: String, + ) : UARTEvent + + data object OnEditConfiguration : UARTEvent + data class OnDeleteConfiguration( + val configuration: UARTConfiguration, + ) : UARTEvent + + data class OnRunInput( + val text: String, + val newLineChar: MacroEol, + ) : UARTEvent + + data object ClearOutputItems : UARTEvent + + data class SetMaxValueLength( + val maxValueLength: Int, + ) : UARTEvent + +} + +@HiltViewModel +internal class UartViewModel @Inject constructor( + private val uartConfigurationRepository: UartConfigurationRepository, + private val analytics: AppAnalytics, + private val deviceRepository: DeviceRepository, + navigator: Navigator, + savedStateHandle: SavedStateHandle, +) : SimpleNavigationViewModel(navigator, savedStateHandle) { + private val _uartState = MutableStateFlow(UARTServiceData()) + val uartState = _uartState.asStateFlow() + + private val address = parameterOf(ProfileDestinationId) + + init { + observeUartProfile() + } + + /** + * Observes the UART profile from the device repository. + */ + private fun observeUartProfile() = viewModelScope.launch { + // Observe the profile handler flow from the device repository. + deviceRepository.profileHandlerFlow + .filter { it.isNotEmpty() } + .onEach { mapOfPeripheralProfiles -> + mapOfPeripheralProfiles.forEach { (peripheral, profiles) -> + if (peripheral.address == address) { + profiles.filter { it.profile == Profile.UART } + .forEach { _ -> + startUartService() + } + } + } + }.launchIn(this) + + } + + /** + * Starts the UART service and observes the data. + */ + private fun startUartService() { + // Initialize the UART repository with the address. + UartRepository.getData(address) + .onEach { data -> + _uartState.value = data + } + .launchIn(viewModelScope) + // Observe the UART configurations. + observeConfigurations() + } + + /** + * Observes the UART configurations from the repository. + * It updates the selected configuration name and loads previous configurations. + */ + private fun observeConfigurations() = with(uartConfigurationRepository) { + // Get the last configuration name from the data store. + getLastConfigurationName() + .filterNotNull() + .onEach { name -> + UartRepository.updateSelectedConfigurationName(address, name) + }.launchIn(viewModelScope) + + // Get all configurations for the device. + getAllConfigurations() + .onEach { uartConfigurations -> + UartRepository.loadPreviousConfigurations(address, uartConfigurations) + }.launchIn(viewModelScope) + } + + // UART events. + fun onEvent(event: UARTEvent) { + when (event) { + UARTEvent.ClearOutputItems -> UartRepository.clearOutputItems(address) // working. + is UARTEvent.OnAddConfiguration -> onAddConfiguration(event.name) // TODO: check if it is working. + + is UARTEvent.OnConfigurationSelected -> onConfigurationSelected(event.configuration) + is UARTEvent.OnCreateMacro -> addNewMacro(event.macroName) + is UARTEvent.OnDeleteConfiguration -> deleteConfiguration(event.configuration) + UARTEvent.OnDeleteMacro -> onDeleteMacro() + is UARTEvent.OnEditConfiguration -> onEditConfiguration() + UARTEvent.OnEditFinished -> onEditFinished() + is UARTEvent.OnEditMacro -> onEditMacro(event.position) + is UARTEvent.OnRunInput -> { + sendText(event.text, event.newLineChar) + } + + is UARTEvent.OnRunMacro -> runMacro(event.macro) + is UARTEvent.SetMaxValueLength -> + UartRepository.updateMaxWriteLength(address, event.maxValueLength) + } + } + + /** + * Deletes the macro from the repository. + */ + private fun onDeleteMacro() = viewModelScope.launch(Dispatchers.IO) { + UartRepository.onDeleteMacro(address) + } + + /** + * Called when the edit is finished. + * It notifies the repository that the edit is finished. + */ + private fun onEditFinished() { + viewModelScope.launch { + UartRepository.onEditFinished(address) + } + } + + /** + * Adds a new macro to the repository. + * It saves the new macro to the database. + */ + private fun addNewMacro(macroName: UARTMacro) = viewModelScope.launch(Dispatchers.IO) { + val newConfig = UartRepository.addOrEditMacro(address, macroName) + if (newConfig != null) { + // Save the new configuration to the database. + uartConfigurationRepository.insertConfiguration(newConfig) + } + } + + /** + * Called when a macro is edited. + * It notifies the repository that the macro is edited. + */ + private fun onEditMacro(position: Int) = viewModelScope.launch { + // Update the configuration in the UART repository. + UartRepository.onEditMacro(address, position) + } + + /** + * Edit uart configuration. + */ + private fun onEditConfiguration() = viewModelScope.launch { + // Update the configuration in the UART repository. + UartRepository.onEditConfiguration(address) + } + + /** + * Runs the macro. + */ + private fun runMacro(macro: UARTMacro) = viewModelScope.launch { + UartRepository.runMacro(address, macro) + // Log the event in the analytics. + analytics.logEvent(UARTSendAnalyticsEvent(UARTMode.PRESET)) + } + + /** + * Adds a new configuration to the repository and database. + */ + private fun onAddConfiguration(name: String) = viewModelScope.launch(Dispatchers.IO) { + // Update the configuration in the UART repository. + UartRepository.updateSelectedConfigurationName(address, name) + // Add configuration to the database. + val configurationId = + uartConfigurationRepository.insertConfiguration(UARTConfiguration(null, name)) + ?: return@launch + // Add configuration to the repository. + UartRepository.addConfiguration(address, UARTConfiguration(configurationId.toInt(), name)) + + // Save the configuration name in the data store. + uartConfigurationRepository.saveLastConfigurationNameToDataSource(name) + // Log the event in the analytics. + analytics.logEvent(UARTCreateConfiguration()) + } + + /** + * Called when a configuration is selected. + * It updates the selected configuration in the repository and saves it to the data store. + */ + private fun onConfigurationSelected(configuration: UARTConfiguration) = viewModelScope.launch { + UartRepository.updateSelectedConfigurationName(address, configuration.name) + // Update the selected configuration in the datastore. + uartConfigurationRepository.saveLastConfigurationNameToDataSource(configuration.name) + // Log the event in the analytics. + analytics.logEvent(UARTChangeConfiguration()) + } + + /** + * Deletes the configuration from the repository and database. + * It also removes the selected configuration if it is deleted. + */ + private fun deleteConfiguration(configuration: UARTConfiguration) = + viewModelScope.launch(Dispatchers.IO) { + // delete the configuration from the list. + UartRepository.deleteConfiguration(address, configuration) + // remove the selected configuration if it is deleted. + UartRepository.removeSelectedConfiguration(address) + // delete the configuration from the database. + uartConfigurationRepository.deleteConfiguration(configuration) + } + + /** + * Sends the text to the UART device. + */ + private fun sendText(text: String, newLineChar: MacroEol) = viewModelScope.launch { + UartRepository.sendText(address, text, newLineChar) + // Log the event in the analytics. + analytics.logEvent(UARTSendAnalyticsEvent(UARTMode.TEXT)) + } +} diff --git a/profile/src/main/res/drawable/ic_arrow.xml b/profile/src/main/res/drawable/ic_arrow.xml new file mode 100644 index 00000000..3e334ffe --- /dev/null +++ b/profile/src/main/res/drawable/ic_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile/src/main/res/drawable/ic_azimuth.xml b/profile/src/main/res/drawable/ic_azimuth.xml new file mode 100644 index 00000000..657a245e --- /dev/null +++ b/profile/src/main/res/drawable/ic_azimuth.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile/src/main/res/drawable/ic_control.xml b/profile/src/main/res/drawable/ic_control.xml new file mode 100644 index 00000000..4a295f53 --- /dev/null +++ b/profile/src/main/res/drawable/ic_control.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile/src/main/res/drawable/ic_distance.xml b/profile/src/main/res/drawable/ic_distance.xml new file mode 100644 index 00000000..38de3da1 --- /dev/null +++ b/profile/src/main/res/drawable/ic_distance.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile/src/main/res/drawable/ic_elevation.xml b/profile/src/main/res/drawable/ic_elevation.xml new file mode 100644 index 00000000..1fd6d90a --- /dev/null +++ b/profile/src/main/res/drawable/ic_elevation.xml @@ -0,0 +1,9 @@ + + + diff --git a/profile_uart/src/main/res/drawable/ic_input.xml b/profile/src/main/res/drawable/ic_input.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_input.xml rename to profile/src/main/res/drawable/ic_input.xml diff --git a/profile_uart/src/main/res/drawable/ic_macro.xml b/profile/src/main/res/drawable/ic_macro.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_macro.xml rename to profile/src/main/res/drawable/ic_macro.xml diff --git a/profile_uart/src/main/res/drawable/ic_output.xml b/profile/src/main/res/drawable/ic_output.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_output.xml rename to profile/src/main/res/drawable/ic_output.xml diff --git a/profile/src/main/res/drawable/ic_rscs.xml b/profile/src/main/res/drawable/ic_rscs.xml new file mode 100644 index 00000000..2ce61436 --- /dev/null +++ b/profile/src/main/res/drawable/ic_rscs.xml @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/profile_uart/src/main/res/drawable/ic_sync_down.xml b/profile/src/main/res/drawable/ic_sync_down.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_sync_down.xml rename to profile/src/main/res/drawable/ic_sync_down.xml diff --git a/profile_uart/src/main/res/drawable/ic_sync_down_off.xml b/profile/src/main/res/drawable/ic_sync_down_off.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_sync_down_off.xml rename to profile/src/main/res/drawable/ic_sync_down_off.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_1.xml b/profile/src/main/res/drawable/ic_uart_1.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_1.xml rename to profile/src/main/res/drawable/ic_uart_1.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_2.xml b/profile/src/main/res/drawable/ic_uart_2.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_2.xml rename to profile/src/main/res/drawable/ic_uart_2.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_3.xml b/profile/src/main/res/drawable/ic_uart_3.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_3.xml rename to profile/src/main/res/drawable/ic_uart_3.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_4.xml b/profile/src/main/res/drawable/ic_uart_4.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_4.xml rename to profile/src/main/res/drawable/ic_uart_4.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_5.xml b/profile/src/main/res/drawable/ic_uart_5.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_5.xml rename to profile/src/main/res/drawable/ic_uart_5.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_6.xml b/profile/src/main/res/drawable/ic_uart_6.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_6.xml rename to profile/src/main/res/drawable/ic_uart_6.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_7.xml b/profile/src/main/res/drawable/ic_uart_7.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_7.xml rename to profile/src/main/res/drawable/ic_uart_7.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_8.xml b/profile/src/main/res/drawable/ic_uart_8.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_8.xml rename to profile/src/main/res/drawable/ic_uart_8.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_9.xml b/profile/src/main/res/drawable/ic_uart_9.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_9.xml rename to profile/src/main/res/drawable/ic_uart_9.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_about.xml b/profile/src/main/res/drawable/ic_uart_about.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_about.xml rename to profile/src/main/res/drawable/ic_uart_about.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_down.xml b/profile/src/main/res/drawable/ic_uart_down.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_down.xml rename to profile/src/main/res/drawable/ic_uart_down.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_forward.xml b/profile/src/main/res/drawable/ic_uart_forward.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_forward.xml rename to profile/src/main/res/drawable/ic_uart_forward.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_left.xml b/profile/src/main/res/drawable/ic_uart_left.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_left.xml rename to profile/src/main/res/drawable/ic_uart_left.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_pause.xml b/profile/src/main/res/drawable/ic_uart_pause.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_pause.xml rename to profile/src/main/res/drawable/ic_uart_pause.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_play.xml b/profile/src/main/res/drawable/ic_uart_play.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_play.xml rename to profile/src/main/res/drawable/ic_uart_play.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_rewind.xml b/profile/src/main/res/drawable/ic_uart_rewind.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_rewind.xml rename to profile/src/main/res/drawable/ic_uart_rewind.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_right.xml b/profile/src/main/res/drawable/ic_uart_right.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_right.xml rename to profile/src/main/res/drawable/ic_uart_right.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_settings.xml b/profile/src/main/res/drawable/ic_uart_settings.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_settings.xml rename to profile/src/main/res/drawable/ic_uart_settings.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_stop.xml b/profile/src/main/res/drawable/ic_uart_stop.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_stop.xml rename to profile/src/main/res/drawable/ic_uart_stop.xml diff --git a/profile_uart/src/main/res/drawable/ic_uart_up.xml b/profile/src/main/res/drawable/ic_uart_up.xml similarity index 100% rename from profile_uart/src/main/res/drawable/ic_uart_up.xml rename to profile/src/main/res/drawable/ic_uart_up.xml diff --git a/profile_hrs/src/main/res/drawable/ic_zoom_in.xml b/profile/src/main/res/drawable/ic_zoom_in.xml similarity index 100% rename from profile_hrs/src/main/res/drawable/ic_zoom_in.xml rename to profile/src/main/res/drawable/ic_zoom_in.xml diff --git a/profile_hrs/src/main/res/drawable/ic_zoom_out.xml b/profile/src/main/res/drawable/ic_zoom_out.xml similarity index 100% rename from profile_hrs/src/main/res/drawable/ic_zoom_out.xml rename to profile/src/main/res/drawable/ic_zoom_out.xml diff --git a/profile_uart/src/main/res/drawable/uart_button.xml b/profile/src/main/res/drawable/uart_button.xml similarity index 100% rename from profile_uart/src/main/res/drawable/uart_button.xml rename to profile/src/main/res/drawable/uart_button.xml diff --git a/profile/src/main/res/values/blinkyStrings.xml b/profile/src/main/res/values/blinkyStrings.xml new file mode 100644 index 00000000..69e1b430 --- /dev/null +++ b/profile/src/main/res/values/blinkyStrings.xml @@ -0,0 +1,9 @@ + + + Light + Switch light ON/OFF + + Button + PRESSED + RELEASED + \ No newline at end of file diff --git a/profile/src/main/res/values/bpsStrings.xml b/profile/src/main/res/values/bpsStrings.xml new file mode 100644 index 00000000..ff613609 --- /dev/null +++ b/profile/src/main/res/values/bpsStrings.xml @@ -0,0 +1,24 @@ + + + Blood pressure + + Waiting for Blood Pressure sample… + Click Button 1 on a DK to trigger one. + + Systolic pressure + Diastolic pressure + Mean arterial pressure (AP) + Heart rate + + %.2f %s + %1$te %1$tb %1$tY at %1$tT + + Body movement detected + Cuff fit detected + Irregular heart rate detected + Pulse rate in range + Measurement position detected + Improper measurement position + Pulse rate less than lower limit + Pulse rate exceeds upper limit + \ No newline at end of file diff --git a/profile/src/main/res/values/cgmStrings.xml b/profile/src/main/res/values/cgmStrings.xml new file mode 100644 index 00000000..311b1797 --- /dev/null +++ b/profile/src/main/res/values/cgmStrings.xml @@ -0,0 +1,7 @@ + + + There is no data available. Every record is created once a minute or longer. Please wait. + + Sequence number + %.2f mg/dL + \ No newline at end of file diff --git a/profile/src/main/res/values/cscStrings.xml b/profile/src/main/res/values/cscStrings.xml new file mode 100644 index 00000000..e6909ab9 --- /dev/null +++ b/profile/src/main/res/values/cscStrings.xml @@ -0,0 +1,17 @@ + + + Cyclic and speed cadence + + Select wheel size + + Speed + Cadence + Distance + Total Distance + Gear Ratio + + Wheel size + + Select unit + + \ No newline at end of file diff --git a/profile/src/main/res/values/dfsStrings.xml b/profile/src/main/res/values/dfsStrings.xml new file mode 100644 index 00000000..ddb6b378 --- /dev/null +++ b/profile/src/main/res/values/dfsStrings.xml @@ -0,0 +1,53 @@ + + + App bar navigation back icon. + + Because of interferences the last read value can be wrong. To minimize this effect the current value is taken as middle value from cached values. The details are available on the chart below. + + Control device + Measurement details + Azimuth + Distance in dm (decimeter) + Elevation + Measurement details + + There is no device in range. + Make sure the device is in range. + Address + Selected device + List of devices in range + Cancel + + Click button to check available distance modes. + Check availability + Distance feature is not available on this device. + Multi-carrier phase difference (MCPD) + Round-Trip Time (RTT) + UNKNOWN + Current mode: + Enable RTT + Enable MCPD + Check mode + Only MCPD feature is available on this device. + Only RTT feature is available on this device. + + Select section for displaying details. + + -90° + 0° + 90° + + %d dm + + Round-Trip Time (RTT) + ifft* + phase + rssi + best + + From: + To: + + Distance range + Measurement details + \ No newline at end of file diff --git a/profile_gls/src/main/res/values/strings.xml b/profile/src/main/res/values/glsStrings.xml similarity index 75% rename from profile_gls/src/main/res/values/strings.xml rename to profile/src/main/res/values/glsStrings.xml index a057913f..53be3859 100644 --- a/profile_gls/src/main/res/values/strings.xml +++ b/profile/src/main/res/values/glsStrings.xml @@ -31,10 +31,9 @@ --> - GLS - There is no data available. This peripheral downloads data only when requested. Please select what kind of data you want to download by clicking one from the above buttons. + There is no data available. This peripheral downloads data only when requested. Please select what kind of data you want to download by clicking one from the working mode settings. - %1$te %1$tb %1$tY at %1$tT + %1$te %1$tb %1$tY,%1$tT Reserved for future use Capillary Whole blood @@ -48,15 +47,12 @@ Interstitial Fluid (ISF) Control Solution - mg/dl - mmol/l + kg/l + mol/l - All - Last - First - - YES - NO + Read all records + Last record + First record Finger Alternate Site Test (AST) @@ -64,9 +60,6 @@ Control solution Value not available - kg/l - mol/l - kg l @@ -100,41 +93,41 @@ Preprandial Postprandial Fasting - Casual + Casual (snacks, drinks, etc.) Bedtime Available Unavailable - Sequence number: - Date & Time: - Type: - Location: - Glucose condensation: + Sequence number + Date & Time + Type + Location + Glucose condensation %.2f %s - Battery low: - Sensor malfunction: - Insufficient sample: - Strip insertion error: - Strip type incorrect: - Sensor result too high: - Sensor result too low: - Temperature too high: - Temperature too low: - Strip pulled too soon: - General device fault: - Time fault: + Battery low + Sensor malfunction + Insufficient sample + Strip insertion error + Strip type incorrect + Sensor result too high + Sensor result too low + Temperature too high + Temperature too low + Strip pulled too soon + General device fault + Time fault - Measurement context: - Carbohydrate: - Meal: - Tester: - Health: - Exercise: - %d min %d%% - Medication: - %.2f%s\n%s - HbA1c: - %.2f%% - + Measurement context + Carbohydrate + Meal + Tester + Health + Exercise + %s (intensity %d %%) + Medication + %s (%.2f %s) + Blood glucose + %.2f %% + \ No newline at end of file diff --git a/profile/src/main/res/values/hrsStrings.xml b/profile/src/main/res/values/hrsStrings.xml new file mode 100644 index 00000000..0621151f --- /dev/null +++ b/profile/src/main/res/values/hrsStrings.xml @@ -0,0 +1,6 @@ + + + Heart rate + Icon to zoom chart in or out + Body sensor location + \ No newline at end of file diff --git a/profile/src/main/res/values/htsStrings.xml b/profile/src/main/res/values/htsStrings.xml new file mode 100644 index 00000000..b4263091 --- /dev/null +++ b/profile/src/main/res/values/htsStrings.xml @@ -0,0 +1,11 @@ + + + Health temperature + Temperature + Reading temperature + Measurement location + Measurement time + + Select temperature unit + Temperature unit settings + \ No newline at end of file diff --git a/profile/src/main/res/values/rscsStrings.xml b/profile/src/main/res/values/rscsStrings.xml new file mode 100644 index 00000000..f3ed7ba3 --- /dev/null +++ b/profile/src/main/res/values/rscsStrings.xml @@ -0,0 +1,25 @@ + + + Running speed & cadence + + Activity + Pace + Cadence + Number of steps + + Walking + Running + %.1f min/km + %d RPM + Number of Steps %d + Stride Length + + Select unit + + Supported features + Instantaneous stride length measurement + Total distance measurement + Walking or running status + Calibration + Multiple sensor locations + \ No newline at end of file diff --git a/profile/src/main/res/values/strings.xml b/profile/src/main/res/values/strings.xml new file mode 100644 index 00000000..9dcca4cf --- /dev/null +++ b/profile/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Reconnect + \ No newline at end of file diff --git a/profile/src/main/res/values/throughputStings.xml b/profile/src/main/res/values/throughputStings.xml new file mode 100644 index 00000000..da14d419 --- /dev/null +++ b/profile/src/main/res/values/throughputStings.xml @@ -0,0 +1,21 @@ + + + Throughput + + Start test + + Data + Write count + MTU (in bytes) + Speed + + Test size (in kB) + Enter the number of bytes to be sent in the throughput test + Test size (in kB) + + Test in time (seconds) + Enter the number of seconds to run the throughput test + Test in time (seconds) + Run + + \ No newline at end of file diff --git a/profile/src/main/res/values/uartStrings.xml b/profile/src/main/res/values/uartStrings.xml new file mode 100644 index 00000000..b151717b --- /dev/null +++ b/profile/src/main/res/values/uartStrings.xml @@ -0,0 +1,66 @@ + + + UART + + Please define a preset to send command to the device. + + New preset configuration + Delete selected configuration. + Edit selected configuration. + + Send + Input + Text to send + Preset + Output + Select preset configuration + Not selected. + Select configuration + + Command: %s + + Run preset + Edit preset + Delete preset + + Add preset + Here will be displayed read value from GATT characteristic. + + New preset name + Name + Add preset + Edit preset + Alias + Command + Confirm + Dismiss + Delete + + End of Line (EOL): + EOL: %s + LF + CR + LF + CR + + Messages sent and received will appear here. + Provided command cannot be empty. + + Name + Configuration name cannot be empty or duplicate. + + Icon representing defined command. + + Delete preset? + Are you sure you want to delete this preset? This action cannot be undone, and all associated data will be permanently lost. + Confirm + Cancel + + Click to switch between text input and preset input. + Clear items. + Click to constantly scroll view to the latest available log. + --> %s + <— %s + + Settings + Go to settings screen. + \ No newline at end of file diff --git a/profile_bps/build.gradle.kts b/profile_bps/build.gradle.kts deleted file mode 100644 index b9a9f663..00000000 --- a/profile_bps/build.gradle.kts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.bps" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.nordic.navigation) - implementation(libs.nordic.theme) - implementation(libs.nordic.logger) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_bps/src/main/AndroidManifest.xml b/profile_bps/src/main/AndroidManifest.xml deleted file mode 100644 index 07fd1f83..00000000 --- a/profile_bps/src/main/AndroidManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt deleted file mode 100644 index 4c254197..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/data/BPSServiceData.kt +++ /dev/null @@ -1,12 +0,0 @@ -package no.nordicsemi.android.bps.data - -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.bps.data.BloodPressureMeasurementData -import no.nordicsemi.android.kotlin.ble.profile.bps.data.IntermediateCuffPressureData - -data class BPSServiceData ( - val bloodPressureMeasurement: BloodPressureMeasurementData? = null, - val intermediateCuffPressure: IntermediateCuffPressureData? = null, - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null -) \ No newline at end of file diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt deleted file mode 100644 index 33bc2f87..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSContentView.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.bps.R -import no.nordicsemi.android.bps.data.BPSServiceData - -@Composable -internal fun BPSContentView(state: BPSServiceData, onEvent: (BPSViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - BPSSensorsReadingView(state = state) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt deleted file mode 100644 index 60a469f9..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.bps.R -import no.nordicsemi.android.bps.viewmodel.BPSViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun BPSScreen() { - val viewModel: BPSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(DisconnectEvent) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.result.connectionState, - title = R.string.bps_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.result.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> BPSContentView(state.result) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt deleted file mode 100644 index 1e649b72..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSSensorsReadingView.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.bps.R -import no.nordicsemi.android.bps.data.BPSServiceData -import no.nordicsemi.android.kotlin.ble.profile.bps.data.BloodPressureMeasurementData -import no.nordicsemi.android.kotlin.ble.profile.bps.data.IntermediateCuffPressureData -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun BPSSensorsReadingView(state: BPSServiceData) { - ScreenSection { - Column { - SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.bps_records)) - - state.bloodPressureMeasurement?.let { - Spacer(modifier = Modifier.height(16.dp)) - BloodPressureView(it) - } - - state.intermediateCuffPressure?.displayHeartRate()?.let { - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.bps_pulse), it) - } - - if (state.intermediateCuffPressure == null && state.bloodPressureMeasurement == null) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - stringResource(id = R.string.no_data_info), - style = MaterialTheme.typography.bodyMedium - ) - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - } -} - -@Composable -private fun BloodPressureView(state: BloodPressureMeasurementData) { - KeyValueField(stringResource(id = R.string.bps_systolic), state.displaySystolic()) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.bps_diastolic), state.displayDiastolic()) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.bps_mean), state.displayMeanArterialPressure()) -} - -@Composable -fun BloodPressureMeasurementData.displaySystolic(): String { - return stringResource(id = R.string.bps_blood_pressure, systolic) -} - -@Composable -fun BloodPressureMeasurementData.displayDiastolic(): String { - return stringResource(id = R.string.bps_blood_pressure, diastolic) -} - -@Composable -fun BloodPressureMeasurementData.displayMeanArterialPressure(): String { - return stringResource(id = R.string.bps_blood_pressure, meanArterialPressure) -} - -@Composable -fun IntermediateCuffPressureData.displayHeartRate(): String? { - return pulseRate?.toString() -} - -@Preview -@Composable -private fun Preview() { - BPSSensorsReadingView(BPSServiceData()) -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewEvent.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewEvent.kt deleted file mode 100644 index 0f3e9d25..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewEvent.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.view - -internal sealed class BPSViewEvent - -internal object DisconnectEvent : BPSViewEvent() - -internal object OpenLoggerEvent : BPSViewEvent() diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt deleted file mode 100644 index 4b9d617b..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/view/BPSViewState.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.view - -import no.nordicsemi.android.bps.data.BPSServiceData -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus - -internal data class BPSViewState( - val result: BPSServiceData = BPSServiceData(), - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - result.connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } -} diff --git a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt b/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt deleted file mode 100644 index 66095193..00000000 --- a/profile_bps/src/main/java/no/nordicsemi/android/bps/viewmodel/BPSViewModel.kt +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.bps.viewmodel - -import android.annotation.SuppressLint -import android.content.Context -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.bps.view.BPSViewEvent -import no.nordicsemi.android.bps.view.BPSViewState -import no.nordicsemi.android.bps.view.DisconnectEvent -import no.nordicsemi.android.bps.view.OpenLoggerEvent -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.bps.BloodPressureMeasurementParser -import no.nordicsemi.android.kotlin.ble.profile.bps.IntermediateCuffPressureParser -import no.nordicsemi.android.kotlin.ble.profile.bps.data.BloodPressureMeasurementData -import no.nordicsemi.android.kotlin.ble.profile.bps.data.IntermediateCuffPressureData -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import java.util.UUID -import javax.inject.Inject - -val BPS_SERVICE_UUID: UUID = UUID.fromString("00001810-0000-1000-8000-00805f9b34fb") -private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") -private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission", "StaticFieldLeak") -@HiltViewModel -internal class BPSViewModel @Inject constructor( - @ApplicationContext - private val context: Context, - private val navigationManager: Navigator, - private val analytics: AppAnalytics, - private val stringConst: StringConst -) : ViewModel() { - - private val _state = MutableStateFlow(BPSViewState()) - val state = _state.asStateFlow() - - private var client: ClientBleGatt? = null - private var logger: nRFLoggerTree? = null - - init { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(BPS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleArgs(it) } - .launchIn(viewModelScope) - } - - private fun handleArgs(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> startGattClient(result.value) - } - } - - fun onEvent(event: BPSViewEvent) { - when (event) { - DisconnectEvent -> onDisconnectEvent() - OpenLoggerEvent -> LoggerLauncher.launch(context, logger?.session as? LogSession) - } - } - - private fun onDisconnectEvent() { - client?.disconnect() - navigationManager.navigateUp() - } - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "BPS", device.address) - .also { Timber.plant(it) } - } - - private fun startGattClient(device: ServerDevice) = viewModelScope.launch { - _state.value = _state.value.copy(deviceName = device.name) - - initLogger(device) - - val client = ClientBleGatt.connect(context, device, viewModelScope) - this@BPSViewModel.client = client - - client.connectionStateWithStatus - .filterNotNull() - .onEach { onDataUpdate(it) } - .onEach { logAnalytics(it.state) } - .launchIn(viewModelScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val bpsService = services.findService(BPS_SERVICE_UUID)!! - val bpmCharacteristic = bpsService.findCharacteristic(BPM_CHARACTERISTIC_UUID)!! - val icpCharacteristic = bpsService.findCharacteristic(ICP_CHARACTERISTIC_UUID) - - bpmCharacteristic.getNotifications() - .mapNotNull { BloodPressureMeasurementParser.parse(it) } - .onEach { onDataUpdate(it) } - .catch { it.printStackTrace() } - .launchIn(viewModelScope) - - icpCharacteristic?.getNotifications() - ?.mapNotNull { IntermediateCuffPressureParser.parse(it) } - ?.onEach { onDataUpdate(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(viewModelScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { onDataUpdate(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(viewModelScope) - } - - private fun onMissingServices() { - _state.value = _state.value.copy(missingServices = true) - client?.disconnect() - } - - private fun onDataUpdate(connectionState: GattConnectionStateWithStatus) { - val newResult = _state.value.result.copy(connectionState = connectionState) - _state.value = _state.value.copy(result = newResult) - } - - private fun onDataUpdate(batteryLevel: Int) { - val newResult = _state.value.result.copy(batteryLevel = batteryLevel) - _state.value = _state.value.copy(result = newResult) - } - - private fun onDataUpdate(data: BloodPressureMeasurementData) { - val newResult = _state.value.result.copy(bloodPressureMeasurement = data) - _state.value = _state.value.copy(result = newResult) - } - - private fun onDataUpdate(data: IntermediateCuffPressureData) { - val newResult = _state.value.result.copy(intermediateCuffPressure = data) - _state.value = _state.value.copy(result = newResult) - } - - private fun logAnalytics(connectionState: GattConnectionState) { - if (connectionState == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.BPS)) - } - } -} diff --git a/profile_bps/src/main/res/values/strings.xml b/profile_bps/src/main/res/values/strings.xml deleted file mode 100644 index 524087c9..00000000 --- a/profile_bps/src/main/res/values/strings.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - Blood pressure - - Data - - No data available. If you are using nRF DK\'s press button 1 to see the result. - - Systolic - Diastolic - Mean AP - Heart rate - Time and Date - - %.0f kPa - diff --git a/profile_cgms/build.gradle.kts b/profile_cgms/build.gradle.kts deleted file mode 100644 index b03d1ff3..00000000 --- a/profile_cgms/build.gradle.kts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.parcelize) -} - -android { - namespace = "no.nordicsemi.android.cgms" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.logger) - implementation(libs.nordic.navigation) - - implementation(libs.nordic.blek.uiscanner) - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation(libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_cgms/module-rules.pro b/profile_cgms/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_cgms/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_cgms/src/main/AndroidManifest.xml b/profile_cgms/src/main/AndroidManifest.xml deleted file mode 100644 index 10b325bc..00000000 --- a/profile_cgms/src/main/AndroidManifest.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt deleted file mode 100644 index 313db73b..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceCommand.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.data - -internal enum class CGMServiceCommand { - REQUEST_ALL_RECORDS, - REQUEST_LAST_RECORD, - REQUEST_FIRST_RECORD, - DISCONNECT -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt deleted file mode 100644 index 7c7133f1..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/data/CGMServiceData.kt +++ /dev/null @@ -1,28 +0,0 @@ -package no.nordicsemi.android.cgms.data - -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMRecord -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus - -internal data class CGMServiceData( - val records: List = emptyList(), - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val requestStatus: RequestStatus = RequestStatus.IDLE, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } -} - -data class CGMRecordWithSequenceNumber( - val sequenceNumber: Int, - val record: CGMRecord, - val timestamp: Long -) diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt deleted file mode 100644 index 7bfdea6c..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMRepository.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber -import no.nordicsemi.android.cgms.data.CGMServiceCommand -import no.nordicsemi.android.cgms.data.CGMServiceData -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CGMRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(CGMServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - private val _command = simpleSharedFlow() - internal val command = _command.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - val hasRecords = data.value.records.isNotEmpty() - val highestSequenceNumber = data.value.records.maxOfOrNull { it.sequenceNumber } ?: -1 - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "CGM", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(CGMService::class.java, device) - } - - fun onDataReceived(data: List) { - _data.value = _data.value.copy(records = _data.value.records + data) - } - - internal fun onCommand(command: CGMServiceCommand) { - _command.tryEmit(command) - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun onNewRequestStatus(requestStatus: RequestStatus) { - _data.value = _data.value.copy(requestStatus = requestStatus) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun clear() { - _data.value = _data.value.copy(records = emptyList()) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = CGMServiceData() - } -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt deleted file mode 100644 index 6f30f868..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/repository/CGMService.kt +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber -import no.nordicsemi.android.cgms.data.CGMServiceCommand -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.core.errors.GattOperationException -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMFeatureParser -import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMMeasurementParser -import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMSpecificOpsControlPointParser -import no.nordicsemi.android.kotlin.ble.profile.cgm.CGMStatusParser -import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMErrorCode -import no.nordicsemi.android.kotlin.ble.profile.cgm.data.CGMOpCode -import no.nordicsemi.android.kotlin.ble.profile.gls.CGMSpecificOpsControlPointDataParser -import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser -import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser -import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData -import no.nordicsemi.android.kotlin.ble.profile.racp.RACPOpCode -import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import no.nordicsemi.android.utils.launchWithCatch -import no.nordicsemi.android.utils.tryOrLog -import java.util.* -import javax.inject.Inject - -val CGMS_SERVICE_UUID: UUID = UUID.fromString("0000181F-0000-1000-8000-00805f9b34fb") -private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb") -private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb") -private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb") -private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb") - -private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class CGMService : NotificationService() { - - @Inject - lateinit var repository: CGMRepository - - private var client: ClientBleGatt? = null - - private var secured = false - - private var recordAccessRequestInProgress = false - - private var sessionStartTime: Long = 0 - - private lateinit var recordAccessControlPointCharacteristic: ClientBleGattCharacteristic - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - repository.command - .onEach { onCommand(it) } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun onCommand(command: CGMServiceCommand) = lifecycleScope.launch{ - when (command) { - CGMServiceCommand.REQUEST_ALL_RECORDS -> requestAllRecords() - CGMServiceCommand.REQUEST_LAST_RECORD -> requestLastRecord() - CGMServiceCommand.REQUEST_FIRST_RECORD -> requestFirstRecord() - CGMServiceCommand.DISCONNECT -> disconnect() - } - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@CGMService, device, lifecycleScope) - this@CGMService.client = client - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val cgmService = services.findService(CGMS_SERVICE_UUID)!! - val statusCharacteristic = cgmService.findCharacteristic(CGM_STATUS_UUID)!! - val featureCharacteristic = cgmService.findCharacteristic(CGM_FEATURE_UUID)!! - val measurementCharacteristic = cgmService.findCharacteristic(CGM_MEASUREMENT_UUID)!! - val opsControlPointCharacteristic = cgmService.findCharacteristic(CGM_OPS_CONTROL_POINT_UUID)!! - recordAccessControlPointCharacteristic = cgmService.findCharacteristic(RACP_UUID)!! - - measurementCharacteristic.getNotifications() - .mapNotNull { CGMMeasurementParser.parse(it) } - .onEach { - if (sessionStartTime == 0L && !recordAccessRequestInProgress) { - val timeOffset = it.minOf { it.timeOffset } - sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L - } - - val result = it.map { - val timestamp = sessionStartTime + it.timeOffset * 60000L - CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp) - } - - repository.onDataReceived(result) - } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - opsControlPointCharacteristic.getNotifications() - .mapNotNull { CGMSpecificOpsControlPointParser.parse(it) } - .onEach { - if (it.isOperationCompleted) { - sessionStartTime = if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION) { - System.currentTimeMillis() - } else { - 0 - } - } else { - if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION && it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED) { - sessionStartTime = 0 - } else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) { - sessionStartTime = 0 - } - } - } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - recordAccessControlPointCharacteristic.getNotifications() - .mapNotNull { RecordAccessControlPointParser.parse(it) } - .onEach { onAccessControlPointDataReceived(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - - lifecycleScope.launchWithCatch { - val featuresEnvelope = featureCharacteristic.read().let { CGMFeatureParser.parse(it) }!! - secured = featuresEnvelope.features.e2eCrcSupported - } - - lifecycleScope.launchWithCatch { - val statusEnvelope = statusCharacteristic.read().let { CGMStatusParser.parse(it) }!! - if (!statusEnvelope.status.sessionStopped) { - sessionStartTime = System.currentTimeMillis() - statusEnvelope.timeOffset * 60000L - } - } - - if (sessionStartTime == 0L) { - tryOrLog { - opsControlPointCharacteristic.write(CGMSpecificOpsControlPointDataParser.startSession(secured)) - } - } - } - - private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = lifecycleScope.launch { - when (data) { - is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords) - is ResponseData -> when (data.responseCode) { - RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode) - RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound() - RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED, - RACPResponseCode.RACP_ERROR_INVALID_OPERATOR, - RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED, - RACPResponseCode.RACP_ERROR_INVALID_OPERAND, - RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL, - RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED, - RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode) - } - } - } - - private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) { - val status = when (requestCode) { - RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED - else -> RequestStatus.SUCCESS - } - repository.onNewRequestStatus(status) - } - - private fun onRecordAccessOperationCompletedWithNoRecordsFound() { - repository.onNewRequestStatus(RequestStatus.SUCCESS) - } - - private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) { - if (numberOfRecords > 0) { - if (repository.hasRecords) { - tryOrLog { - recordAccessControlPointCharacteristic.write( - RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(repository.highestSequenceNumber) - ) - } - } else { - tryOrLog { - recordAccessControlPointCharacteristic.write( - RecordAccessControlPointInputParser.reportAllStoredRecords() - ) - } - } - } - repository.onNewRequestStatus(RequestStatus.SUCCESS) - } - - private fun onRecordAccessOperationError(response: RACPResponseCode) { - if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - repository.onNewRequestStatus(RequestStatus.NOT_SUPPORTED) - } else { - repository.onNewRequestStatus(RequestStatus.FAILED) - } - } - - private fun clear() { - repository.clear() - } - - private suspend fun requestLastRecord() { - clear() - repository.onNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord()) - } catch (e: GattOperationException) { - e.printStackTrace() - repository.onNewRequestStatus(RequestStatus.FAILED) - } - } - - private suspend fun requestFirstRecord() { - clear() - repository.onNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord()) - } catch (e: GattOperationException) { - e.printStackTrace() - repository.onNewRequestStatus(RequestStatus.FAILED) - } - } - - private suspend fun requestAllRecords() { - clear() - repository.onNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords()) - } catch (e: GattOperationException) { - e.printStackTrace() - repository.onNewRequestStatus(RequestStatus.FAILED) - } - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt deleted file mode 100644 index b2449b7f..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMContentView.kt +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.view - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.data.CGMRecordWithSequenceNumber -import no.nordicsemi.android.cgms.data.CGMServiceCommand -import no.nordicsemi.android.cgms.data.CGMServiceData -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun CGMContentView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SettingsView(state, onEvent) - - Spacer(modifier = Modifier.height(16.dp)) - - RecordsView(state) - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - - Spacer(modifier = Modifier.height(16.dp)) - } -} - -@Composable -private fun SettingsView(state: CGMServiceData, onEvent: (CGMViewEvent) -> Unit) { - ScreenSection { - SectionTitle(icon = Icons.Default.Settings, title = "Request items") - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - if (state.requestStatus == RequestStatus.PENDING) { - CircularProgressIndicator() - } else { - Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_ALL_RECORDS)) }) { - Text(stringResource(id = R.string.cgms__working_mode__all)) - } - Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_LAST_RECORD)) }) { - Text(stringResource(id = R.string.cgms__working_mode__last)) - } - Button(onClick = { onEvent(OnWorkingModeSelected(CGMServiceCommand.REQUEST_FIRST_RECORD)) }) { - Text(stringResource(id = R.string.cgms__working_mode__first)) - } - } - } - } -} - -@Composable -private fun RecordsView(state: CGMServiceData) { - ScreenSection { - if (state.records.isEmpty()) { - RecordsViewWithoutData() - } else { - RecordsViewWithData(state) - } - - } -} - -@Composable -private fun RecordsViewWithData(state: CGMServiceData) { - Column(modifier = Modifier.fillMaxWidth()) { - SectionTitle(resId = R.drawable.ic_records, title = "Records") - - Spacer(modifier = Modifier.height(16.dp)) - - state.records.forEachIndexed { i, it -> - RecordItem(it) - - if (i < state.records.size - 1) { - Spacer(modifier = Modifier.size(8.dp)) - } - } - } -} - -@Composable -private fun RecordItem(record: CGMRecordWithSequenceNumber) { - Row(verticalAlignment = Alignment.CenterVertically) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - Text( - text = record.formattedTime(), - style = MaterialTheme.typography.labelLarge - ) - - Text( - text = stringResource(id = R.string.cgms_sequence_number, record.sequenceNumber), - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer(modifier = Modifier.size(16.dp)) - - Text( - text = record.glucoseConcentration(), - style = MaterialTheme.typography.titleMedium, - ) - } -} - -@Composable -private fun RecordsViewWithoutData() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SectionTitle(icon = Icons.Default.Search, title = "No items") - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.cgms_no_records_info), - style = MaterialTheme.typography.bodyMedium - ) - } -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt deleted file mode 100644 index 23d2dd45..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMScreen.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.cgms.R -import no.nordicsemi.android.cgms.viewmodel.CGMViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun CGMScreen() { - val viewModel: CGMViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUp) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.cgms_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - - GattConnectionState.STATE_CONNECTED -> CGMContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt deleted file mode 100644 index fb9c0412..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/view/CGMViewEvent.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.view - -import no.nordicsemi.android.cgms.data.CGMServiceCommand - -internal sealed class CGMViewEvent - -internal data class OnWorkingModeSelected(val workingMode: CGMServiceCommand) : CGMViewEvent() - -internal object NavigateUp : CGMViewEvent() - -internal object DisconnectEvent : CGMViewEvent() - -internal object OpenLoggerEvent : CGMViewEvent() diff --git a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt b/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt deleted file mode 100644 index 67c09256..00000000 --- a/profile_cgms/src/main/java/no/nordicsemi/android/cgms/viewmodel/CGMViewModel.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.cgms.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.cgms.data.CGMServiceCommand -import no.nordicsemi.android.cgms.repository.CGMRepository -import no.nordicsemi.android.cgms.repository.CGMS_SERVICE_UUID -import no.nordicsemi.android.cgms.view.CGMViewEvent -import no.nordicsemi.android.cgms.view.DisconnectEvent -import no.nordicsemi.android.cgms.view.NavigateUp -import no.nordicsemi.android.cgms.view.OnWorkingModeSelected -import no.nordicsemi.android.cgms.view.OpenLoggerEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class CGMViewModel @Inject constructor( - private val repository: CGMRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.CGMS)) - } - }.launchIn(viewModelScope) - } - - fun onEvent(event: CGMViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - is OnWorkingModeSelected -> onCommandReceived(event.workingMode) - NavigateUp -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() - } - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(CGMS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - private fun onCommandReceived(workingMode: CGMServiceCommand) { - repository.onCommand(workingMode) - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_cgms/src/main/res/values/strings.xml b/profile_cgms/src/main/res/values/strings.xml deleted file mode 100644 index ef615d8a..00000000 --- a/profile_cgms/src/main/res/values/strings.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - CGMS - There is no data available. Every record is created once a minute or longer. Please wait. - - All - Last - First - - Sequence number: %d - %.2f mg/dL - diff --git a/profile_csc/build.gradle.kts b/profile_csc/build.gradle.kts deleted file mode 100644 index e6f35db2..00000000 --- a/profile_csc/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.csc" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.ui) - implementation(libs.nordic.theme) - implementation(libs.nordic.logger) - implementation(libs.nordic.navigation) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_csc/module-rules.pro b/profile_csc/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_csc/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_csc/src/main/AndroidManifest.xml b/profile_csc/src/main/AndroidManifest.xml deleted file mode 100644 index dfa65a53..00000000 --- a/profile_csc/src/main/AndroidManifest.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt deleted file mode 100644 index 1bb81d26..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/CSCServiceData.kt +++ /dev/null @@ -1,21 +0,0 @@ -package no.nordicsemi.android.csc.data - -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.csc.data.CSCData - -internal data class CSCServiceData( - val data: CSCData = CSCData(), - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val speedUnit: SpeedUnit = SpeedUnit.M_S, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/SpeedUnit.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/data/SpeedUnit.kt deleted file mode 100644 index 0ebea419..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/data/SpeedUnit.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.data - -internal enum class SpeedUnit(val displayName: String) { - M_S("m/s"), - KM_H("km/h"), - MPH("mph") -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt deleted file mode 100644 index 81cd6b42..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCRepository.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.csc.data.CSCServiceData -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.csc.data.CSCData -import no.nordicsemi.android.kotlin.ble.profile.csc.data.WheelSize -import no.nordicsemi.android.kotlin.ble.profile.csc.data.WheelSizes -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CSCRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _wheelSize = MutableStateFlow(WheelSizes.default) - internal val wheelSize = _wheelSize.asStateFlow() - - private val _data = MutableStateFlow(CSCServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "CSC", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(CSCService::class.java, device) - } - - internal fun setSpeedUnit(speedUnit: SpeedUnit) { - _data.value = _data.value.copy(speedUnit = speedUnit) - } - - fun setWheelSize(wheelSize: WheelSize) { - _wheelSize.value = wheelSize - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun onCSCDataChanged(cscData: CSCData) { - _data.value = _data.value.copy(data = cscData) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = CSCServiceData() - } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt deleted file mode 100644 index b07bedf5..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/repository/CSCService.kt +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.csc.CSCDataParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.* -import javax.inject.Inject - -val CSC_SERVICE_UUID: UUID = UUID.fromString("00001816-0000-1000-8000-00805f9b34fb") -private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class CSCService : NotificationService() { - - @Inject - lateinit var repository: CSCRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@CSCService, device, lifecycleScope) - this@CSCService.client = client - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val cscService = services.findService(CSC_SERVICE_UUID)!! - val cscMeasurementCharacteristic = cscService.findCharacteristic(CSC_MEASUREMENT_CHARACTERISTIC_UUID)!! - - val cscDataParser = CSCDataParser() - cscMeasurementCharacteristic.getNotifications() - .mapNotNull { cscDataParser.parse(it, repository.wheelSize.value) } - .onEach { repository.onCSCDataChanged(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - - repository.setServiceRunning(false) - } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt deleted file mode 100644 index 74c31f56..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCContentView.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.common.ui.view.RadioButtonGroup -import no.nordicsemi.android.csc.R -import no.nordicsemi.android.csc.data.CSCServiceData -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.kotlin.ble.profile.csc.data.CSCData -import no.nordicsemi.android.kotlin.ble.profile.csc.data.WheelSize -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle -import no.nordicsemi.android.ui.view.dialog.FlowCanceled -import no.nordicsemi.android.ui.view.dialog.ItemSelectedResult - -@Composable -internal fun CSCContentView(state: CSCServiceData, onEvent: (CSCViewEvent) -> Unit) { - val showDialog = rememberSaveable { mutableStateOf(false) } - - if (showDialog.value) { - val wheelEntries = stringArrayResource(R.array.wheel_entries) - val wheelValues = stringArrayResource(R.array.wheel_values) - - SelectWheelSizeDialog { - when (it) { - FlowCanceled -> showDialog.value = false - is ItemSelectedResult -> { - onEvent(OnWheelSizeSelected(WheelSize(wheelValues[it.index].toInt(), wheelEntries[it.index]))) - showDialog.value = false - } - } - } - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SettingsSection(state.data, state.speedUnit, onEvent) { showDialog.value = true } - - Spacer(modifier = Modifier.height(16.dp)) - - SensorsReadingView(state = state, state.speedUnit) - - Spacer(modifier = Modifier.height(16.dp)) - - Button( - onClick = { onEvent(OnDisconnectButtonClick) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} - -@Composable -private fun SettingsSection( - state: CSCData, - speedUnit: SpeedUnit, - onEvent: (CSCViewEvent) -> Unit, - onWheelButtonClick: () -> Unit, -) { - ScreenSection { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - SectionTitle( - icon = Icons.Default.Settings, - title = stringResource(R.string.csc_settings) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - WheelSizeView(state, onWheelButtonClick) - - Spacer(modifier = Modifier.height(16.dp)) - - RadioButtonGroup(viewEntity = speedUnit.temperatureSettingsItems()) { - onEvent(OnSelectedSpeedUnitSelected(it.label.toSpeedUnit())) - } - } - } -} - -@Preview -@Composable -private fun ConnectedPreview() { - CSCContentView(CSCServiceData()) { } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCMappers.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCMappers.kt deleted file mode 100644 index a70b6d35..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCMappers.kt +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import no.nordicsemi.android.common.ui.view.RadioButtonItem -import no.nordicsemi.android.common.ui.view.RadioGroupViewEntity -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.kotlin.ble.profile.csc.data.CSCData -import java.util.Locale - -private const val DISPLAY_M_S = "m/s" -private const val DISPLAY_KM_H = "km/h" -private const val DISPLAY_MPH = "mph" - -internal fun CSCData.speedWithSpeedUnit(speedUnit: SpeedUnit): Float { - return when (speedUnit) { - SpeedUnit.M_S -> speed - SpeedUnit.KM_H -> speed * 3.6f - SpeedUnit.MPH -> speed * 2.2369f - } -} - -internal fun CSCData.displaySpeed(speedUnit: SpeedUnit): String { - val speedWithUnit = speedWithSpeedUnit(speedUnit) - return when (speedUnit) { - SpeedUnit.M_S -> String.format(Locale.US, "%.1f m/s", speedWithUnit) - SpeedUnit.KM_H -> String.format(Locale.US, "%.1f km/h", speedWithUnit) - SpeedUnit.MPH -> String.format(Locale.US, "%.1f mph", speedWithUnit) - } -} - -internal fun CSCData.displayCadence(): String { - return String.format(Locale.US, "%.0f RPM", cadence) -} - -internal fun CSCData.displayDistance(speedUnit: SpeedUnit): String { - return when (speedUnit) { - SpeedUnit.M_S -> String.format(Locale.US, "%.0f m", distance) - SpeedUnit.KM_H -> String.format(Locale.US, "%.0f m", distance) - SpeedUnit.MPH -> String.format(Locale.US, "%.0f yd", distance.toYards()) - } -} - -internal fun CSCData.displayTotalDistance(speedUnit: SpeedUnit): String { - return when (speedUnit) { - SpeedUnit.M_S -> String.format(Locale.US, "%.2f m", totalDistance) - SpeedUnit.KM_H -> String.format(Locale.US, "%.2f km", totalDistance.toKilometers()) - SpeedUnit.MPH -> String.format(Locale.US, "%.2f mile", totalDistance.toMiles()) - } -} - -internal fun CSCData.displayGearRatio(): String { - return String.format(Locale.US, "%.1f", gearRatio) -} - -internal fun String.toSpeedUnit(): SpeedUnit { - return when (this) { - DISPLAY_KM_H -> SpeedUnit.KM_H - DISPLAY_M_S -> SpeedUnit.M_S - DISPLAY_MPH -> SpeedUnit.MPH - else -> throw IllegalArgumentException("Can't create SpeedUnit from this label: $this") - } -} - -internal fun SpeedUnit.temperatureSettingsItems(): RadioGroupViewEntity { - return RadioGroupViewEntity( - SpeedUnit.entries.toTypedArray().map { createRadioButtonItem(it, this) } - ) -} - -private fun createRadioButtonItem(unit: SpeedUnit, selectedSpeedUnit: SpeedUnit): RadioButtonItem { - return RadioButtonItem(displayTemperature(unit), unit == selectedSpeedUnit) -} - -private fun displayTemperature(unit: SpeedUnit): String { - return when (unit) { - SpeedUnit.KM_H -> DISPLAY_KM_H - SpeedUnit.M_S -> DISPLAY_M_S - SpeedUnit.MPH -> DISPLAY_MPH - } -} - -private fun Float.toYards(): Float { - return this*1.0936f -} - -private fun Float.toKilometers(): Float { - return this/1000f -} - -private fun Float.toMiles(): Float { - return this*0.0006f -} - diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt deleted file mode 100644 index 6accb0f6..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.csc.R -import no.nordicsemi.android.csc.viewmodel.CSCViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun CSCScreen() { - val viewModel: CSCViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUp) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.csc_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(OnDisconnectButtonClick) }, - openLogger = { viewModel.onEvent(OpenLogger) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> CSCContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt deleted file mode 100644 index d48b4e7e..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/CSCViewEvent.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.kotlin.ble.profile.csc.data.WheelSize - -internal sealed class CSCViewEvent - -internal data class OnWheelSizeSelected(val wheelSize: WheelSize) : CSCViewEvent() - -internal data class OnSelectedSpeedUnitSelected(val selectedSpeedUnit: SpeedUnit) : CSCViewEvent() - -internal object OnDisconnectButtonClick : CSCViewEvent() - -internal object NavigateUp : CSCViewEvent() - -internal object OpenLogger : CSCViewEvent() diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt deleted file mode 100644 index 1cb03414..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SelectWheelSizeDialog.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringArrayResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import no.nordicsemi.android.common.theme.NordicTheme -import no.nordicsemi.android.csc.R -import no.nordicsemi.android.ui.view.dialog.StringListDialog -import no.nordicsemi.android.ui.view.dialog.StringListDialogConfig -import no.nordicsemi.android.ui.view.dialog.StringListDialogResult -import no.nordicsemi.android.ui.view.dialog.toAnnotatedString - -@Composable -internal fun SelectWheelSizeDialog(onEvent: (StringListDialogResult) -> Unit) { - val wheelEntries = stringArrayResource(R.array.wheel_entries) - val wheelValues = stringArrayResource(R.array.wheel_values) - - StringListDialog(createConfig(wheelEntries) { - onEvent(it) - }) -} - -@Composable -private fun createConfig(entries: Array, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig { - return StringListDialogConfig( - title = stringResource(id = R.string.csc_dialog_title).toAnnotatedString(), - items = entries.toList(), - onResult = onResult - ) -} - -@Preview -@Composable -internal fun DefaultPreview() { - NordicTheme { - val wheelEntries = stringArrayResource(R.array.wheel_entries) - StringListDialog(createConfig(wheelEntries) {}) - } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt deleted file mode 100644 index 3bbba565..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/SensorsReadingView.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.csc.R -import no.nordicsemi.android.csc.data.CSCServiceData -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun SensorsReadingView(state: CSCServiceData, speedUnit: SpeedUnit) { - val csc = state.data - ScreenSection { - SectionTitle(resId = R.drawable.ic_records, title = "Records") - - Spacer(modifier = Modifier.height(16.dp)) - - Column { - KeyValueField(stringResource(id = R.string.csc_field_speed), csc.displaySpeed(speedUnit)) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.csc_field_cadence), csc.displayCadence()) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.csc_field_distance), csc.displayDistance(speedUnit)) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField( - stringResource(id = R.string.csc_field_total_distance), - csc.displayTotalDistance(speedUnit) - ) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.csc_field_gear_ratio), csc.displayGearRatio()) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - } -} - -@Preview -@Composable -private fun Preview() { - SensorsReadingView(CSCServiceData(), SpeedUnit.KM_H) -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt deleted file mode 100644 index a6b1d1ec..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/view/WheelSizeView.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.view - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import no.nordicsemi.android.csc.R -import no.nordicsemi.android.kotlin.ble.profile.csc.data.CSCData - -@Composable -internal fun WheelSizeView(state: CSCData, onClick: () -> Unit) { - OutlinedButton(onClick = { onClick() }) { - Row( - modifier = Modifier.fillMaxWidth(0.5f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ){ - Column { - Text( - text = stringResource(id = R.string.csc_field_wheel_size), - style = MaterialTheme.typography.labelSmall - ) - Text(text = state.wheelSize.name, style = MaterialTheme.typography.bodyMedium) - } - - Icon(Icons.Default.ArrowDropDown, contentDescription = "") - } - } -} - -@Preview -@Composable -private fun WheelSizeViewPreview() { - WheelSizeView(CSCData()) { } -} diff --git a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt b/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt deleted file mode 100644 index dc447ac7..00000000 --- a/profile_csc/src/main/java/no/nordicsemi/android/csc/viewmodel/CSCViewModel.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.csc.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.csc.data.SpeedUnit -import no.nordicsemi.android.csc.repository.CSCRepository -import no.nordicsemi.android.csc.repository.CSC_SERVICE_UUID -import no.nordicsemi.android.csc.view.CSCViewEvent -import no.nordicsemi.android.csc.view.NavigateUp -import no.nordicsemi.android.csc.view.OnDisconnectButtonClick -import no.nordicsemi.android.csc.view.OnSelectedSpeedUnitSelected -import no.nordicsemi.android.csc.view.OnWheelSizeSelected -import no.nordicsemi.android.csc.view.OpenLogger -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class CSCViewModel @Inject constructor( - private val repository: CSCRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.CSC)) - } - }.launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(CSC_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - fun onEvent(event: CSCViewEvent) { - when (event) { - is OnSelectedSpeedUnitSelected -> setSpeedUnit(event.selectedSpeedUnit) - is OnWheelSizeSelected -> repository.setWheelSize(event.wheelSize) - OnDisconnectButtonClick -> disconnect() - NavigateUp -> navigationManager.navigateUp() - OpenLogger -> repository.openLogger() - } - } - - private fun setSpeedUnit(speedUnit: SpeedUnit) { - repository.setSpeedUnit(speedUnit) - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_csc/src/main/res/values/strings.xml b/profile_csc/src/main/res/values/strings.xml deleted file mode 100644 index 3596909a..00000000 --- a/profile_csc/src/main/res/values/strings.xml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - Cyclic and speed cadence - - Select wheel size - - Speed - Cadence - Distance - Total Distance - Gear Ratio - - Wheel size - - Settings - - - 60–622 - 50–622 - 47–622 - 44–622 - 40–635 - 40–622 - 38–622 - 37–622 - 35–622 - 32–630 - 32–622 - 32–622 - 28–622 - 60–559 - 28–622 - 25–622 - 25–622 - 23–622 - 20–622 - 18–622 - 35–630 - 32–630 - 28–630 - 57–559 - 54–559 - 37–590 - 23–622 - 50–559 - 20–622 - 54–559 - 47–559 - 35–590 - 37–590 - 47–559 - 50–559 - 44–559 - 40–559 - 23–571 - 20–571 - 32–559 - 25–571 - 34–540 - 50–507 - 47–507 - 28–451 - 50–406 - 47–406 - 28–369 - 35–349 - 47–305 - - - - 2340 - 2284 - 2268 - 2224 - 2265 - 2224 - 2180 - 2205 - 2168 - 2199 - 2174 - 2155 - 2149 - 2146 - 2136 - 2146 - 2105 - 2133 - 2114 - 2102 - 2169 - 2161 - 2155 - 2133 - 2114 - 2105 - 2097 - 2089 - 2086 - 2114 - 2070 - 2068 - 2105 - 2055 - 2089 - 2051 - 2026 - 1973 - 1954 - 1953 - 1952 - 1948 - 1910 - 1907 - 1618 - 1593 - 1590 - 1325 - 1282 - 1272 - - diff --git a/profile_data/build.gradle.kts b/profile_data/build.gradle.kts new file mode 100644 index 00000000..bde63559 --- /dev/null +++ b/profile_data/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(libs.plugins.nordic.feature) +} + +android { + namespace = "no.nordicsemi.android.toolbox.profile.data" +} + +dependencies { + implementation(project(":profile-parsers")) + implementation(project(":lib_utils")) +} \ No newline at end of file diff --git a/profile_bps/module-rules.pro b/profile_data/module-rules.pro similarity index 99% rename from profile_bps/module-rules.pro rename to profile_data/module-rules.pro index 83f673f9..7e9d74c9 100644 --- a/profile_bps/module-rules.pro +++ b/profile_data/module-rules.pro @@ -14,4 +14,4 @@ # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; -#} +#} \ No newline at end of file diff --git a/profile_data/src/main/AndroidManifest.xml b/profile_data/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1d26c87a --- /dev/null +++ b/profile_data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BPSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BPSServiceData.kt new file mode 100644 index 00000000..f0423a63 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BPSServiceData.kt @@ -0,0 +1,20 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureData +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureData +import no.nordicsemi.android.toolbox.lib.utils.Profile + +/** + * BPS service data class that holds the blood pressure measurement and intermediate cuff pressure data. + * + * @param profile The profile. + * @param bloodPressureMeasurement The blood pressure measurement data. + * @param intermediateCuffPressure The intermediate cuff pressure data. + */ +data class BPSServiceData( + override val profile: Profile = Profile.BPS, + val bloodPressureMeasurement: BloodPressureMeasurementData? = null, + val intermediateCuffPressure: IntermediateCuffPressureData? = null, + val bloodPressureFeature: BloodPressureFeatureData? = null +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BatteryServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BatteryServiceData.kt new file mode 100644 index 00000000..0d06f224 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/BatteryServiceData.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.lib.utils.Profile + +/** + * Battery Service data class that holds the battery level. + * + * @param profile The profile. + * @param batteryLevel The battery level. + */ +data class BatteryServiceData( + override val profile: Profile = Profile.BATTERY, + val batteryLevel: Int? = null, +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CGMServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CGMServiceData.kt new file mode 100644 index 00000000..4d581b28 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CGMServiceData.kt @@ -0,0 +1,19 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMRecord +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class CGMServiceData( + override val profile: Profile = Profile.CGM, + val records: List = emptyList(), + val requestStatus: RequestStatus = RequestStatus.IDLE, + val workingMode: WorkingMode? = null, +) : ProfileServiceData() + +data class CGMRecordWithSequenceNumber( + val sequenceNumber: Int, + val record: CGMRecord, + val timestamp: Long +) diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CSCServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CSCServiceData.kt new file mode 100644 index 00000000..ef335eb8 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/CSCServiceData.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.csc.CSCData +import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class CSCServiceData( + override val profile: Profile = Profile.CSC, + val data: CSCData = CSCData(), + val speedUnit: SpeedUnit = SpeedUnit.M_S, +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ChannelSoundingServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ChannelSoundingServiceData.kt new file mode 100644 index 00000000..103cb51d --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ChannelSoundingServiceData.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class ChannelSoundingServiceData( + override val profile: Profile = Profile.CHANNEL_SOUNDING +) : ProfileServiceData() \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFSServiceData.kt new file mode 100644 index 00000000..3d96de48 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/DFSServiceData.kt @@ -0,0 +1,58 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf.DDFData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.McpdMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.RttMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.data.directionFinder.MeasurementSection +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range + +private const val MAX_STORED_ITEMS = 5 + +data class DFSServiceData( + override val profile: Profile = Profile.DFS, + val requestStatus: RequestStatus = RequestStatus.IDLE, + val data: Map = emptyMap(), + val ddfFeature: DDFData? = null, + val selectedDevice: PeripheralBluetoothAddress? = null, + val distanceRange: Range = Range(0, 50), +) : ProfileServiceData() { + + private val isMcpdAvailable = ddfFeature?.isMcpdAvailable + private val isRttAvailable = ddfFeature?.isRttAvailable + + fun isDistanceAvailable(): Boolean { + return isMcpdAvailable == true || isRttAvailable == true + } + + fun isDistanceAvailabilityChecked(): Boolean { + return isMcpdAvailable != null || isRttAvailable != null + } + + fun isDoubleModeAvailable(): Boolean { + return isMcpdAvailable == true && isRttAvailable == true + } +} + +data class SensorData( + val azimuth: SensorValue? = null, + val elevation: SensorValue? = null, + val mcpdDistance: SensorValue? = null, + val rttDistance: SensorValue? = null, + val distanceMode: DistanceMode? = null, + val selectedMeasurementSection: MeasurementSection? = null +) + +data class SensorValue( + val values: List = emptyList(), + val maxItems: Int = MAX_STORED_ITEMS +) { + fun copyWithNewValue(value: T): SensorValue { + return SensorValue(values.takeLast(maxItems - 1) + value) + } +} diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/GLSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/GLSServiceData.kt new file mode 100644 index 00000000..b6b04cb4 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/GLSServiceData.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class GLSServiceData( + override val profile: Profile = Profile.GLS, + val records: Map = mapOf(), + val requestStatus: RequestStatus = RequestStatus.IDLE, + val workingMode: WorkingMode? = null, +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HRSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HRSServiceData.kt new file mode 100644 index 00000000..fd8e055b --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HRSServiceData.kt @@ -0,0 +1,22 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.hrs.HRSData +import no.nordicsemi.android.toolbox.lib.utils.Profile + +/** + * Heart Rate Service data. + * + * @param profile the profile. + * @param data the list of heart rate data. + * @param bodySensorLocation the body sensor location. + * @param zoomIn true if the chart is zoomed in. + */ +data class HRSServiceData( + override val profile: Profile = Profile.HRS, + val heartRate: Int? = null, + val data: List = emptyList(), + val bodySensorLocation: Int? = null, + val zoomIn: Boolean = false, +) : ProfileServiceData() { + val heartRates = data.map { it.heartRate } +} diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HTSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HTSServiceData.kt new file mode 100644 index 00000000..590d6f6b --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/HTSServiceData.kt @@ -0,0 +1,18 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.hts.HTSData +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit + +/** + * HTS service data class that holds the HTS data. + * + * @param profile The profile. + * @param data The HTS data. + * @param temperatureUnit The temperature unit. + */ +data class HTSServiceData( + override val profile: Profile = Profile.HTS, + val data: HTSData? = null, + val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/LBSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/LBSServiceData.kt new file mode 100644 index 00000000..1f5afaa5 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/LBSServiceData.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class LBSServiceData( + override val profile: Profile = Profile.LBS, + val data: LBSData = LBSData( + ledState = false, + buttonState = false, + ), +) : ProfileServiceData() + +data class LBSData( + val ledState: Boolean, + val buttonState: Boolean, +) \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ProfileServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ProfileServiceData.kt new file mode 100644 index 00000000..0f7c8af5 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ProfileServiceData.kt @@ -0,0 +1,10 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.lib.utils.Profile + +/** + * Profile service data class that holds the profile and the service data. + */ +sealed class ProfileServiceData { + abstract val profile: Profile +} diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/RSCSServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/RSCSServiceData.kt new file mode 100644 index 00000000..9c90b325 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/RSCSServiceData.kt @@ -0,0 +1,13 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCFeatureData +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSData +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class RSCSServiceData( + override val profile: Profile = Profile.RSCS, + val data: RSCSData = RSCSData(), + val unit: RSCSSettingsUnit? = RSCSSettingsUnit.UNIT_M, + val feature: RSCFeatureData? = null, +) : ProfileServiceData() diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ThroughputServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ThroughputServiceData.kt new file mode 100644 index 00000000..01832679 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/ThroughputServiceData.kt @@ -0,0 +1,37 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputMetrics +import no.nordicsemi.android.toolbox.lib.utils.Profile + +data class ThroughputServiceData( + override val profile: Profile = Profile.THROUGHPUT, + val throughputData: ThroughputMetrics = ThroughputMetrics(), + val writingStatus: WritingStatus = WritingStatus.IDEAL, + val maxWriteValueLength: Int? = null +) : ProfileServiceData() + +sealed interface ThroughputInputType + +data class NumberOfBytes( + val numberOfBytes: Int +) : ThroughputInputType { + + companion object { + private const val DISPLAY_NAME = "Test in size (in kB) " + fun getString(): String = DISPLAY_NAME + } +} + +data class NumberOfSeconds( + val numberOfSeconds: Int +) : ThroughputInputType { + + companion object { + private const val DISPLAY_NAME = "Test in time (seconds)" + fun getString(): String = DISPLAY_NAME + } +} + +enum class WritingStatus { + IDEAL, IN_PROGRESS, COMPLETED +} \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/UARTServiceData.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/UARTServiceData.kt new file mode 100644 index 00000000..0285cc55 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/UARTServiceData.kt @@ -0,0 +1,42 @@ +package no.nordicsemi.android.toolbox.profile.data + +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro + +data class UARTServiceData( + override val profile: Profile = Profile.UART, + val messages: List = emptyList(), + val command: String? = null, + val maxWriteLength: Int = 20, + val uartViewState: UARTViewState = UARTViewState() +) : ProfileServiceData() + +data class UARTRecord( + val text: String, + val type: UARTRecordType, + val timestamp: Long = System.currentTimeMillis() +) + +enum class UARTRecordType { + INPUT, OUTPUT +} + +data class UARTViewState( + val editedPosition: Int? = null, + val selectedConfigurationName: String? = null, + val isConfigurationEdited: Boolean = false, + val configurations: List = emptyList(), + val isInputVisible: Boolean = true +) { + val showEditDialog: Boolean = editedPosition != null + + val selectedConfiguration: UARTConfiguration? = + configurations.find { selectedConfigurationName == it.name } + + val selectedMacro: UARTMacro? = selectedConfiguration?.let { configuration -> + editedPosition?.let { + configuration.macros.getOrNull(it) + } + } +} \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/directionFinder/SensorDataExt.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/directionFinder/SensorDataExt.kt new file mode 100644 index 00000000..feeb7461 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/directionFinder/SensorDataExt.kt @@ -0,0 +1,85 @@ +package no.nordicsemi.android.toolbox.profile.data.directionFinder + +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue + +fun SensorValue?.mapValues(selector: (T) -> R): List? = + this?.values?.map(selector) + +fun > SensorValue?.medianValue(selector: (T) -> R): R? = + this?.values?.map(selector)?.sorted()?.let { it.getOrNull(it.size / 2) } + +fun SensorData.azimuthValues() = azimuth.mapValues { it.azimuth } + +fun SensorData.elevationValues() = elevation.mapValues { it.elevation } + +fun SensorData.ifftValues() = mcpdDistance.mapValues { it.mcpd.ifft } + +fun SensorData.phaseSlopeValues() = mcpdDistance.mapValues { it.mcpd.phaseSlope } + +fun SensorData.rssiValues() = mcpdDistance.mapValues { it.mcpd.rssi } + +fun SensorData.bestEffortValues() = mcpdDistance.mapValues { it.mcpd.best } + +fun SensorData.rttValues() = rttDistance.mapValues { it.rtt.value } + +fun SensorData.azimuthValue() = azimuth.medianValue { it.azimuth } + +fun SensorData.elevationValue() = elevation.medianValue { it.elevation } + +fun SensorData.ifftValue() = mcpdDistance.medianValue { it.mcpd.ifft } + +fun SensorData.phaseSlopeValue() = mcpdDistance.medianValue { it.mcpd.phaseSlope } + +fun SensorData.rssiValue() = mcpdDistance.medianValue { it.mcpd.rssi } + +fun SensorData.bestEffortValue() = mcpdDistance.medianValue { it.mcpd.best } + +fun SensorData.rttValue() = rttDistance.medianValue { it.rtt.value } + +fun SensorData.distanceValue() = bestEffortValue() ?: rttValue() + +fun SensorData.displayAzimuth() = azimuthValue()?.let { "$it°" } + +fun SensorData.displayDistance() = distanceValue()?.let { "${it}dm" } + +fun SensorData.displayElevation() = elevationValue()?.let { "$it°" } + +fun SensorData.isDistanceSettingsAvailable() = mcpdDistance != null || rttDistance != null + +fun SensorData.isMcpdSectionAvailable() = + rttValue() != null || rssiValue() != null || phaseSlopeValue() != null || bestEffortValue() != null + +enum class MeasurementSection(val displayName: String) { + DISTANCE_RTT("Round-Trip Time (RTT)"), + DISTANCE_MCPD_IFFT("Inverse Fast Fourier Transform (IFFT)"), + DISTANCE_MCPD_PHASE_SLOPE("Phase slope"), + DISTANCE_MCPD_RSSI("Rssi"), + DISTANCE_MCPD_BEST("Best effort distance estimate"); + + override fun toString(): String = displayName +} + +fun SensorData.availableSections(): List = listOfNotNull( + this.rttValue()?.let { MeasurementSection.DISTANCE_RTT }, + this.rssiValue()?.let { MeasurementSection.DISTANCE_MCPD_RSSI }, + this.ifftValue()?.let { MeasurementSection.DISTANCE_MCPD_IFFT }, + this.phaseSlopeValue()?.let { MeasurementSection.DISTANCE_MCPD_PHASE_SLOPE }, + this.bestEffortValue()?.let { MeasurementSection.DISTANCE_MCPD_BEST }, +) + +// Direction Finder Profile Events +data class Range( + val from: Int, + val to: Int +) + +fun SensorData.selectedMeasurementSectionValues(): List? = + when (this.selectedMeasurementSection) { + MeasurementSection.DISTANCE_RTT -> this.rttValues() + MeasurementSection.DISTANCE_MCPD_IFFT -> this.ifftValues() + MeasurementSection.DISTANCE_MCPD_PHASE_SLOPE -> this.phaseSlopeValues() + MeasurementSection.DISTANCE_MCPD_RSSI -> this.rssiValues() + MeasurementSection.DISTANCE_MCPD_BEST -> this.bestEffortValues() + null -> this.bestEffortValues() + } \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroEol.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroEol.kt new file mode 100644 index 00000000..f6b868f7 --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroEol.kt @@ -0,0 +1,31 @@ +package no.nordicsemi.android.toolbox.profile.data.uart + +enum class MacroEol { + LF, + CR, + CR_LF; + + override fun toString(): String { + return when (this) { + LF -> "LF" + CR -> "CR" + CR_LF -> "CR+LF" + } + } +} + +fun String.parseWithNewLineChar(newLineChar: MacroEol): String { + return when (newLineChar) { + MacroEol.LF -> this + MacroEol.CR_LF -> this.replace("\n", "\r\n") + MacroEol.CR -> this.replace("\n", "\r") + } +} + +fun String.toMacroEolUnicodeCharDisplay(newLineChar: MacroEol): String { + return when (newLineChar) { + MacroEol.CR -> this.replace("\n", "\u240D\r\n") + MacroEol.LF -> this + "\u240A" + MacroEol.CR_LF -> this.replace("\n", "\u240A\u240D\r") + } +} diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroIcon.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroIcon.kt new file mode 100644 index 00000000..c773e8cf --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/MacroIcon.kt @@ -0,0 +1,31 @@ +package no.nordicsemi.android.toolbox.profile.data.uart + +enum class MacroIcon(val index: Int) { + LEFT(0), + UP(1), + RIGHT(2), + DOWN(3), + SETTINGS(4), + REW(5), + PLAY(6), + PAUSE(7), + STOP(8), + FWD(9), + INFO(10), + NUMBER_1(11), + NUMBER_2(12), + NUMBER_3(13), + NUMBER_4(14), + NUMBER_5(15), + NUMBER_6(16), + NUMBER_7(17), + NUMBER_8(18), + NUMBER_9(19); + + companion object { + fun create(index: Int): MacroIcon { + return entries.firstOrNull { it.index == index } + ?: throw IllegalArgumentException("Cannot create MacroIcon for index: $index") + } + } +} \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTConfiguration.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTConfiguration.kt new file mode 100644 index 00000000..fff5ed1c --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTConfiguration.kt @@ -0,0 +1,16 @@ +package no.nordicsemi.android.toolbox.profile.data.uart + +private const val MACROS_SIZES = 9 + +data class UARTConfiguration( + val id: Int?, + val name: String, + val macros: List = List(MACROS_SIZES) { null } +) { + + init { + if (macros.size < MACROS_SIZES) { + throw IllegalArgumentException("Macros should always have $MACROS_SIZES positions.") + } + } +} \ No newline at end of file diff --git a/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTMacro.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTMacro.kt new file mode 100644 index 00000000..89bc88dd --- /dev/null +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uart/UARTMacro.kt @@ -0,0 +1,7 @@ +package no.nordicsemi.android.toolbox.profile.data.uart + +data class UARTMacro( + val icon: MacroIcon, + val command: String?, + val newLineChar: MacroEol +) \ No newline at end of file diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/TemperatureUnit.kt b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uiMapper/TemperatureUnit.kt similarity index 84% rename from profile_hts/src/main/java/no/nordicsemi/android/hts/view/TemperatureUnit.kt rename to profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uiMapper/TemperatureUnit.kt index 48245d26..582a28e9 100644 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/TemperatureUnit.kt +++ b/profile_data/src/main/java/no/nordicsemi/android/toolbox/profile/data/uiMapper/TemperatureUnit.kt @@ -29,10 +29,18 @@ * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package no.nordicsemi.android.hts.view +package no.nordicsemi.android.toolbox.profile.data.uiMapper -internal enum class TemperatureUnit { +enum class TemperatureUnit { CELSIUS, FAHRENHEIT, - KELVIN + KELVIN; + + override fun toString(): String { + return when (this) { + CELSIUS -> "Celsius" + FAHRENHEIT -> "Fahrenheit" + KELVIN -> "Kelvin" + } + } } diff --git a/profile_gls/build.gradle.kts b/profile_gls/build.gradle.kts deleted file mode 100644 index f49b3b98..00000000 --- a/profile_gls/build.gradle.kts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.gls" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.server) - implementation(libs.nordic.blek.advertiser) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.chart) - - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) - - testImplementation(libs.hilt.android.testing) - kaptTest(libs.hilt.compiler) - testImplementation(libs.androidx.test.rules) - - testImplementation(libs.junit4) - testImplementation(libs.test.mockk) - testImplementation(libs.androidx.test.ext) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.slf4j.simple) - testImplementation(libs.test.robolectric) - testImplementation(libs.kotlin.junit) -} diff --git a/profile_gls/module-rules.pro b/profile_gls/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_gls/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_gls/src/main/AndroidManifest.xml b/profile_gls/src/main/AndroidManifest.xml deleted file mode 100644 index 07fd1f83..00000000 --- a/profile_gls/src/main/AndroidManifest.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt deleted file mode 100644 index db69b290..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSDestination.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls - -import no.nordicsemi.android.common.navigation.createDestination -import no.nordicsemi.android.common.navigation.defineDestination -import no.nordicsemi.android.gls.details.view.GLSDetailsScreen -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord - -internal val GlsDetailsDestinationId = createDestination, Unit>("gls-details-screen") - -val GLSDestination = defineDestination(GlsDetailsDestinationId) { GLSDetailsScreen() } diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSServer.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSServer.kt deleted file mode 100644 index 8799c40f..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/GLSServer.kt +++ /dev/null @@ -1,288 +0,0 @@ -package no.nordicsemi.android.gls - -import android.annotation.SuppressLint -import android.content.Context -import android.os.ParcelUuid -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.gls.main.viewmodel.BATTERY_LEVEL_CHARACTERISTIC_UUID -import no.nordicsemi.android.gls.main.viewmodel.BATTERY_SERVICE_UUID -import no.nordicsemi.android.gls.main.viewmodel.GLS_SERVICE_UUID -import no.nordicsemi.android.gls.main.viewmodel.GLUCOSE_MEASUREMENT_CHARACTERISTIC -import no.nordicsemi.android.gls.main.viewmodel.GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC -import no.nordicsemi.android.gls.main.viewmodel.RACP_CHARACTERISTIC -import no.nordicsemi.android.kotlin.ble.advertiser.BleAdvertiser -import no.nordicsemi.android.kotlin.ble.core.MockServerDevice -import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertisingConfig -import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertisingData -import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertisingSettings -import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission -import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty -import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray -import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser -import no.nordicsemi.android.kotlin.ble.server.main.ServerBleGatt -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristicConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceType -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBluetoothGattConnection -import javax.inject.Inject -import javax.inject.Singleton - -private const val STANDARD_DELAY = 1000L - -@SuppressLint("MissingPermission") -@Singleton -class GLSServer @Inject constructor( - private val scope: CoroutineScope, -) { - - private lateinit var server: ServerBleGatt - - private lateinit var glsCharacteristic: ServerBleGattCharacteristic - private lateinit var glsContextCharacteristic: ServerBleGattCharacteristic - private lateinit var racpCharacteristic: ServerBleGattCharacteristic - private lateinit var batteryLevelCharacteristic: ServerBleGattCharacteristic - - private var lastRequest = DataByteArray() - - val YOUNGEST_RECORD = DataByteArray.from( - 0x07, - 0x00, - 0x00, - 0xDC.toByte(), - 0x07, - 0x01, - 0x01, - 0x0C, - 0x1E, - 0x05, - 0x00, - 0x00, - 0x26, - 0xD2.toByte(), - 0x11 - ) - val OLDEST_RECORD = DataByteArray.from( - 0x07, - 0x04, - 0x00, - 0xDC.toByte(), - 0x07, - 0x01, - 0x01, - 0x0C, - 0x1E, - 0x11, - 0x00, - 0x00, - 0x82.toByte(), - 0xD2.toByte(), - 0x11 - ) - - val records = listOf( - YOUNGEST_RECORD, - DataByteArray.from( - 0x07, - 0x01, - 0x00, - 0xDC.toByte(), - 0x07, - 0x01, - 0x01, - 0x0C, - 0x1E, - 0x08, - 0x00, - 0x00, - 0x3D, - 0xD2.toByte(), - 0x11 - ), - DataByteArray.from( - 0x07, - 0x02, - 0x00, - 0xDC.toByte(), - 0x07, - 0x01, - 0x01, - 0x0C, - 0x1E, - 0x0B, - 0x00, - 0x00, - 0x54, - 0xD2.toByte(), - 0x11 - ), - DataByteArray.from( - 0x07, - 0x03, - 0x00, - 0xDC.toByte(), - 0x07, - 0x01, - 0x01, - 0x0C, - 0x1E, - 0x0E, - 0x00, - 0x00, - 0x6B, - 0xD2.toByte(), - 0x11 - ), - OLDEST_RECORD - ) - - private val SUCCESS = DataByteArray.from(0x06, 0x00, 0x01, 0x01) - - fun start( - context: Context, - device: MockServerDevice = MockServerDevice( - name = "Mock Glucose Server", - address = "AA:BB:CC:DD:EE:FF" - ), - ) = scope.launch { - val gmCharacteristic = ServerBleGattCharacteristicConfig( - GLUCOSE_MEASUREMENT_CHARACTERISTIC, - listOf(BleGattProperty.PROPERTY_NOTIFY), - listOf() - ) - - val gmContextCharacteristic = ServerBleGattCharacteristicConfig( - GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC, - listOf(BleGattProperty.PROPERTY_NOTIFY), - listOf() - ) - - val racpCharacteristic = ServerBleGattCharacteristicConfig( - RACP_CHARACTERISTIC, - listOf(BleGattProperty.PROPERTY_INDICATE, BleGattProperty.PROPERTY_WRITE), - listOf(BleGattPermission.PERMISSION_WRITE) - ) - - val serviceConfig = ServerBleGattServiceConfig( - GLS_SERVICE_UUID, - ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - listOf(gmCharacteristic, gmContextCharacteristic, racpCharacteristic) - ) - - val batteryLevelCharacteristic = ServerBleGattCharacteristicConfig( - BATTERY_LEVEL_CHARACTERISTIC_UUID, - listOf(BleGattProperty.PROPERTY_READ, BleGattProperty.PROPERTY_NOTIFY), - listOf(BleGattPermission.PERMISSION_READ) - ) - - val batteryService = ServerBleGattServiceConfig( - BATTERY_SERVICE_UUID, - ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - listOf(batteryLevelCharacteristic) - ) - - server = ServerBleGatt.create( - context = context, - config = arrayOf(serviceConfig, batteryService), - mock = device, - scope = scope, - ) - - BleAdvertiser.create(context) - .advertise( - config = BleAdvertisingConfig( - settings = BleAdvertisingSettings( - deviceName = "Glucose", - legacyMode = true, - ), - advertiseData = BleAdvertisingData( - includeDeviceName = true, - serviceUuid = ParcelUuid(GLS_SERVICE_UUID), - ) - ), - mock = device) - .launchIn(scope) - - launch { - server.connections - .mapNotNull { it.values.firstOrNull() } - .collect { setUpConnection(it) } - } - } - - internal fun stopServer() { - server.stopServer() - } - - private fun setUpConnection(connection: ServerBluetoothGattConnection) { - val glsService = connection.services.findService(GLS_SERVICE_UUID)!! - glsCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! - glsContextCharacteristic = glsService.findCharacteristic( - GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC - )!! - racpCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! - - val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!! - batteryLevelCharacteristic = batteryService.findCharacteristic( - BATTERY_LEVEL_CHARACTERISTIC_UUID - )!! - - startGlsService() - startBatteryService() - } - - private fun startGlsService() { - racpCharacteristic.value - .onEach { lastRequest = it } - .onEach { continueWithResponse() } //comment to make tests working - .launchIn(scope) - } - - internal fun continueWithResponse() { - sendResponse(lastRequest) - } - - private fun sendResponse(request: DataByteArray) = scope.launch { - when (request) { - RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords() -> { - records.forEach { - send(glsCharacteristic, it) - delay(100) - } - racpCharacteristic.setValueAndNotifyClient(SUCCESS) - } - - RecordAccessControlPointInputParser.reportLastStoredRecord() -> { - send(glsCharacteristic, records.last()) - send(racpCharacteristic, SUCCESS) - } - - RecordAccessControlPointInputParser.reportFirstStoredRecord() -> { - send(glsCharacteristic, records.first()) - send(racpCharacteristic, SUCCESS) - } - } - } - - private suspend fun send(characteristics: ServerBleGattCharacteristic, data: DataByteArray) { - characteristics.setValueAndNotifyClient(data) - } - - private fun startBatteryService() { - scope.launch { - repeat(100) { - send(batteryLevelCharacteristic, DataByteArray.from(0x61)) - delay(STANDARD_DELAY) - send(batteryLevelCharacteristic, DataByteArray.from(0x60)) - delay(STANDARD_DELAY) - send(batteryLevelCharacteristic, DataByteArray.from(0x5F)) - delay(STANDARD_DELAY) - } - } - } -} \ No newline at end of file diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt deleted file mode 100644 index 8a75c921..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/data/GLSServiceData.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.data - -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus - -internal data class GLSServiceData( - val records: Map = mapOf(), - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val requestStatus: RequestStatus = RequestStatus.IDLE -) diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/Field.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/Field.kt deleted file mode 100644 index 12869a2c..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/Field.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.details.view - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import no.nordicsemi.android.gls.R - -@Composable -internal fun Field(title: String, value: String) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = title, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline - ) - Text( - text = value, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.End - ) - } -} - -@Composable -internal fun BooleanField(title: String, value: Boolean) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.outline - ) - - if (value) { - Text( - text = stringResource(id = R.string.gls_yes), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } else { - Text( - text = stringResource(id = R.string.gls_no), - style = MaterialTheme.typography.bodyMedium, - color = colorResource(id = R.color.nordicGrass) - ) - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt deleted file mode 100644 index 0162cad4..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsContentView.kt +++ /dev/null @@ -1,260 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.details.view - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.main.view.toDisplayString -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord -import no.nordicsemi.android.ui.view.ScreenSection - -@Composable -internal fun GLSDetailsContentView(record: GLSRecord, context: GLSMeasurementContext?) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - Column(modifier = Modifier.padding(16.dp)) { - ScreenSection { - Field( - stringResource(id = R.string.gls_details_sequence_number), - record.sequenceNumber.toString() - ) - - record.time?.let { - Field( - stringResource(id = R.string.gls_details_date_and_time), - stringResource(R.string.gls_timestamp, it) - ) - } - - HorizontalDivider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - record.type?.let { - Field(stringResource(id = R.string.gls_details_type), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - - record.sampleLocation?.let { - Field(stringResource(id = R.string.gls_details_location), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - - record.glucoseConcentration?.let { glucoseConcentration -> - record.unit?.let { unit -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom - ) { - Text( - text = stringResource(id = R.string.gls_details_glucose_condensation_title), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline - ) - Text( - text = stringResource( - id = R.string.gls_details_glucose_condensation_field, - glucoseConcentration, - unit.toDisplayString() - ), - style = MaterialTheme.typography.titleLarge - ) - } - } - } - - record.status?.let { - HorizontalDivider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - BooleanField( - stringResource(id = R.string.gls_details_battery_low), - it.deviceBatteryLow - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_malfunction), - it.sensorMalfunction - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_insufficient_sample), - it.sampleSizeInsufficient - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_insertion_error), - it.stripInsertionError - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_type_incorrect), - it.stripTypeIncorrect - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_result_too_high), - it.sensorResultHigherThenDeviceCanProcess - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_sensor_result_too_low), - it.sensorResultLowerThenDeviceCanProcess - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_temperature_too_high), - it.sensorTemperatureTooHigh - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_temperature_too_low), - it.sensorTemperatureTooLow - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_strip_pulled_too_soon), - it.sensorReadInterrupted - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField( - stringResource(id = R.string.gls_details_general_device_fault), - it.generalDeviceFault - ) - Spacer(modifier = Modifier.size(4.dp)) - BooleanField(stringResource(id = R.string.gls_details_time_fault), it.timeFault) - Spacer(modifier = Modifier.size(4.dp)) - } - - context?.let { - HorizontalDivider( - color = MaterialTheme.colorScheme.secondary, - thickness = 1.dp, - modifier = Modifier.padding(vertical = 16.dp) - ) - - Field( - stringResource(id = R.string.gls_context_title), - stringResource(id = R.string.gls_available) - ) - Spacer(modifier = Modifier.size(4.dp)) - it.carbohydrate?.let { - Field( - stringResource(id = R.string.gls_context_carbohydrate), - it.toDisplayString() - ) - Spacer(modifier = Modifier.size(4.dp)) - } - it.meal?.let { - Field(stringResource(id = R.string.gls_context_meal), it.toDisplayString()) - Spacer(modifier = Modifier.size(4.dp)) - } - it.tester?.let { - Field( - stringResource(id = R.string.gls_context_tester), - it.toDisplayString() - ) - Spacer(modifier = Modifier.size(4.dp)) - } - it.health?.let { - Field( - stringResource(id = R.string.gls_context_health), - it.toDisplayString() - ) - Spacer(modifier = Modifier.size(4.dp)) - } - it.exerciseDuration?.let { exerciseDuration -> - it.exerciseIntensity?.let { exerciseIntensity -> - Field( - stringResource(id = R.string.gls_context_exercise_title), - stringResource( - id = R.string.gls_context_exercise_field, - exerciseDuration, - exerciseIntensity - ) - ) - } - } - - it.medicationUnit?.let { medicationUnit -> - Spacer(modifier = Modifier.size(4.dp)) - val medicationField = String.format( - stringResource(id = R.string.gls_context_medication_field), - it.medicationQuantity, - medicationUnit.toDisplayString(), - it.medication?.toDisplayString() - ) - Field( - stringResource(id = R.string.gls_context_medication_title), - medicationField - ) - } - - it.HbA1c?.let { hbA1c -> - Spacer(modifier = Modifier.size(4.dp)) - Field( - stringResource(id = R.string.gls_context_hba1c_title), - stringResource(id = R.string.gls_context_hba1c_field, hbA1c) - ) - } - - Spacer(modifier = Modifier.size(4.dp)) - } ?: Field( - stringResource(id = R.string.gls_context_title), - stringResource(id = R.string.gls_unavailable) - ) - } - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt deleted file mode 100644 index 67c0e49e..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/view/GLSDetailsScreen.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.details.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.res.stringResource -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.details.viewmodel.GLSDetailsViewModel -import no.nordicsemi.android.ui.view.LoggerBackIconAppBar - -@Composable -internal fun GLSDetailsScreen() { - val viewModel: GLSDetailsViewModel = hiltViewModel() - val record by viewModel.record.collectAsStateWithLifecycle() - - Column { - LoggerBackIconAppBar(stringResource(id = R.string.gls_title)) { - viewModel.navigateBack() - } - - GLSDetailsContentView(record.first, record.second) - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/viewmodel/GLSDetailsViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/details/viewmodel/GLSDetailsViewModel.kt deleted file mode 100644 index 1c5bd9ec..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/details/viewmodel/GLSDetailsViewModel.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.details.viewmodel - -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.common.navigation.viewmodel.SimpleNavigationViewModel -import no.nordicsemi.android.gls.GlsDetailsDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class GLSDetailsViewModel @Inject constructor( - private val navigationManager: Navigator, - private val savedStateHandle: SavedStateHandle -) : SimpleNavigationViewModel(navigationManager, savedStateHandle) { - - private val _record = MutableStateFlow(parameterOf(GlsDetailsDestinationId)) - val record = _record.asStateFlow() - - fun navigateBack() { - navigationManager.navigateUp() - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt deleted file mode 100644 index f8c9830b..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSContentView.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.view - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.data.GLSServiceData -import no.nordicsemi.android.gls.data.WorkingMode -import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun GLSContentView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SettingsView(state, onEvent) - - Spacer(modifier = Modifier.height(16.dp)) - - RecordsView(state) - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - - Spacer(modifier = Modifier.height(16.dp)) - } -} - -@Composable -private fun SettingsView(state: GLSServiceData, onEvent: (GLSScreenViewEvent) -> Unit) { - ScreenSection { - SectionTitle(icon = Icons.Default.Settings, title = "Request items") - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - if (state.requestStatus == RequestStatus.PENDING) { - CircularProgressIndicator() - } else { - WorkingMode.values().forEach { - Button(onClick = { onEvent(OnWorkingModeSelected(it)) }) { - Text(it.toDisplayString()) - } - } - } - } - } -} - -@Composable -private fun RecordsView(state: GLSServiceData) { - ScreenSection { - if (state.records.isEmpty()) { - RecordsViewWithoutData() - } else { - RecordsViewWithData(state) - } - - } -} - -@Composable -private fun RecordsViewWithData(state: GLSServiceData) { - Column(modifier = Modifier.fillMaxWidth()) { - SectionTitle(resId = R.drawable.ic_records, title = "Records") - - Spacer(modifier = Modifier.height(16.dp)) - - state.records.keys.forEachIndexed { i, it -> - RecordItem(it) - - if (i < state.records.size - 1) { - Spacer(modifier = Modifier.size(8.dp)) - } - } - } -} - -@Composable -private fun RecordItem(record: GLSRecord) { - val viewModel: GLSViewModel = hiltViewModel() - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(10.dp)) - .clickable { viewModel.onEvent(OnGLSRecordClick(record)) } - .padding(8.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - record.time?.let { - Text( - text = stringResource(R.string.gls_timestamp, it), - style = MaterialTheme.typography.titleMedium - ) - } - - Spacer(modifier = Modifier.size(4.dp)) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Bottom, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = record.type.toDisplayString(), - style = MaterialTheme.typography.bodySmall - ) - - record.glucoseConcentration?.let { glucoseConcentration -> record.unit?.let { unit -> - Text( - text = glucoseConcentrationDisplayValue(glucoseConcentration, unit), - style = MaterialTheme.typography.labelLarge, - ) - } } - } - } - } -} - -@Composable -private fun RecordsViewWithoutData() { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - SectionTitle(icon = Icons.Default.Search, title = "No items") - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = stringResource(id = R.string.gls_no_records_info), - style = MaterialTheme.typography.bodyMedium - ) - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt deleted file mode 100644 index 0e5e8658..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSMapper.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.data.WorkingMode -import no.nordicsemi.android.kotlin.ble.profile.gls.data.ConcentrationUnit -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordType - -@Composable -internal fun RecordType?.toDisplayString(): String { - return when (this) { - RecordType.CAPILLARY_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_capillary_whole_blood) - RecordType.CAPILLARY_PLASMA -> stringResource(id = R.string.gls_type_capillary_plasma) - RecordType.VENOUS_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_venous_whole_blood) - RecordType.VENOUS_PLASMA -> stringResource(id = R.string.gls_type_venous_plasma) - RecordType.ARTERIAL_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_arterial_whole_blood) - RecordType.ARTERIAL_PLASMA -> stringResource(id = R.string.gls_type_arterial_plasma) - RecordType.UNDETERMINED_WHOLE_BLOOD -> stringResource(id = R.string.gls_type_undetermined_whole_blood) - RecordType.UNDETERMINED_PLASMA -> stringResource(id = R.string.gls_type_undetermined_plasma) - RecordType.INTERSTITIAL_FLUID -> stringResource(id = R.string.gls_type_interstitial_fluid) - RecordType.CONTROL_SOLUTION -> stringResource(id = R.string.gls_type_control_solution) - null -> stringResource(id = R.string.gls_type_reserved) - } -} - -@Composable -internal fun ConcentrationUnit.toDisplayString(): String { - return when (this) { - //TODO("Check unit_kgpl --> mgpdl") - ConcentrationUnit.UNIT_KGPL -> stringResource(id = R.string.gls_unit_mgpdl) - ConcentrationUnit.UNIT_MOLPL -> stringResource(id = R.string.gls_unit_mmolpl) - } -} - -@Composable -internal fun WorkingMode.toDisplayString(): String { - return when (this) { - WorkingMode.ALL -> stringResource(id = R.string.gls__working_mode__all) - WorkingMode.LAST -> stringResource(id = R.string.gls__working_mode__last) - WorkingMode.FIRST -> stringResource(id = R.string.gls__working_mode__first) - } -} - -@Composable -internal fun glucoseConcentrationDisplayValue(value: Float, unit: ConcentrationUnit): String { - val result = when (unit) { - ConcentrationUnit.UNIT_KGPL -> value * 100000.0f - ConcentrationUnit.UNIT_MOLPL -> value * 1000.0f - } - return String.format("%.2f %s", result, unit.toDisplayString()) -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt deleted file mode 100644 index f7df8e4b..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.gls.R -import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun GLSScreen() { - val viewModel: GLSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(DisconnectEvent) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.glsServiceData.connectionState, - title = R.string.gls_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.glsServiceData.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> GLSContentView(state.glsServiceData) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt deleted file mode 100644 index 2ca44d61..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSScreenViewEvent.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.view - -import no.nordicsemi.android.gls.data.WorkingMode -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord - -internal sealed class GLSScreenViewEvent - -internal data class OnWorkingModeSelected(val workingMode: WorkingMode) : GLSScreenViewEvent() - -internal data class OnGLSRecordClick(val record: GLSRecord) : GLSScreenViewEvent() - -internal object DisconnectEvent : GLSScreenViewEvent() - -internal object OpenLoggerEvent : GLSScreenViewEvent() diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSViewState.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSViewState.kt deleted file mode 100644 index 9d5125c3..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/view/GLSViewState.kt +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.view - -import no.nordicsemi.android.gls.data.GLSServiceData -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSMeasurementContext -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus - -internal data class GLSViewState( - val glsServiceData: GLSServiceData = GLSServiceData(), - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - glsServiceData.connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } - - fun copyWithNewConnectionState(connectionState: GattConnectionStateWithStatus): GLSViewState { - return copy(glsServiceData = glsServiceData.copy(connectionState = connectionState)) - } - - fun copyAndClear(): GLSViewState { - return copy(glsServiceData = glsServiceData.copy(records = mapOf(), requestStatus = RequestStatus.IDLE)) - } - - fun copyWithNewRequestStatus(requestStatus: RequestStatus): GLSViewState { - return copy(glsServiceData = glsServiceData.copy(requestStatus = requestStatus)) - } - - fun copyWithNewBatteryLevel(batteryLevel: Int): GLSViewState { - return copy(glsServiceData = glsServiceData.copy(batteryLevel = batteryLevel)) - } - - //todo optimise - fun copyWithNewRecord(record: GLSRecord): GLSViewState { - val records = glsServiceData.records.toMutableMap() - records[record] = null - return copy(glsServiceData = glsServiceData.copy(records = records.toMap())) - } - - //todo optimise - fun copyWithNewContext(context: GLSMeasurementContext): GLSViewState { - val records = glsServiceData.records.toMutableMap() - return records.keys.firstOrNull { it.sequenceNumber == context.sequenceNumber }?.let { - records[it] = context - copy(glsServiceData = glsServiceData.copy(records = records.toMap())) - } ?: this - } -} diff --git a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt b/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt deleted file mode 100644 index 54720677..00000000 --- a/profile_gls/src/main/java/no/nordicsemi/android/gls/main/viewmodel/GLSViewModel.kt +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.gls.main.viewmodel - -import android.annotation.SuppressLint -import android.content.Context -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.gls.GlsDetailsDestinationId -import no.nordicsemi.android.gls.data.WorkingMode -import no.nordicsemi.android.gls.main.view.DisconnectEvent -import no.nordicsemi.android.gls.main.view.GLSScreenViewEvent -import no.nordicsemi.android.gls.main.view.GLSViewState -import no.nordicsemi.android.gls.main.view.OnGLSRecordClick -import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected -import no.nordicsemi.android.gls.main.view.OpenLoggerEvent -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.core.errors.GattOperationException -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementContextParser -import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementParser -import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointInputParser -import no.nordicsemi.android.kotlin.ble.profile.gls.RecordAccessControlPointParser -import no.nordicsemi.android.kotlin.ble.profile.gls.data.GLSRecord -import no.nordicsemi.android.kotlin.ble.profile.gls.data.NumberOfRecordsData -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RecordAccessControlPointData -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.data.ResponseData -import no.nordicsemi.android.kotlin.ble.profile.racp.RACPOpCode -import no.nordicsemi.android.kotlin.ble.profile.racp.RACPResponseCode -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import no.nordicsemi.android.ui.view.StringConst -import no.nordicsemi.android.utils.tryOrLog -import timber.log.Timber -import java.util.UUID -import javax.inject.Inject - -val GLS_SERVICE_UUID: UUID = UUID.fromString("00001808-0000-1000-8000-00805f9b34fb") - -val GLUCOSE_MEASUREMENT_CHARACTERISTIC = UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") -val GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC = UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") -val GLUCOSE_FEATURE_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") -val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") - -val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@HiltViewModel -internal class GLSViewModel @Inject constructor( - @ApplicationContext private val context: Context, - private val navigationManager: Navigator, - private val analytics: AppAnalytics, - private val stringConst: StringConst, -) : ViewModel() { - - private var client: ClientBleGatt? = null - private var logger: nRFLoggerTree? = null - - private lateinit var glucoseMeasurementCharacteristic: ClientBleGattCharacteristic - private lateinit var recordAccessControlPointCharacteristic: ClientBleGattCharacteristic - - private val _state = MutableStateFlow(GLSViewState()) - val state = _state.asStateFlow() - - private val highestSequenceNumber - get() = state.value.glsServiceData.records.keys.maxByOrNull { it.sequenceNumber }?.sequenceNumber ?: -1 - - init { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(GLS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - internal fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - fun onEvent(event: GLSScreenViewEvent) { - when (event) { - OpenLoggerEvent -> LoggerLauncher.launch(context, logger?.session as? LogSession) - is OnWorkingModeSelected -> onEvent(event) - is OnGLSRecordClick -> navigateToDetails(event.record) - DisconnectEvent -> onDisconnectEvent() - } - } - - private fun onDisconnectEvent() { - client?.disconnect() - navigationManager.navigateUp() - } - - private fun navigateToDetails(record: GLSRecord) { - val context = state.value.glsServiceData.records[record] - navigationManager.navigateTo(GlsDetailsDestinationId, record to context) - } - - private fun onDeviceSelected(device: ServerDevice) { - startGattClient(device) - } - - private fun onEvent(event: OnWorkingModeSelected) = viewModelScope.launch { - when (event.workingMode) { - WorkingMode.ALL -> requestAllRecords() - WorkingMode.LAST -> requestLastRecord() - WorkingMode.FIRST -> requestFirstRecord() - } - } - - private fun startGattClient(device: ServerDevice) = viewModelScope.launch { - _state.value = _state.value.copy(deviceName = device.name) - initLogger(device) - - val client = ClientBleGatt.connect(context, device, viewModelScope) - this@GLSViewModel.client = client - - client.waitForBonding() - - client.connectionStateWithStatus - .filterNotNull() - .onEach { _state.value = _state.value.copyWithNewConnectionState(it) } - .onEach { logAnalytics(it) } - .launchIn(viewModelScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - onMissingServices() - } - } - - private fun onMissingServices() { - _state.value = state.value.copy(missingServices = true) - client?.disconnect() - } - - internal fun logAnalytics(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.GLS)) - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val glsService = services.findService(GLS_SERVICE_UUID)!! - glucoseMeasurementCharacteristic = glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CHARACTERISTIC)!! - recordAccessControlPointCharacteristic = glsService.findCharacteristic(RACP_CHARACTERISTIC)!! - - glucoseMeasurementCharacteristic.getNotifications() - .mapNotNull { GlucoseMeasurementParser.parse(it) } - .onEach { _state.value = _state.value.copyWithNewRecord(it) } - .catch { it.printStackTrace() } - .launchIn(viewModelScope) - - glsService.findCharacteristic(GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC)?.getNotifications() - ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) } - ?.onEach { _state.value = _state.value.copyWithNewContext(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(viewModelScope) - - recordAccessControlPointCharacteristic.getNotifications() - .mapNotNull { RecordAccessControlPointParser.parse(it) } - .onEach { onAccessControlPointDataReceived(it) } - .catch { it.printStackTrace() } - .launchIn(viewModelScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { _state.value = _state.value.copyWithNewBatteryLevel(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(viewModelScope) - } - - private fun onAccessControlPointDataReceived(data: RecordAccessControlPointData) = viewModelScope.launch { - when (data) { - is NumberOfRecordsData -> onNumberOfRecordsReceived(data.numberOfRecords) - is ResponseData -> when (data.responseCode) { - RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted(data.requestCode) - RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> onRecordAccessOperationCompletedWithNoRecordsFound() - RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED, - RACPResponseCode.RACP_ERROR_INVALID_OPERATOR, - RACPResponseCode.RACP_ERROR_OPERATOR_NOT_SUPPORTED, - RACPResponseCode.RACP_ERROR_INVALID_OPERAND, - RACPResponseCode.RACP_ERROR_ABORT_UNSUCCESSFUL, - RACPResponseCode.RACP_ERROR_PROCEDURE_NOT_COMPLETED, - RACPResponseCode.RACP_ERROR_OPERAND_NOT_SUPPORTED -> onRecordAccessOperationError(data.responseCode) - } - } - } - - private fun onRecordAccessOperationCompleted(requestCode: RACPOpCode) { - val status = when (requestCode) { - RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED - else -> RequestStatus.SUCCESS - } - _state.value = _state.value.copyWithNewRequestStatus(status) - } - - private fun onRecordAccessOperationCompletedWithNoRecordsFound() { - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) - } - - private suspend fun onNumberOfRecordsReceived(numberOfRecords: Int) { - if (numberOfRecords > 0) { - try { - if (state.value.glsServiceData.records.isNotEmpty()) { - tryOrLog { - recordAccessControlPointCharacteristic.write( - RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo(highestSequenceNumber) - ) - } - } else { - tryOrLog { - recordAccessControlPointCharacteristic.write( - RecordAccessControlPointInputParser.reportAllStoredRecords() - ) - } - } - } catch (e: GattOperationException) { - e.printStackTrace() - } - } - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.SUCCESS) - } - - private fun onRecordAccessOperationError(response: RACPResponseCode) { - if (response == RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED) { - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.NOT_SUPPORTED) - } else { - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) - } - } - - private fun clear() { - _state.value = _state.value.copyAndClear() - } - - private suspend fun requestLastRecord() { - clear() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportLastStoredRecord()) - } catch (e: Exception) { - e.printStackTrace() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) - } - } - - private suspend fun requestFirstRecord() { - clear() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportFirstStoredRecord()) - } catch (e: Exception) { - e.printStackTrace() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) - } - } - - private suspend fun requestAllRecords() { - clear() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.PENDING) - try { - recordAccessControlPointCharacteristic.write(RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords()) - } catch (e: Exception) { - e.printStackTrace() - _state.value = _state.value.copyWithNewRequestStatus(RequestStatus.FAILED) - } - } - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "GLS", device.address) - .also { Timber.plant(it) } - } -} diff --git a/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt b/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt deleted file mode 100644 index 3097cda7..00000000 --- a/profile_gls/src/test/java/no/nordicsemi/android/gls/GLSViewModelTest.kt +++ /dev/null @@ -1,188 +0,0 @@ -package no.nordicsemi.android.gls - -import android.content.Context -import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.HiltTestApplication -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit4.MockKRule -import io.mockk.justRun -import io.mockk.mockkStatic -import io.mockk.spyk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.common.core.ApplicationScope -import no.nordicsemi.android.common.logger.BleLoggerAndLauncher -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.gls.data.WorkingMode -import no.nordicsemi.android.gls.main.view.OnWorkingModeSelected -import no.nordicsemi.android.gls.main.viewmodel.GLSViewModel -import no.nordicsemi.android.kotlin.ble.core.MockServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.gls.GlucoseMeasurementParser -import no.nordicsemi.android.kotlin.ble.profile.gls.data.RequestStatus -import no.nordicsemi.android.ui.view.NordicLoggerFactory -import no.nordicsemi.android.ui.view.StringConst -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.robolectric.annotation.Config -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import kotlin.test.assertContentEquals - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@HiltAndroidTest -@Config(application = HiltTestApplication::class) -@RunWith(RobolectricTestRunner::class) -internal class GLSViewModelTest { - - @get:Rule - val mockkRule = MockKRule(this) - - @RelaxedMockK - lateinit var navigator: Navigator - - @RelaxedMockK - lateinit var analytics: AppAnalytics - - @MockK - lateinit var stringConst: StringConst - - @RelaxedMockK - lateinit var context: Context - - @RelaxedMockK - lateinit var logger: BleLoggerAndLauncher - - lateinit var viewModel: GLSViewModel - - lateinit var glsServer: GLSServer - - private val device = MockServerDevice( - name = "GLS Server", - address = "55:44:33:22:11" - ) - - @Before - fun setUp() { - Dispatchers.setMain(UnconfinedTestDispatcher()) - } - - @After - fun release() { - Dispatchers.resetMain() - } - - @Before - fun before() { - runBlocking { - mockkStatic("no.nordicsemi.android.common.core.ApplicationScopeKt") - every { ApplicationScope } returns CoroutineScope(UnconfinedTestDispatcher()) - every { stringConst.APP_NAME } returns "Test" - - viewModel = spyk(GLSViewModel(context, navigator, analytics, stringConst, object : - NordicLoggerFactory { - override fun createNordicLogger( - context: Context, - profile: String?, - key: String, - name: String?, - ): BleLoggerAndLauncher { - return logger - } - - })) - justRun { viewModel.logAnalytics(any()) } - - glsServer = GLSServer(CoroutineScope(UnconfinedTestDispatcher()), context, logger) - glsServer.start(spyk(), device) - } - } - - @Test - fun `when connection fails return disconnected`() = runTest { - val disconnectedState = GattConnectionStateWithStatus( - GattConnectionState.STATE_DISCONNECTED, - BleGattConnectionStatus.SUCCESS - ) - viewModel.handleResult(NavigationResult.Success(device)) - glsServer.stopServer() - - advanceUntilIdle() - - assertEquals(disconnectedState, viewModel.state.value.glsServiceData.connectionState) - } - - @Test - fun `when request first record then change status and get 1 record`() = runTest { - viewModel.handleResult(NavigationResult.Success(device)) - advanceUntilIdle() //Needed because of delay() in waitForBonding() - assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) - - viewModel.onEvent(OnWorkingModeSelected(WorkingMode.FIRST)) - assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) - - glsServer.continueWithResponse() //continue server breakpoint - - assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) - assertEquals(1, viewModel.state.value.glsServiceData.records.size) - - val parsedResponse = GlucoseMeasurementParser.parse(glsServer.YOUNGEST_RECORD) - assertEquals(parsedResponse, viewModel.state.value.glsServiceData.records.keys.first()) - } - - @Test - fun `when request last record then change status and get 1 record`() = runTest { - viewModel.handleResult(NavigationResult.Success(device)) - advanceUntilIdle() //Needed because of delay() in waitForBonding() - assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) - - viewModel.onEvent(OnWorkingModeSelected(WorkingMode.LAST)) - assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) - - glsServer.continueWithResponse() //continue server breakpoint - - assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) - assertEquals(1, viewModel.state.value.glsServiceData.records.size) - - val parsedResponse = GlucoseMeasurementParser.parse(glsServer.OLDEST_RECORD) - assertEquals(parsedResponse, viewModel.state.value.glsServiceData.records.keys.first()) - } - - @Test - fun `when request all record then change status and get 5 records`() = runTest { - viewModel.handleResult(NavigationResult.Success(device)) - advanceUntilIdle() //Needed because of delay() in waitForBonding() - assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) - - viewModel.onEvent(OnWorkingModeSelected(WorkingMode.ALL)) - assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) - - glsServer.continueWithResponse() //continue server breakpoint - advanceUntilIdle() //We have to use because of delay() in sendAll() - - assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) - assertEquals(5, viewModel.state.value.glsServiceData.records.size) - - val expectedRecords = glsServer.records.map { GlucoseMeasurementParser.parse(it) } - assertContentEquals(expectedRecords, viewModel.state.value.glsServiceData.records.keys) - } -} diff --git a/profile_hrs/build.gradle.kts b/profile_hrs/build.gradle.kts deleted file mode 100644 index 48c2e8ba..00000000 --- a/profile_hrs/build.gradle.kts +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.hrs" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.chart) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_hrs/module-rules.pro b/profile_hrs/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_hrs/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_hrs/src/main/AndroidManifest.xml b/profile_hrs/src/main/AndroidManifest.xml deleted file mode 100644 index 86df61d9..00000000 --- a/profile_hrs/src/main/AndroidManifest.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt deleted file mode 100644 index b86fce95..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/data/HRSServiceData.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.data - -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hrs.data.HRSData - -internal data class HRSServiceData( - val data: List = emptyList(), - val bodySensorLocation: Int? = null, - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val zoomIn: Boolean = false, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } - - val heartRates = data.map { it.heartRate } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt deleted file mode 100644 index ec6acc4f..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSRepository.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.service - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.hrs.data.HRSServiceData -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hrs.data.HRSData -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HRSRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(HRSServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "HRS", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(HRSService::class.java, device) - } - - fun switchZoomIn() { - _data.value = _data.value.copy(zoomIn = !_data.value.zoomIn) - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onHRSDataChanged(data: HRSData) { - _data.value = _data.value.copy(data = _data.value.data + data) - } - - fun onBodySensorLocationChanged(bodySensorLocation: Int) { - _data.value = _data.value.copy(bodySensorLocation = bodySensorLocation) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = HRSServiceData() - } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt deleted file mode 100644 index b1785774..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/service/HRSService.kt +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.service - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.hrs.BodySensorLocationParser -import no.nordicsemi.android.kotlin.ble.profile.hrs.HRSDataParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.* -import javax.inject.Inject - -val HRS_SERVICE_UUID: UUID = UUID.fromString("0000180D-0000-1000-8000-00805f9b34fb") -private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID = UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb") -private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class HRSService : NotificationService() { - - @Inject - lateinit var repository: HRSRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@HRSService, device, lifecycleScope) - this@HRSService.client = client - - client.waitForBonding() - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val hrsService = services.findService(HRS_SERVICE_UUID)!! - val hrsMeasurementCharacteristic = hrsService.findCharacteristic(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID)!! - val bodySensorLocationCharacteristic = hrsService.findCharacteristic(BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID)!! - - val bodySensorLocation = bodySensorLocationCharacteristic.read() - BodySensorLocationParser.parse(bodySensorLocation)?.let { repository.onBodySensorLocationChanged(it) } - - hrsMeasurementCharacteristic.getNotifications() - .mapNotNull { HRSDataParser.parse(it) } - .onEach { repository.onHRSDataChanged(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt deleted file mode 100644 index 52dbc95c..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSContentView.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.hrs.R -import no.nordicsemi.android.hrs.data.HRSServiceData -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun HRSContentView(state: HRSServiceData, onEvent: (HRSScreenViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - ScreenSection { - SectionTitle( - resId = R.drawable.ic_chart_line, - title = stringResource(id = R.string.hrs_section_data), - menu = { Menu(state.zoomIn, onEvent) } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - LineChartView(state, state.zoomIn) - } - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} - -@Composable -private fun Menu(zoomIn: Boolean, onEvent: (HRSScreenViewEvent) -> Unit) { - val icon = when (zoomIn) { - true -> R.drawable.ic_zoom_out - false -> R.drawable.ic_zoom_in - } - IconButton(onClick = { onEvent(SwitchZoomEvent) }) { - Icon( - painter = painterResource(id = icon), - contentDescription = stringResource(id = R.string.hrs_zoom_icon) - ) - } -} - -@Preview -@Composable -private fun Preview() { - HRSContentView(state = HRSServiceData()) { } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt deleted file mode 100644 index 991aed56..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.hrs.R -import no.nordicsemi.android.hrs.viewmodel.HRSViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun HRSScreen() { - val viewModel: HRSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUpEvent) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.hrs_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> HRSContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt deleted file mode 100644 index a2eee68a..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/view/HRSScreenViewEvent.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.view - -internal sealed class HRSScreenViewEvent - -internal object SwitchZoomEvent : HRSScreenViewEvent() - -internal object DisconnectEvent : HRSScreenViewEvent() - -internal object NavigateUpEvent : HRSScreenViewEvent() - -internal object OpenLoggerEvent : HRSScreenViewEvent() diff --git a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt b/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt deleted file mode 100644 index 8f836513..00000000 --- a/profile_hrs/src/main/java/no/nordicsemi/android/hrs/viewmodel/HRSViewModel.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hrs.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.hrs.service.HRSRepository -import no.nordicsemi.android.hrs.service.HRS_SERVICE_UUID -import no.nordicsemi.android.hrs.view.DisconnectEvent -import no.nordicsemi.android.hrs.view.HRSScreenViewEvent -import no.nordicsemi.android.hrs.view.NavigateUpEvent -import no.nordicsemi.android.hrs.view.OpenLoggerEvent -import no.nordicsemi.android.hrs.view.SwitchZoomEvent -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class HRSViewModel @Inject constructor( - private val repository: HRSRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.HRS)) - } - }.launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(HRS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - fun onEvent(event: HRSScreenViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - NavigateUpEvent -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() - SwitchZoomEvent -> onZoomButtonClicked() - } - } - - private fun onZoomButtonClicked() { - repository.switchZoomIn() - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_hrs/src/main/res/values/strings.xml b/profile_hrs/src/main/res/values/strings.xml deleted file mode 100644 index a89f5a11..00000000 --- a/profile_hrs/src/main/res/values/strings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - HRS - Data - Icon to zoom chart in or out - diff --git a/profile_hts/build.gradle.kts b/profile_hts/build.gradle.kts deleted file mode 100644 index a4dde001..00000000 --- a/profile_hts/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.hts" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.nordic.core) - implementation(libs.nordic.ui) - implementation(libs.nordic.theme) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_hts/module-rules.pro b/profile_hts/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_hts/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_hts/src/main/AndroidManifest.xml b/profile_hts/src/main/AndroidManifest.xml deleted file mode 100644 index 2c7c225b..00000000 --- a/profile_hts/src/main/AndroidManifest.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt deleted file mode 100644 index d56e3f85..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/data/HTSServiceData.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.data - -import no.nordicsemi.android.hts.view.TemperatureUnit -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hts.data.HTSData - -internal data class HTSServiceData( - val data: HTSData = HTSData(), - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val temperatureUnit: TemperatureUnit = TemperatureUnit.CELSIUS, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt deleted file mode 100644 index 6942b27b..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSRepository.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.hts.data.HTSServiceData -import no.nordicsemi.android.hts.view.TemperatureUnit -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.hts.data.HTSData -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HTSRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(HTSServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "HTS", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - _data.value = _data.value.copy(deviceName = device.name) - initLogger(device) - serviceManager.startService(HTSService::class.java, device) - } - - internal fun setTemperatureUnit(temperatureUnit: TemperatureUnit) { - _data.value = _data.value.copy(temperatureUnit = temperatureUnit) - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onHTSDataChanged(data: HTSData) { - _data.value = _data.value.copy(data = data) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = HTSServiceData() - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt deleted file mode 100644 index 1b40888c..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/repository/HTSService.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.hts.HTSDataParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.* -import javax.inject.Inject - -val HTS_SERVICE_UUID: UUID = UUID.fromString("00001809-0000-1000-8000-00805f9b34fb") -private val HTS_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class HTSService : NotificationService() { - - @Inject - lateinit var repository: HTSRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@HTSService, device, lifecycleScope) - this@HTSService.client = client - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val htsService = services.findService(HTS_SERVICE_UUID)!! - val htsMeasurementCharacteristic = htsService.findCharacteristic(HTS_MEASUREMENT_CHARACTERISTIC_UUID)!! - - htsMeasurementCharacteristic.getNotifications() - .mapNotNull { HTSDataParser.parse(it) } - .onEach { repository.onHTSDataChanged(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt deleted file mode 100644 index a3794439..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSContentView.kt +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.common.ui.view.RadioButtonGroup -import no.nordicsemi.android.hts.R -import no.nordicsemi.android.hts.data.HTSServiceData -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun HTSContentView(state: HTSServiceData, onEvent: (HTSScreenViewEvent) -> Unit) { - Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - ScreenSection { - SectionTitle(resId = R.drawable.ic_thermometer, title = "Settings") - - Spacer(modifier = Modifier.height(16.dp)) - - RadioButtonGroup(viewEntity = state.temperatureUnit.temperatureSettingsItems()) { - onEvent(OnTemperatureUnitSelected(it.label.toTemperatureUnit())) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - ScreenSection { - SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.hts_records_section)) - - Spacer(modifier = Modifier.height(16.dp)) - - KeyValueField( - stringResource(id = R.string.hts_temperature), - displayTemperature(state.data.temperature, state.temperatureUnit) - ) - } - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} - -@Preview -@Composable -private fun Preview() { - HTSContentView(state = HTSServiceData()) { } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSMapper.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSMapper.kt deleted file mode 100644 index 4519f77a..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSMapper.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -import no.nordicsemi.android.common.ui.view.RadioButtonItem -import no.nordicsemi.android.common.ui.view.RadioGroupViewEntity -import java.util.Locale - -private const val DISPLAY_FAHRENHEIT = "°F" -private const val DISPLAY_CELSIUS = "°C" -private const val DISPLAY_KELVIN = "°K" - -internal fun displayTemperature(value: Float, temperatureUnit: TemperatureUnit): String { - return when (temperatureUnit) { - TemperatureUnit.CELSIUS -> String.format(Locale.US, "%.1f °C", value) - TemperatureUnit.FAHRENHEIT -> String.format(Locale.US, "%.1f °F", value * 1.8f + 32f) - TemperatureUnit.KELVIN -> String.format(Locale.US, "%.1f °K", value + 273.15f) - } -} - -internal fun String.toTemperatureUnit(): TemperatureUnit { - return when (this) { - DISPLAY_CELSIUS -> TemperatureUnit.CELSIUS - DISPLAY_FAHRENHEIT -> TemperatureUnit.FAHRENHEIT - DISPLAY_KELVIN -> TemperatureUnit.KELVIN - else -> throw IllegalArgumentException("Can't create TemperatureUnit from this label: $this") - } -} - -internal fun TemperatureUnit.temperatureSettingsItems(): RadioGroupViewEntity { - return RadioGroupViewEntity( - TemperatureUnit.entries.map { createRadioButtonItem(it, this) } - ) -} - -private fun createRadioButtonItem( - unit: TemperatureUnit, - selectedTemperatureUnit: TemperatureUnit -): RadioButtonItem { - return RadioButtonItem(displayTemperature(unit), unit == selectedTemperatureUnit) -} - -private fun displayTemperature(unit: TemperatureUnit): String { - return when (unit) { - TemperatureUnit.CELSIUS -> DISPLAY_CELSIUS - TemperatureUnit.FAHRENHEIT -> DISPLAY_FAHRENHEIT - TemperatureUnit.KELVIN -> DISPLAY_KELVIN - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt deleted file mode 100644 index 4e6cf825..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreen.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.hts.R -import no.nordicsemi.android.hts.viewmodel.HTSViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun HTSScreen() { - val viewModel: HTSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUp) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.hts_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> HTSContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt deleted file mode 100644 index eb12feaa..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/view/HTSScreenViewEvent.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.view - -internal sealed class HTSScreenViewEvent - -internal data class OnTemperatureUnitSelected(val value: TemperatureUnit) : HTSScreenViewEvent() - -internal object DisconnectEvent : HTSScreenViewEvent() - -internal object NavigateUp : HTSScreenViewEvent() - -internal object OpenLoggerEvent : HTSScreenViewEvent() diff --git a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt b/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt deleted file mode 100644 index c7d64d71..00000000 --- a/profile_hts/src/main/java/no/nordicsemi/android/hts/viewmodel/HTSViewModel.kt +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.hts.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.hts.repository.HTSRepository -import no.nordicsemi.android.hts.repository.HTS_SERVICE_UUID -import no.nordicsemi.android.hts.view.DisconnectEvent -import no.nordicsemi.android.hts.view.HTSScreenViewEvent -import no.nordicsemi.android.hts.view.NavigateUp -import no.nordicsemi.android.hts.view.OnTemperatureUnitSelected -import no.nordicsemi.android.hts.view.OpenLoggerEvent -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class HTSViewModel @Inject constructor( - private val repository: HTSRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.HTS)) - } - }.launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(HTS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - fun onEvent(event: HTSScreenViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - is OnTemperatureUnitSelected -> onTemperatureUnitSelected(event) - NavigateUp -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() - } - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - private fun onTemperatureUnitSelected(event: OnTemperatureUnitSelected) { - repository.setTemperatureUnit(event.value) - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_hts/src/main/res/values/strings.xml b/profile_hts/src/main/res/values/strings.xml deleted file mode 100644 index 47bc028d..00000000 --- a/profile_hts/src/main/res/values/strings.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - HTS - - %.1f °C - %.1f °F - %.1f °K - - Temperature - Data - diff --git a/profile_manager/build.gradle.kts b/profile_manager/build.gradle.kts new file mode 100644 index 00000000..204a4ad1 --- /dev/null +++ b/profile_manager/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(libs.plugins.nordic.feature) +} + +android { + namespace = "no.nordicsemi.android.toolbox.profile.manager" +} + +dependencies { + implementation(project(":profile_data")) + implementation(project(":profile-parsers")) + implementation(project(":lib_utils")) + + implementation(libs.nordic.logger) + implementation(libs.nordic.log.timber) + implementation(libs.nordic.blek.client.android) + +} \ No newline at end of file diff --git a/profile_manager/module-rules.pro b/profile_manager/module-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/profile_manager/module-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/profile_manager/src/main/AndroidManifest.xml b/profile_manager/src/main/AndroidManifest.xml new file mode 100644 index 00000000..568741e5 --- /dev/null +++ b/profile_manager/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BPSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BPSManager.kt new file mode 100644 index 00000000..5ff48e8b --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BPSManager.kt @@ -0,0 +1,64 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureParser +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementParser +import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureParser +import no.nordicsemi.android.toolbox.profile.manager.repository.BPSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val BPM_CHARACTERISTIC_UUID = UUID.fromString("00002A35-0000-1000-8000-00805f9b34fb") +private val ICP_CHARACTERISTIC_UUID = UUID.fromString("00002A36-0000-1000-8000-00805f9b34fb") +private val BPF_CHARACTERISTIC_UUID = UUID.fromString("00002A49-0000-1000-8000-00805f9b34fb") + +internal class BPSManager : ServiceManager { + override val profile: Profile = Profile.BPS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics.firstOrNull { it.uuid == BPM_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { BloodPressureMeasurementParser.parse(it) } + ?.onEach { BPSRepository.updateBPSData(deviceId, it) } + ?.onCompletion { BPSRepository.clear(deviceId) } + ?.catch { e -> + e.printStackTrace() + Timber.e(e) + }?.launchIn(scope) + + remoteService.characteristics.firstOrNull { it.uuid == ICP_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { IntermediateCuffPressureParser.parse(it) } + ?.onEach { BPSRepository.updateICPData(deviceId, it) } + ?.onCompletion { BPSRepository.clear(deviceId) } + ?.catch { e -> + e.printStackTrace() + Timber.e(e) + }?.launchIn(scope) + + remoteService.characteristics.firstOrNull { it.uuid == BPF_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.read() + ?.let { + BloodPressureFeatureParser.parse(it) + }?.also { featureData -> + BPSRepository.updateBPSFeatureData(deviceId, featureData) + } + } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BatteryManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BatteryManager.kt new file mode 100644 index 00000000..090dca4a --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/BatteryManager.kt @@ -0,0 +1,70 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.toolbox.profile.parser.battery.BatteryLevelParser +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.manager.repository.BatteryRepository +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.CharacteristicProperty +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val BATTERY_LEVEL_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") + +internal class BatteryManager : ServiceManager { + override val profile: Profile = Profile.BATTERY + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + val batteryChar = remoteService.characteristics + .firstOrNull { it.uuid == BATTERY_LEVEL_CHARACTERISTIC_UUID.toKotlinUuid() } + + batteryChar?.let { characteristic -> + // If the characteristic supports READ, read the initial value + if (characteristic.properties.contains(CharacteristicProperty.READ)) { + try { + characteristic.read() + .let { + BatteryLevelParser.parse(it) + } + ?.let { batteryLevel -> + BatteryRepository.updateBatteryLevel(deviceId, batteryLevel) + } + + } catch (e: Exception) { + Timber.e("Error reading battery level: ${e.message}") + } + } + // Check if the characteristic supports NOTIFY or INDICATE property + if (characteristic.properties.contains(CharacteristicProperty.NOTIFY) + || characteristic.properties.contains(CharacteristicProperty.INDICATE) + ) { + // Start subscription for battery level updates + characteristic.subscribe() + .mapNotNull { BatteryLevelParser.parse(it) } + .onEach { batteryLevel -> + BatteryRepository.updateBatteryLevel(deviceId, batteryLevel) + } + .onCompletion { + BatteryRepository.clear(deviceId) + } + .catch { e -> + Timber.e(e) + } + .launchIn(scope) + } + } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CGMManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CGMManager.kt new file mode 100644 index 00000000..7b6d04a3 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CGMManager.kt @@ -0,0 +1,264 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMFeatureParser +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMMeasurementParser +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMSpecificOpsControlPointParser +import no.nordicsemi.android.toolbox.profile.parser.cgms.CGMStatusParser +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMErrorCode +import no.nordicsemi.android.toolbox.profile.parser.cgms.data.CGMOpCode +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.CGMSpecificOpsControlPointDataParser +import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointInputParser +import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointParser +import no.nordicsemi.android.toolbox.profile.parser.gls.data.NumberOfRecordsData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordAccessControlPointData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ResponseData +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode +import no.nordicsemi.android.toolbox.profile.manager.repository.CGMRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.lib.utils.logAndReport +import no.nordicsemi.android.toolbox.lib.utils.tryOrLog +import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.CharacteristicProperty +import no.nordicsemi.kotlin.ble.core.WriteType +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val CGM_STATUS_UUID = UUID.fromString("00002AA9-0000-1000-8000-00805f9b34fb") +private val CGM_FEATURE_UUID = UUID.fromString("00002AA8-0000-1000-8000-00805f9b34fb") +private val CGM_MEASUREMENT_UUID = UUID.fromString("00002AA7-0000-1000-8000-00805f9b34fb") +private val CGM_OPS_CONTROL_POINT_UUID = UUID.fromString("00002AAC-0000-1000-8000-00805f9b34fb") + +private val RACP_UUID = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") + +internal class CGMManager : ServiceManager { + override val profile: Profile + get() = Profile.CGM + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + // 1. Subscribe to CGM Measurement first + remoteService.characteristics + .firstOrNull { it.uuid == CGM_MEASUREMENT_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { CGMMeasurementParser.parse(it) }?.onEach { cgmRecords -> + if (sessionStartTime == 0L && !recordAccessRequestInProgress) { + val timeOffset = cgmRecords.minOf { it.timeOffset } + sessionStartTime = System.currentTimeMillis() - timeOffset * 60000L + } + + cgmRecords.map { + val timestamp = sessionStartTime + it.timeOffset * 60000L + CGMRecordWithSequenceNumber(it.timeOffset, it, timestamp) + }.apply { + CGMRepository.onMeasurementDataReceived(deviceId, this) + } + }?.onCompletion { CGMRepository.clear(deviceId) } + ?.catch { it.logAndReport() } + ?.launchIn(scope) + + // 2. Subscribe to RACP and store reference + remoteService.characteristics + .firstOrNull { it.uuid == RACP_UUID.toKotlinUuid() } + ?.let { racpCharacteristic -> + recordAccessControlPointCharacteristic = racpCharacteristic + racpCharacteristic.subscribe() + .mapNotNull { RecordAccessControlPointParser.parse(it) } + .onEach { onAccessControlPointDataReceived(deviceId, it, scope) } + .catch { it.logAndReport() } + .launchIn(scope) + } + + // 3. Read CGM Feature + remoteService.characteristics + .firstOrNull { it.uuid == CGM_FEATURE_UUID.toKotlinUuid() } + ?.takeIf { it.properties.contains(CharacteristicProperty.READ) } + ?.read() + ?.let { CGMFeatureParser.parse(it) } + ?.let { secured = it.features.e2eCrcSupported } + + // 4. Read CGM Status + remoteService.characteristics + .firstOrNull { it.uuid == CGM_STATUS_UUID.toKotlinUuid() } + ?.takeIf { it.properties.contains(CharacteristicProperty.READ) } + ?.read() + ?.let { CGMStatusParser.parse(it) } + ?.let { + if (!it.status.sessionStopped) { + sessionStartTime = System.currentTimeMillis() - it.timeOffset * 60000L + } + } + + // 5. Subscribe to Ops Control Point + remoteService.characteristics + .firstOrNull { it.uuid == CGM_OPS_CONTROL_POINT_UUID.toKotlinUuid() } + ?.let { cgmOpsControlPointCharacteristic -> + opsControlPointCharacteristic = cgmOpsControlPointCharacteristic + cgmOpsControlPointCharacteristic.subscribe() + .mapNotNull { CGMSpecificOpsControlPointParser.parse(it) } + .onEach { + if (it.isOperationCompleted) { + sessionStartTime = + if (it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION) + System.currentTimeMillis() else 0 + } else if ( + it.requestCode == CGMOpCode.CGM_OP_CODE_START_SESSION && + it.errorCode == CGMErrorCode.CGM_ERROR_PROCEDURE_NOT_COMPLETED + ) { + sessionStartTime = 0 + } else if (it.requestCode == CGMOpCode.CGM_OP_CODE_STOP_SESSION) { + sessionStartTime = 0 + } + } + .onCompletion { CGMRepository.clear(deviceId) } + .catch { it.logAndReport() } + .launchIn(scope) + } + + // 6. Write to Ops Control if needed + if (sessionStartTime == 0L) { + try { + opsControlPointCharacteristic.write( + CGMSpecificOpsControlPointDataParser.startSession(secured), + WriteType.WITH_RESPONSE + ) + } catch (e: Exception) { + Timber.e("Error while starting session: ${e.message}") + } + } + } + } + + private fun onAccessControlPointDataReceived( + deviceId: String, + data: RecordAccessControlPointData, + scope: CoroutineScope + ) = scope.launch { + when (data) { + is NumberOfRecordsData -> onNumberOfRecordsReceived(deviceId, data.numberOfRecords) + + is ResponseData -> when (data.responseCode) { + RACPResponseCode.RACP_RESPONSE_SUCCESS -> + onRecordAccessOperationCompleted(deviceId, data.requestCode) + + RACPResponseCode.RACP_ERROR_NO_RECORDS_FOUND -> + onRecordAccessOperationCompletedWithNoRecordsFound(deviceId) + + else -> onRecordAccessOperationError(deviceId, data.responseCode) + } + } + } + + private fun onRecordAccessOperationError(deviceId: String, responseCode: RACPResponseCode) { + CGMRepository.updateNewRequestStatus( + deviceId = deviceId, + requestStatus = when (responseCode) { + RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED -> RequestStatus.NOT_SUPPORTED + else -> RequestStatus.FAILED + } + ) + } + + private fun onRecordAccessOperationCompletedWithNoRecordsFound(deviceId: String) { + CGMRepository.updateNewRequestStatus( + deviceId = deviceId, + requestStatus = RequestStatus.SUCCESS + ) + } + + private fun onRecordAccessOperationCompleted(deviceId: String, requestCode: RACPOpCode) { + CGMRepository.updateNewRequestStatus( + deviceId = deviceId, + requestStatus = when (requestCode) { + RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED + else -> RequestStatus.SUCCESS + } + ) + } + + private suspend fun onNumberOfRecordsReceived( + deviceId: String, + numberOfRecords: Int, + ) { + val state = CGMRepository.getData(deviceId) + val highestSequenceNumber = state.value + .records + .maxByOrNull { it.sequenceNumber } + ?.sequenceNumber + ?: -1 + + if (numberOfRecords > 0) + tryOrLog { + recordAccessControlPointCharacteristic + .write( + if (state.value.records.isNotEmpty()) { + RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo( + highestSequenceNumber.toShort() + ) + } else { + RecordAccessControlPointInputParser.reportAllStoredRecords() + }, + WriteType.WITH_RESPONSE + ) + } + CGMRepository.updateNewRequestStatus( + deviceId = deviceId, + requestStatus = RequestStatus.SUCCESS + ) + } + + companion object { + private lateinit var recordAccessControlPointCharacteristic: RemoteCharacteristic + private lateinit var opsControlPointCharacteristic: RemoteCharacteristic + + private var recordAccessRequestInProgress = false + private var sessionStartTime: Long = 0 + private var secured = false + + suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) { + writeOrSetStatusFailed(deviceId) { + recordAccessControlPointCharacteristic.write( + when (workingMode) { + WorkingMode.ALL -> RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords() + WorkingMode.LAST -> RecordAccessControlPointInputParser.reportLastStoredRecord() + WorkingMode.FIRST -> RecordAccessControlPointInputParser.reportFirstStoredRecord() + }, + WriteType.WITH_RESPONSE + ) + } + + } + + private suspend fun writeOrSetStatusFailed( + deviceId: String, + block: suspend () -> Unit + ) { + try { + block() + + } catch (e: Exception) { + e.printStackTrace() + CGMRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } + } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CSCManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CSCManager.kt new file mode 100644 index 00000000..8009970b --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/CSCManager.kt @@ -0,0 +1,42 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.toolbox.profile.parser.csc.CSCDataParser +import no.nordicsemi.android.toolbox.profile.manager.repository.CSCRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val CSC_MEASUREMENT_CHARACTERISTIC_UUID = + UUID.fromString("00002A5B-0000-1000-8000-00805f9b34fb") + +internal class CSCManager : ServiceManager { + override val profile: Profile + get() = Profile.CSC + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + remoteService.characteristics + .firstOrNull { it.uuid == CSC_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { + CSCDataParser.parse(it, CSCRepository.getData(deviceId).value.data.wheelSize) + } + ?.onEach { CSCRepository.onCSCDataChanged(deviceId, it) } + ?.catch { it.printStackTrace() } + ?.onCompletion { CSCRepository.clear(deviceId) } + ?.launchIn(scope) + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ChannelSoundingManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ChannelSoundingManager.kt new file mode 100644 index 00000000..cc140489 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ChannelSoundingManager.kt @@ -0,0 +1,66 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val RAS_FEATURES = UUID.fromString("00002C14-0000-1000-8000-00805F9B34FB") +private val REALTIME_RANGING_DATA = UUID.fromString("00002C15-0000-1000-8000-00805F9B34FB") +private val RAS_ON_DEMAND_RD = UUID.fromString("00002C16-0000-1000-8000-00805F9B34FB") +private val RAS_CP = UUID.fromString("00002C17-0000-1000-8000-00805F9B34FB") +private val RAS_RD_READY = UUID.fromString("00002C18-0000-1000-8000-00805F9B34FB") +private val RAS_RD_OVERWRITTEN = UUID.fromString("00002C19-0000-1000-8000-00805F9B34FB") + +internal class ChannelSoundingManager : ServiceManager { + override val profile: Profile + get() = Profile.CHANNEL_SOUNDING + + @OptIn(ExperimentalUuidApi::class, ExperimentalStdlibApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + remoteService.characteristics.firstOrNull { + it.uuid == RAS_FEATURES.toKotlinUuid() + } + ?.read() + ?.let { + val rasFeature = RasFeatureParser.parse(it) + Timber.tag("ChannelSoundingManager").d("Ranging Feature: $rasFeature") + } + + } + + data class RasFeature( + val realTimeRangingData: Boolean, + val retrieveLostSegments: Boolean, + val abortOperation: Boolean, + val filterRangingData: Boolean, + ) + + + object RasFeatureParser { + + fun parse(data: ByteArray): RasFeature { + require(data.size >= 4) { "RAS Features characteristic must be at least 4 bytes." } + + val featureBits = (data[0].toInt() and 0xFF) or + ((data[1].toInt() and 0xFF) shl 8) or + ((data[2].toInt() and 0xFF) shl 16) or + ((data[3].toInt() and 0xFF) shl 24) + + return RasFeature( + realTimeRangingData = featureBits and (1 shl 0) != 0, + retrieveLostSegments = featureBits and (1 shl 1) != 0, + abortOperation = featureBits and (1 shl 2) != 0, + filterRangingData = featureBits and (1 shl 3) != 0 + ) + } + + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFSManager.kt new file mode 100644 index 00000000..d27ff3ac --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/DFSManager.kt @@ -0,0 +1,157 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthalMeasurementDataParser +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointDataParser +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf.DDFDataParser +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMeasurementDataParser +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementDataParser +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.manager.repository.DFSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.lib.utils.logAndReport +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.CharacteristicProperty +import no.nordicsemi.kotlin.ble.core.WriteType +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val DISTANCE_MEASUREMENT_CHARACTERISTIC_UUID = + UUID.fromString("21490001-494a-4573-98af-f126af76f490") +private val AZIMUTH_MEASUREMENT_CHARACTERISTIC_UUID = + UUID.fromString("21490002-494a-4573-98af-f126af76f490") +private val ELEVATION_MEASUREMENT_CHARACTERISTIC_UUID = + UUID.fromString("21490003-494a-4573-98af-f126af76f490") +private val DDF_FEATURE_CHARACTERISTIC_UUID = + UUID.fromString("21490004-494a-4573-98af-f126af76f490") +private val CONTROL_POINT_CHARACTERISTIC_UUID = + UUID.fromString("21490005-494a-4573-98af-f126af76f490") + +internal class DFSManager : ServiceManager { + override val profile: Profile + get() = Profile.DFS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics + .firstOrNull { it.uuid == AZIMUTH_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { AzimuthalMeasurementDataParser().parse(it) } + ?.onEach { DFSRepository.addNewAzimuth(deviceId, it) } + ?.catch { it.logAndReport() } + ?.onCompletion { DFSRepository.clear(deviceId) } + ?.launchIn(scope) + + remoteService.characteristics + .firstOrNull { it.uuid == DISTANCE_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { DistanceMeasurementDataParser().parse(it) } + ?.onEach { DFSRepository.addNewDistance(deviceId, it) } + ?.catch { it.logAndReport() } + ?.onCompletion { DFSRepository.clear(deviceId) } + ?.launchIn(scope) + + remoteService.characteristics + .firstOrNull { it.uuid == ELEVATION_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { ElevationMeasurementDataParser().parse(it) } + ?.onEach { DFSRepository.addNewElevation(deviceId, it) } + ?.catch { it.logAndReport() } + ?.onCompletion { DFSRepository.clear(deviceId) } + ?.launchIn(scope) + + val ddfFeatureCharacteristics = remoteService.characteristics + .firstOrNull { it.uuid == DDF_FEATURE_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.apply { ddfFeatureCharacteristic = this } + val isReadPropertyAvailable = ddfFeatureCharacteristics + ?.properties?.contains(CharacteristicProperty.READ) + if (isReadPropertyAvailable == true) { + ddfFeatureCharacteristics.read() + .let { DDFDataParser().parse(it) } + ?.apply { DFSRepository.setAvailableDistanceModes(deviceId, this) } + } else { + Timber.e("Characteristic Property READ is not available for $ddfFeatureCharacteristics") + } + + remoteService.characteristics + .firstOrNull { it.uuid == CONTROL_POINT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.apply { controlPointCharacteristic = this } + ?.subscribe() + ?.mapNotNull { ControlPointDataParser().parse(it) } + ?.onEach { DFSRepository.onControlPointDataReceived(deviceId, it, scope) } + ?.catch { it.logAndReport() } + ?.onCompletion { DFSRepository.clear(deviceId) } + ?.launchIn(scope) + } + } + + companion object { + private lateinit var controlPointCharacteristic: RemoteCharacteristic + private lateinit var ddfFeatureCharacteristic: RemoteCharacteristic + + private val MCPD_ENABLED_BYTES = byteArrayOf(0x01, 0x01) + private val RTT_ENABLED_BYTES = byteArrayOf(0x01, 0x00) + private val CHECK_CONFIG_BYTES = byteArrayOf(0x0A) + + suspend fun enableDistanceMode(deviceId: String, mode: DistanceMode) { + val data = when (mode) { + DistanceMode.MCPD -> MCPD_ENABLED_BYTES + DistanceMode.RTT -> RTT_ENABLED_BYTES + } + try { + controlPointCharacteristic.write(data, WriteType.WITH_RESPONSE) + } catch (e: Exception) { + Timber.e(e, "Failed to enable distance mode: $mode for device: $deviceId") + DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } finally { + DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + + } + + suspend fun checkForCurrentDistanceMode(deviceId: String) { + try { + controlPointCharacteristic.write( + CHECK_CONFIG_BYTES, + writeType = WriteType.WITH_RESPONSE + ) + } catch (e: Exception) { + Timber.e(e, "Failed to check current distance mode for device: $deviceId") + DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } finally { + DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + } + + suspend fun checkAvailableFeatures(deviceId: String) { + DFSRepository.updateNewRequestStatus(deviceId, RequestStatus.PENDING) + val isReadPropertyAvailable = ddfFeatureCharacteristic + .properties.contains(CharacteristicProperty.READ) + if (isReadPropertyAvailable) { + ddfFeatureCharacteristic.read() + .let { DDFDataParser().parse(it) } + ?.apply { + DFSRepository.setAvailableDistanceModes(deviceId, this) + } + } else { + Timber.e("Characteristic Property READ is not available for $ddfFeatureCharacteristic") + } + } + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/GLSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/GLSManager.kt new file mode 100644 index 00000000..2c57dbe8 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/GLSManager.kt @@ -0,0 +1,184 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.GlucoseMeasurementContextParser +import no.nordicsemi.android.toolbox.profile.parser.gls.GlucoseMeasurementParser +import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointInputParser +import no.nordicsemi.android.toolbox.profile.parser.gls.RecordAccessControlPointParser +import no.nordicsemi.android.toolbox.profile.parser.gls.data.NumberOfRecordsData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RecordAccessControlPointData +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.parser.gls.data.ResponseData +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPOpCode +import no.nordicsemi.android.toolbox.profile.parser.racp.RACPResponseCode +import no.nordicsemi.android.toolbox.profile.manager.repository.GLSRepository +import no.nordicsemi.android.toolbox.profile.manager.repository.GLSRepository.updateNewRequestStatus +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.lib.utils.logAndReport +import no.nordicsemi.android.toolbox.lib.utils.tryOrLog +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.WriteType +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val GLUCOSE_MEASUREMENT_CHARACTERISTIC = + UUID.fromString("00002A18-0000-1000-8000-00805f9b34fb") +private val GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC = + UUID.fromString("00002A34-0000-1000-8000-00805f9b34fb") +private val GLUCOSE_FEATURE_CHARACTERISTIC = UUID.fromString("00002A51-0000-1000-8000-00805f9b34fb") +private val RACP_CHARACTERISTIC = UUID.fromString("00002A52-0000-1000-8000-00805f9b34fb") + +internal class GLSManager : ServiceManager { + override val profile: Profile = Profile.GLS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics + .firstOrNull { it.uuid == GLUCOSE_MEASUREMENT_CHARACTERISTIC.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { GlucoseMeasurementParser.parse(it) } + ?.onEach { GLSRepository.updateNewRecord(deviceId, it) } + ?.onCompletion { GLSRepository.clear(deviceId) } + ?.catch { it.logAndReport() } + ?.launchIn(scope) + + remoteService.characteristics + .firstOrNull { it.uuid == GLUCOSE_MEASUREMENT_CONTEXT_CHARACTERISTIC.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { GlucoseMeasurementContextParser.parse(it) } + ?.onEach { GLSRepository.updateWithNewContext(deviceId, it) } + ?.onCompletion { GLSRepository.clear(deviceId) } + ?.catch { it.logAndReport() } + ?.launchIn(scope) + + remoteService.characteristics + .firstOrNull { it.uuid == RACP_CHARACTERISTIC.toKotlinUuid() } + ?.apply { recordAccessControlPointCharacteristic = this } + ?.subscribe() + ?.mapNotNull { RecordAccessControlPointParser.parse(it) } + ?.onEach { onAccessControlPointDataReceived(deviceId, it, scope) } + ?.catch { it.logAndReport() } + ?.launchIn(scope) + } + } + + private fun onAccessControlPointDataReceived( + deviceId: String, + data: RecordAccessControlPointData, + scope: CoroutineScope + ) = scope.launch { + when (data) { + is NumberOfRecordsData -> onNumberOfRecordsReceived(deviceId, data.numberOfRecords) + + is ResponseData -> when (data.responseCode) { + RACPResponseCode.RACP_RESPONSE_SUCCESS -> onRecordAccessOperationCompleted( + deviceId, + data.requestCode + ) + + RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED -> + onRecordAccessOperationCompletedWithNoRecordsFound(deviceId) + + else -> onRecordAccessOperationError(deviceId, data.responseCode) + } + } + } + + private fun onRecordAccessOperationError(deviceId: String, responseCode: RACPResponseCode) { + updateNewRequestStatus( + deviceId, + when (responseCode) { + RACPResponseCode.RACP_ERROR_OP_CODE_NOT_SUPPORTED -> RequestStatus.NOT_SUPPORTED + else -> RequestStatus.FAILED + } + ) + } + + private fun onRecordAccessOperationCompleted(deviceId: String, requestCode: RACPOpCode) { + updateNewRequestStatus( + deviceId, + when (requestCode) { + RACPOpCode.RACP_OP_CODE_ABORT_OPERATION -> RequestStatus.ABORTED + else -> RequestStatus.SUCCESS + } + ) + } + + private fun onRecordAccessOperationCompletedWithNoRecordsFound(deviceId: String) { + updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + + private suspend fun onNumberOfRecordsReceived( + deviceId: String, + numberOfRecords: Int, + ) { + val state = GLSRepository.getData(deviceId) + val highestSequenceNumber = state.value + .records + .keys + .maxByOrNull { it.sequenceNumber } + ?.sequenceNumber ?: -1 + + if (numberOfRecords > 0) + tryOrLog { + recordAccessControlPointCharacteristic + .write( + if (state.value.records.isNotEmpty()) { + RecordAccessControlPointInputParser.reportStoredRecordsGreaterThenOrEqualTo( + highestSequenceNumber.toShort() + ) + } else { + RecordAccessControlPointInputParser.reportAllStoredRecords() + }, + WriteType.WITH_RESPONSE + ) + } + updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + + companion object { + private lateinit var recordAccessControlPointCharacteristic: RemoteCharacteristic + + suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) { + writeOrSetStatusFailed(deviceId) { + recordAccessControlPointCharacteristic.write( + when (workingMode) { + WorkingMode.ALL -> RecordAccessControlPointInputParser.reportNumberOfAllStoredRecords() + WorkingMode.LAST -> RecordAccessControlPointInputParser.reportLastStoredRecord() + WorkingMode.FIRST -> RecordAccessControlPointInputParser.reportFirstStoredRecord() + }, + WriteType.WITH_RESPONSE + ) + } + } + + private suspend fun writeOrSetStatusFailed( + deviceId: String, + block: suspend () -> Unit + ) { + try { + block() + + } catch (e: Exception) { + e.printStackTrace() + updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } + } + + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HRSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HRSManager.kt new file mode 100644 index 00000000..c6dc61d1 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HRSManager.kt @@ -0,0 +1,56 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.hrs.BodySensorLocationParser +import no.nordicsemi.android.toolbox.profile.parser.hrs.HRSDataParser +import no.nordicsemi.android.toolbox.profile.manager.repository.HRSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00002A38-0000-1000-8000-00805f9b34fb") +private val HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00002A37-0000-1000-8000-00805f9b34fb") + +internal class HRSManager : ServiceManager { + override val profile: Profile = Profile.HRS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics.firstOrNull { it.uuid == HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { HRSDataParser.parse(it) } + ?.onEach { data -> + HRSRepository.updateHRSData(deviceId, data) + } + ?.onCompletion { HRSRepository.clear(deviceId) } + ?.catch { e -> + // Handle the error + e.printStackTrace() + Timber.e(e) + }?.launchIn(scope) + + remoteService.characteristics.firstOrNull { it.uuid == BODY_SENSOR_LOCATION_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.read() + ?.let { BodySensorLocationParser.parse(it) } + ?.let { bodySensorLocation -> + HRSRepository.updateBodySensorLocation(deviceId, bodySensorLocation) + } + } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HTSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HTSManager.kt new file mode 100644 index 00000000..1d1e8326 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/HTSManager.kt @@ -0,0 +1,45 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.hts.HTSDataParser +import no.nordicsemi.android.toolbox.profile.manager.repository.HTSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val HTS_MEASUREMENT_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00002A1C-0000-1000-8000-00805f9b34fb") + +internal class HTSManager : ServiceManager { + override val profile: Profile = Profile.HTS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics.firstOrNull { it.uuid == HTS_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { HTSDataParser.parse(it) } + ?.onEach { htsData -> + HTSRepository.updateHTSData(deviceId, htsData) + } + ?.onCompletion { HTSRepository.clear(deviceId) } + ?.catch { e -> + Timber.e(e) + }?.launchIn(scope) + } + + } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/LBSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/LBSManager.kt new file mode 100644 index 00000000..4ccb56c7 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/LBSManager.kt @@ -0,0 +1,100 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.toolbox.profile.manager.repository.LBSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.WriteType +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val BLINKY_BUTTON_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00001524-1212-EFDE-1523-785FEABCD123") +private val BLINKY_LED_CHARACTERISTIC_UUID: UUID = + UUID.fromString("00001525-1212-EFDE-1523-785FEABCD123") + +internal class LBSManager : ServiceManager { + override val profile: Profile + get() = Profile.LBS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + // Ensure the characteristic is initialized before writing + ledWriteCharacteristics = remoteService.characteristics.firstOrNull { + it.uuid == BLINKY_LED_CHARACTERISTIC_UUID.toKotlinUuid() + } ?: throw IllegalStateException("LED characteristic not found") + + val blinkyCharacteristics = remoteService.characteristics.firstOrNull { + it.uuid == BLINKY_BUTTON_CHARACTERISTIC_UUID.toKotlinUuid() + } + + // Subscribe to the button state changes. + blinkyCharacteristics?.subscribe() + ?.mapNotNull { ButtonStateParser.parse(it) } + ?.onEach { LBSRepository.updateButtonState(deviceId, it) } + ?.catch { + Timber.e("Error observing button state: ${it.message}") + } + ?.onCompletion { + LBSRepository.clear(deviceId) + }?.launchIn(scope) + + // Read the initial state of the button + try { + blinkyCharacteristics?.read() + ?.let { ButtonStateParser.parse(it) } + ?.let { LBSRepository.updateButtonState(deviceId, it) } + } catch (e: Exception) { + Timber.e("Error reading button state: ${e.message}") + } + } + + companion object { + private lateinit var ledWriteCharacteristics: RemoteCharacteristic + + /** + * Writes the LED state to the Blinky LED characteristic. + * + * @param deviceId The ID of the device to which the LED state should be written. + * @param ledState The desired state of the LED (true for ON, false for OFF). + */ + suspend fun writeToBlinkyLED( + deviceId: String, + ledState: Boolean + ) { + val data = byteArrayOf((0x01.takeIf { ledState } ?: 0x00).toByte()) + + try { + if (::ledWriteCharacteristics.isInitialized) { + ledWriteCharacteristics.write(data, WriteType.WITHOUT_RESPONSE) + } + } catch (e: Exception) { + Timber.e("Error writing to Blinky LED characteristic: ${e.message}") + } finally { + LBSRepository.updateLedState(deviceId, ledState) + } + } + } +} + +object ButtonStateParser { + fun parse(data: ByteArray): Boolean { + return if (data.isNotEmpty()) { + data[0].toInt() == 0x01 + } else { + false + } + } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/RSCSManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/RSCSManager.kt new file mode 100644 index 00000000..e562bbd8 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/RSCSManager.kt @@ -0,0 +1,55 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSDataParser +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSFeatureDataParser +import no.nordicsemi.android.toolbox.profile.manager.repository.RSCSRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.lib.utils.logAndReport +import no.nordicsemi.kotlin.ble.client.RemoteService +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = + UUID.fromString("00002A53-0000-1000-8000-00805F9B34FB") +private val RSC_FEATURE_CHARACTERISTIC_UUID = + UUID.fromString("00002A54-0000-1000-8000-00805F9B34FB") + +internal class RSCSManager : ServiceManager { + override val profile: Profile + get() = Profile.RSCS + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics + .firstOrNull { it.uuid == RSC_MEASUREMENT_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { RSCSDataParser.parse(it) } + ?.onEach { RSCSRepository.onRSCSDataChanged(deviceId, it) } + ?.catch { it.logAndReport() } + ?.onCompletion { RSCSRepository.clear(deviceId) } + ?.launchIn(scope) + + remoteService.characteristics + .firstOrNull { it.uuid == RSC_FEATURE_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.read() + ?.let { + RSCSFeatureDataParser.parse(it) + }?.also { + RSCSRepository.updateRSCSFeatureData(deviceId, it) + } + } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManager.kt new file mode 100644 index 00000000..30af67d5 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManager.kt @@ -0,0 +1,14 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteService + +sealed interface ServiceManager { + val profile: Profile + suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt new file mode 100644 index 00000000..018e388b --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ServiceManagerFactory.kt @@ -0,0 +1,42 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import no.nordicsemi.android.toolbox.lib.utils.spec.BATTERY_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.BPS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.CGMS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.CSC_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.DF_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.GLS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.HRS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.HTS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.LBS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.RSCS_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.THROUGHPUT_SERVICE_UUID +import no.nordicsemi.android.toolbox.lib.utils.spec.UART_SERVICE_UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.Uuid +import kotlin.uuid.toKotlinUuid + +@OptIn(ExperimentalUuidApi::class) +object ServiceManagerFactory { + + private val serviceManagers = mapOf( + BATTERY_SERVICE_UUID to ::BatteryManager, + BPS_SERVICE_UUID to ::BPSManager, + CSC_SERVICE_UUID to ::CSCManager, + CGMS_SERVICE_UUID to ::CGMManager, + DF_SERVICE_UUID to ::DFSManager, + GLS_SERVICE_UUID to ::GLSManager, + HTS_SERVICE_UUID to ::HTSManager, + HRS_SERVICE_UUID to ::HRSManager, + RSCS_SERVICE_UUID to ::RSCSManager, + THROUGHPUT_SERVICE_UUID to ::ThroughputManager, + UART_SERVICE_UUID to ::UARTManager, +// CHANNEL_SOUND_SERVICE_UUID to ::ChannelSoundingManager, + LBS_SERVICE_UUID to ::LBSManager, + // Add more service UUIDs to handler mappings as needed + ).mapKeys { it.key.toKotlinUuid() } + + fun createServiceManager(serviceUuid: Uuid): ServiceManager? { + return serviceManagers[serviceUuid]?.invoke() + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ThroughputManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ThroughputManager.kt new file mode 100644 index 00000000..2946fdad --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/ThroughputManager.kt @@ -0,0 +1,120 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputDataParser +import no.nordicsemi.android.toolbox.profile.manager.repository.ThroughputRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.android.toolbox.profile.data.NumberOfBytes +import no.nordicsemi.android.toolbox.profile.data.NumberOfSeconds +import no.nordicsemi.android.toolbox.profile.data.ThroughputInputType +import no.nordicsemi.android.toolbox.profile.data.WritingStatus +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.WriteType +import no.nordicsemi.kotlin.ble.core.util.chunked +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val THROUGHPUT_CHAR_UUID = UUID.fromString("00001524-0000-1000-8000-00805F9B34FB") + +internal class ThroughputManager : ServiceManager { + override val profile: Profile + get() = Profile.THROUGHPUT + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + try { + remoteService.characteristics + .firstOrNull { it.uuid == THROUGHPUT_CHAR_UUID.toKotlinUuid() } + ?.also { writeCharacteristicProperty = it } + } finally { + ThroughputRepository.clearData(deviceId) + } + } + + companion object { + private lateinit var writeCharacteristicProperty: RemoteCharacteristic + + suspend fun writeRequest( + deviceId: String, + maxWriteValueLength: Int, + inputType: ThroughputInputType, + ) { + try { + ThroughputRepository.updateWriteStatus(deviceId, WritingStatus.IN_PROGRESS) + when (inputType) { + is NumberOfBytes -> { + writeBytesData(maxWriteValueLength, inputType.numberOfBytes) + } + + is NumberOfSeconds -> { + writeTimesData(maxWriteValueLength, inputType.numberOfSeconds) + } + } + } catch (e: Exception) { + Timber.tag("ThroughputService").e("Error ${e.message}") + } finally { + readThroughputMetrics(deviceId) + ThroughputRepository.updateWriteStatus(deviceId, WritingStatus.COMPLETED) + } + } + + private suspend fun writeBytesData( + maxWriteValueLength: Int, + numberOfBytes: Int + ) { + val array = ByteArray(numberOfBytes) { 0x3D } + writeCharacteristicProperty.write( + data = byteArrayOf(0x3D), + writeType = WriteType.WITHOUT_RESPONSE + ) + array.chunked(maxWriteValueLength).map { + writeCharacteristicProperty.write( + data = it, + writeType = WriteType.WITHOUT_RESPONSE + ) + } + } + + private suspend fun writeTimesData( + maxWriteValueLength: Int, + numberOfSecond: Int + ) { + val array = ByteArray(maxWriteValueLength) { 0x3D } + val startTime = System.currentTimeMillis() + writeCharacteristicProperty.write( + data = byteArrayOf(0x3D), + writeType = WriteType.WITHOUT_RESPONSE + ) + while (System.currentTimeMillis() - startTime < numberOfSecond * 1000) { + writeCharacteristicProperty.write( + data = array, + writeType = WriteType.WITHOUT_RESPONSE + ) + } + } + + private suspend fun readThroughputMetrics(deviceId: String) { + try { + // Read data after write operation is complete + val readData = writeCharacteristicProperty.read() + + // Parse the read data + ThroughputDataParser.parse(data = readData)?.let { + ThroughputRepository.updateThroughput(deviceId, it) + } + } catch (e: Exception) { + Timber.tag("ThroughputService").e("Error ${e.message}") + } + } + + } + +} + diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/UARTManager.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/UARTManager.kt new file mode 100644 index 00000000..dcb458a8 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/UARTManager.kt @@ -0,0 +1,89 @@ +package no.nordicsemi.android.toolbox.profile.manager + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext +import no.nordicsemi.android.toolbox.profile.manager.repository.UartRepository +import no.nordicsemi.android.toolbox.lib.utils.Profile +import no.nordicsemi.kotlin.ble.client.RemoteCharacteristic +import no.nordicsemi.kotlin.ble.client.RemoteService +import no.nordicsemi.kotlin.ble.core.CharacteristicProperty +import no.nordicsemi.kotlin.ble.core.WriteType +import no.nordicsemi.kotlin.ble.core.util.chunked +import timber.log.Timber +import java.util.UUID +import kotlin.uuid.ExperimentalUuidApi +import kotlin.uuid.toKotlinUuid + +private val UART_RX_CHARACTERISTIC_UUID: UUID = + UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") +private val UART_TX_CHARACTERISTIC_UUID: UUID = + UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") + +internal class UARTManager : ServiceManager { + override val profile: Profile + get() = Profile.UART + + @OptIn(ExperimentalUuidApi::class) + override suspend fun observeServiceInteractions( + deviceId: String, + remoteService: RemoteService, + scope: CoroutineScope + ) { + withContext(scope.coroutineContext) { + remoteService.characteristics.firstOrNull { it.uuid == UART_TX_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.subscribe() + ?.mapNotNull { String(it) } + ?.onEach { UartRepository.onNewMessageReceived(deviceId, it) } + ?.catch { it.printStackTrace() } + ?.onCompletion { + // Clear the device resources. + UartRepository.clear(deviceId) + } + ?.launchIn(scope) + + val writeCharacteristics = + remoteService.characteristics.firstOrNull { it.uuid == UART_RX_CHARACTERISTIC_UUID.toKotlinUuid() } + ?.also { rxCharacteristic = it } + writeCharacteristics?.properties?.let { + if (it.contains(CharacteristicProperty.WRITE_WITHOUT_RESPONSE)) { + rxCharacteristicWriteType = WriteType.WITHOUT_RESPONSE + } else if (it.contains(CharacteristicProperty.WRITE)) { + rxCharacteristicWriteType = WriteType.WITH_RESPONSE + } + } + } + } + + companion object { + private lateinit var rxCharacteristic: RemoteCharacteristic + private var rxCharacteristicWriteType: WriteType? = null + + suspend fun sendText( + device: String, + message: String, + maxWriteLength: Int, + macroEolUnicodeMessage: String? = null, + ) { + val messageBytes = message.toByteArray() + try { + if (rxCharacteristicWriteType == null) { + Timber.e("Write type not set.") + // Todo: Handle this case. + } else { + messageBytes.chunked(maxWriteLength).forEach { + rxCharacteristic.write(it, rxCharacteristicWriteType!!) + } + } + } catch (e: Exception) { + Timber.tag("UARTService").e("Error ${e.message}") + } finally { + UartRepository.onNewMessageSent(device, macroEolUnicodeMessage ?: message) + } + } + } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BPSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BPSRepository.kt new file mode 100644 index 00000000..a8d81442 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BPSRepository.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureFeatureData +import no.nordicsemi.android.toolbox.profile.parser.bps.BloodPressureMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.bps.IntermediateCuffPressureData +import no.nordicsemi.android.toolbox.profile.data.BPSServiceData + +object BPSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(BPSServiceData()) } + } + + fun updateBPSData(deviceId: String, bpsData: BloodPressureMeasurementData) { + _dataMap[deviceId]?.update { it.copy(bloodPressureMeasurement = bpsData) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun updateICPData(deviceId: String, icpData: IntermediateCuffPressureData) { + _dataMap[deviceId]?.update { it.copy(intermediateCuffPressure = icpData) } + } + + fun updateBPSFeatureData(deviceId: String, bpsFeatureData: BloodPressureFeatureData) { + _dataMap[deviceId]?.update { it.copy(bloodPressureFeature = bpsFeatureData) } + } + +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BatteryRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BatteryRepository.kt new file mode 100644 index 00000000..026b42a0 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/BatteryRepository.kt @@ -0,0 +1,23 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.data.BatteryServiceData + +object BatteryRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(BatteryServiceData()) } + } + + fun updateBatteryLevel(deviceId: String, data: Int) { + _dataMap[deviceId]?.update { it.copy(batteryLevel = data) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CGMRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CGMRepository.kt new file mode 100644 index 00000000..4501ecfe --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CGMRepository.kt @@ -0,0 +1,50 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.data.CGMRecordWithSequenceNumber +import no.nordicsemi.android.toolbox.profile.data.CGMServiceData +import no.nordicsemi.android.toolbox.profile.manager.CGMManager + +object CGMRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): StateFlow = + _dataMap.getOrPut(deviceId) { MutableStateFlow(CGMServiceData()) } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun onMeasurementDataReceived(deviceId: String, data: List) { + _dataMap[deviceId]?.update { + it.copy( + records = it.records + data + + ) + } + } + + fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) { + _dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) } + } + + private fun clearState(deviceId: String) { + _dataMap[deviceId]?.update { + it.copy( + records = emptyList(), + ) + } + } + + suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) { + clearState(deviceId) + updateNewRequestStatus(deviceId, RequestStatus.PENDING) + _dataMap[deviceId]?.update { it.copy(workingMode = workingMode) } + CGMManager.requestRecord(deviceId, workingMode) + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CSCRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CSCRepository.kt new file mode 100644 index 00000000..590e2901 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/CSCRepository.kt @@ -0,0 +1,40 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.data.CSCServiceData +import no.nordicsemi.android.toolbox.profile.parser.csc.CSCData +import no.nordicsemi.android.toolbox.profile.parser.csc.SpeedUnit +import no.nordicsemi.android.toolbox.profile.parser.csc.WheelSize + +object CSCRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): StateFlow = _dataMap.getOrPut(deviceId) { + MutableStateFlow(CSCServiceData()) + } + + fun onCSCDataChanged(deviceId: String, cscData: CSCData) { + _dataMap[deviceId]?.update { it.copy(data = cscData) } + } + + fun setWheelSize(deviceId: String, wheelSize: WheelSize) { + _dataMap[deviceId]?.update { currentValue -> + currentValue.copy( + data = CSCData( + wheelSize = wheelSize + ) + ) + } + } + + fun setSpeedUnit(deviceId: String, speedUnit: SpeedUnit) { + _dataMap[deviceId]?.update { it.copy(speedUnit = speedUnit) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ChannelSoundingRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ChannelSoundingRepository.kt new file mode 100644 index 00000000..85f94a9a --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ChannelSoundingRepository.kt @@ -0,0 +1,11 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import no.nordicsemi.android.toolbox.profile.data.ChannelSoundingServiceData + +object ChannelSoundingRepository { + private val dataMap = mutableMapOf>() + + fun getData(deviceId: String): MutableStateFlow = + dataMap.getOrPut(deviceId) { MutableStateFlow(ChannelSoundingServiceData()) } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFSRepository.kt new file mode 100644 index 00000000..9845a279 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/DFSRepository.kt @@ -0,0 +1,216 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.PeripheralBluetoothAddress +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.azimuthal.AzimuthMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointChangeModeError +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointChangeModeSuccess +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointCheckModeError +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointCheckModeSuccess +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.controlPoint.ControlPointResult +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.ddf.DDFData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.DistanceMode +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.McpdMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.distance.RttMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.elevation.ElevationMeasurementData +import no.nordicsemi.android.toolbox.profile.parser.directionFinder.toDistanceMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.data.DFSServiceData +import no.nordicsemi.android.toolbox.profile.data.SensorData +import no.nordicsemi.android.toolbox.profile.data.SensorValue +import no.nordicsemi.android.toolbox.profile.data.directionFinder.MeasurementSection +import no.nordicsemi.android.toolbox.profile.data.directionFinder.Range +import no.nordicsemi.android.toolbox.profile.manager.DFSManager + +object DFSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): StateFlow = _dataMap.getOrPut(deviceId) { + MutableStateFlow(DFSServiceData()) + } + + fun updateSelectedDevice(deviceId: String, device: PeripheralBluetoothAddress) { + _dataMap[deviceId]?.update { it.copy(selectedDevice = device) } + } + + fun addNewAzimuth(deviceId: String, azimuth: AzimuthMeasurementData) { + _dataMap[deviceId]?.update { current -> + val validatedAzimuth = azimuth.copy(azimuth = azimuth.azimuth.coerceIn(0, 359)) + val key = validatedAzimuth.address + val sensorData = current.data[key] ?: SensorData() + val azimuths = sensorData.azimuth ?: SensorValue() + val newAzimuths = azimuths.copyWithNewValue(validatedAzimuth) + val newSensorData = sensorData.copy(azimuth = newAzimuths) + val newDevicesData = current.data.toMutableMap().apply { + put(key, newSensorData) + }.toMap() + current.copy(data = newDevicesData) + } + } + + fun addNewDistance(deviceId: String, distance: DistanceMeasurementData) { + when (distance) { + is McpdMeasurementData -> addDistance(deviceId, distance, DistanceMode.MCPD) + is RttMeasurementData -> addDistance(deviceId, distance, DistanceMode.RTT) + } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + private fun addDistance( + deviceId: String, + distance: DistanceMeasurementData, + distanceMode: DistanceMode, + ) { + _dataMap[deviceId]?.update { current -> + val key = distance.address + val sensorData = current.data[key] ?: SensorData() + val newSensorData = when (distanceMode) { + DistanceMode.MCPD -> sensorData.copy( + mcpdDistance = sensorData.mcpdDistance + ?.copyWithNewValue(distance as McpdMeasurementData) ?: SensorValue(), + distanceMode = distanceMode + ) + + DistanceMode.RTT -> sensorData.copy( + rttDistance = sensorData.rttDistance + ?.copyWithNewValue(distance as RttMeasurementData) ?: SensorValue(), + distanceMode = distanceMode + ) + } + val newDevicesData = current.data.toMutableMap().apply { + put(key, newSensorData) + }.toMap() + current.copy(data = newDevicesData) + } + } + + fun addNewElevation(deviceId: String, elevation: ElevationMeasurementData) { + _dataMap[deviceId]?.update { current -> + val validatedElevation = + elevation.copy(elevation = elevation.elevation.coerceIn(-90, 90)) + val key = validatedElevation.address + val sensorData = current.data[key] ?: SensorData() + val elevations = sensorData.elevation ?: SensorValue() + val newElevation = elevations.copyWithNewValue(validatedElevation) + val newSensorData = sensorData.copy(elevation = newElevation) + val newDevicesData = current.data.toMutableMap().apply { + put(key, newSensorData) + }.toMap() + current.copy(data = newDevicesData) + + } + } + + fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) { + _dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) } + } + + suspend fun enableDistanceMode(deviceId: String, distanceMode: DistanceMode) { + _dataMap[deviceId]?.update { it.copy(requestStatus = RequestStatus.PENDING) } + DFSManager.enableDistanceMode(deviceId, distanceMode) + } + + private fun setDistanceMode(deviceId: String, distanceMode: DistanceMode) { + _dataMap[deviceId]?.update { serviceData -> + serviceData.copy( + data = serviceData.data.mapValues { (key, sensorData) -> + if (key == serviceData.selectedDevice) { + sensorData.copy(distanceMode = distanceMode) + } else { + sensorData + } + } + ) + } + } + + fun setAvailableDistanceModes(deviceId: String, ddfData: DDFData) { + updateNewRequestStatus(deviceId, RequestStatus.PENDING) + _dataMap[deviceId]?.update { + it.copy( + ddfFeature = DDFData( + isMcpdAvailable = ddfData.isMcpdAvailable, + isRttAvailable = ddfData.isRttAvailable + ) + ) + } + updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + + fun onControlPointDataReceived( + deviceId: String, + data: ControlPointResult, + scope: CoroutineScope + ) { + when (data) { + ControlPointChangeModeError -> { + scope.launch { + checkCurrentDistanceMode(deviceId) + updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } + } + + is ControlPointChangeModeSuccess -> { + scope.launch { + setDistanceMode(deviceId, data.mode.toDistanceMode()) + updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + } + + ControlPointCheckModeError -> { + scope.launch { + checkCurrentDistanceMode(deviceId) + updateNewRequestStatus(deviceId, RequestStatus.FAILED) + } + } + + is ControlPointCheckModeSuccess -> { + scope.launch { + setDistanceMode(deviceId, data.mode.toDistanceMode()) + updateNewRequestStatus(deviceId, RequestStatus.SUCCESS) + } + } + } + } + + + suspend fun checkCurrentDistanceMode(deviceId: String) { + updateNewRequestStatus(deviceId, RequestStatus.PENDING) + DFSManager.checkForCurrentDistanceMode(deviceId) + } + + + fun updateDistanceRange(deviceId: String, range: Range) { + _dataMap[deviceId]?.update { it.copy(distanceRange = range) } + } + + /** + * Update section to the sensor data. + */ + fun updateDetailsSection(deviceId: String, section: MeasurementSection) { + _dataMap[deviceId]?.update { serviceData -> + serviceData.copy( + data = serviceData.data.mapValues { (key, sensorData) -> + if (key == serviceData.selectedDevice) { + sensorData.copy(selectedMeasurementSection = section) + } else { + sensorData + } + } + ) + } + } + + suspend fun checkAvailableFeatures(deviceId: String) { + DFSManager.checkAvailableFeatures(deviceId) + } + +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/GLSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/GLSRepository.kt new file mode 100644 index 00000000..da6715ee --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/GLSRepository.kt @@ -0,0 +1,72 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.common.WorkingMode +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSMeasurementContext +import no.nordicsemi.android.toolbox.profile.parser.gls.data.GLSRecord +import no.nordicsemi.android.toolbox.profile.parser.gls.data.RequestStatus +import no.nordicsemi.android.toolbox.profile.data.GLSServiceData +import no.nordicsemi.android.toolbox.profile.manager.GLSManager + +object GLSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): StateFlow = _dataMap.getOrPut(deviceId) { + MutableStateFlow(GLSServiceData()) + } + + fun updateNewRecord(deviceId: String, record: GLSRecord) { + val records = _dataMap[deviceId]?.value?.records?.toMutableMap() + records?.set(record, null) + if (records != null) { + _dataMap[deviceId]?.update { + it.copy( + records = records.toMap() + ) + } + } + } + + fun updateWithNewContext(deviceId: String, context: GLSMeasurementContext) { + val records = _dataMap[deviceId]?.value?.records?.toMutableMap() + records?.keys?.firstOrNull { it.sequenceNumber == context.sequenceNumber }?.let { + records[it] = context + } + if (records != null) { + _dataMap[deviceId]?.update { + it.copy( + records = records.toMap() + ) + } + } + + } + + suspend fun requestRecord(deviceId: String, workingMode: WorkingMode) { + clearState(deviceId) + updateNewRequestStatus(deviceId, RequestStatus.PENDING) + _dataMap[deviceId]?.update { it.copy(workingMode = workingMode) } + GLSManager.requestRecord(deviceId, workingMode) + } + + fun updateNewRequestStatus(deviceId: String, requestStatus: RequestStatus) { + _dataMap[deviceId]?.update { it.copy(requestStatus = requestStatus) } + } + + private fun clearState(deviceId: String) { + _dataMap[deviceId]?.update { + it.copy( + records = mapOf(), + requestStatus = RequestStatus.IDLE + ) + } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + +} + diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HRSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HRSRepository.kt new file mode 100644 index 00000000..eb9d752d --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HRSRepository.kt @@ -0,0 +1,36 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.hrs.HRSData +import no.nordicsemi.android.toolbox.profile.data.HRSServiceData + +object HRSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(HRSServiceData()) } + } + + fun updateHRSData(deviceId: String, data: HRSData) { + _dataMap[deviceId]?.update { + it.copy( + heartRate = data.heartRate, + data = it.data + data + ) + } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun updateBodySensorLocation(deviceId: String, location: Int) { + _dataMap[deviceId]?.update { it.copy(bodySensorLocation = location) } + } + + fun updateZoomIn(deviceId: String) { + _dataMap[deviceId]?.update { it.copy(zoomIn = !it.zoomIn) } + } +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HTSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HTSRepository.kt new file mode 100644 index 00000000..f7342ae4 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/HTSRepository.kt @@ -0,0 +1,29 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.hts.HTSData +import no.nordicsemi.android.toolbox.profile.data.uiMapper.TemperatureUnit +import no.nordicsemi.android.toolbox.profile.data.HTSServiceData + +object HTSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(HTSServiceData()) } + } + + fun updateHTSData(deviceId: String, data: HTSData) { + _dataMap[deviceId]?.update { it.copy(data = data) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun onTemperatureUnitChange(deviceId: String, unit: TemperatureUnit) { + _dataMap[deviceId]?.update { it.copy(temperatureUnit = unit) } + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/LBSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/LBSRepository.kt new file mode 100644 index 00000000..9d61e63b --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/LBSRepository.kt @@ -0,0 +1,55 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.data.LBSServiceData +import no.nordicsemi.android.toolbox.profile.manager.LBSManager + +data object LBSRepository { + private val _dataMap = mutableMapOf>() + + /** + * Returns a [MutableStateFlow] that holds the [LBSServiceData] for the given device ID. + * If no data exists for the device ID, it initializes a new [MutableStateFlow] with an empty [LBSServiceData]. + */ + fun getData(deviceId: String): Flow = + _dataMap.getOrPut(deviceId) { MutableStateFlow(LBSServiceData()) } + + /** + * Updates the LED state for the given device ID. + * If the device ID does not exist, it will not perform any action. + */ + fun updateLedState(deviceId: String, ledState: Boolean) { + _dataMap[deviceId]?.update { + it.copy(data = it.data.copy(ledState = ledState)) + } + } + + /** + * Updates the button state for the given device ID. + * If the device ID does not exist, it will not perform any action. + */ + fun updateButtonState(deviceId: String, buttonState: Boolean) { + _dataMap[deviceId]?.update { + it.copy( + data = it.data.copy( + buttonState = buttonState + ) + ) + } + } + + /** + * Clears the data for the given device ID. + * This will remove the [MutableStateFlow] associated with the device ID from the repository. + */ + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + suspend fun writeToBlinkyLED(address: String, ledState: Boolean) { + // Update the LED state for the given device address + LBSManager.writeToBlinkyLED(deviceId = address, ledState) + } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/PRXRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/PRXRepository.kt new file mode 100644 index 00000000..a4cda7ac --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/PRXRepository.kt @@ -0,0 +1,27 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.prx.AlarmLevel +import no.nordicsemi.android.toolbox.profile.parser.prx.PRXData + +object PRXRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(PRXData()) } + } + + fun updatePRXData(deviceId: String, alarmLevel: AlarmLevel) { + _dataMap[deviceId]?.update { it.copy(localAlarmLevel = alarmLevel) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun updateLinkLossAlarmLevelData(deviceId: String, linkLossAlarmLevel: AlarmLevel) { + _dataMap[deviceId]?.update { it.copy(linkLossAlarmLevel = linkLossAlarmLevel) } + } +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/RSCSRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/RSCSRepository.kt new file mode 100644 index 00000000..79dd51f8 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/RSCSRepository.kt @@ -0,0 +1,34 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCFeatureData +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSData +import no.nordicsemi.android.toolbox.profile.parser.rscs.RSCSSettingsUnit +import no.nordicsemi.android.toolbox.profile.data.RSCSServiceData + +object RSCSRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(RSCSServiceData()) } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun onRSCSDataChanged(deviceId: String, data: RSCSData) { + _dataMap[deviceId]?.update { it.copy(data = data) } + } + + fun updateUnitSettings(deviceId: String, rscsUnitSettings: RSCSSettingsUnit) { + _dataMap[deviceId]?.update { it.copy(unit = rscsUnitSettings) } + } + + fun updateRSCSFeatureData(deviceId: String, feature: RSCFeatureData) { + _dataMap[deviceId]?.update { it.copy(feature = feature) } + } + +} diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ThroughputRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ThroughputRepository.kt new file mode 100644 index 00000000..7d8769cb --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/ThroughputRepository.kt @@ -0,0 +1,48 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.parser.throughput.ThroughputMetrics +import no.nordicsemi.android.toolbox.profile.data.ThroughputInputType +import no.nordicsemi.android.toolbox.profile.data.ThroughputServiceData +import no.nordicsemi.android.toolbox.profile.data.WritingStatus +import no.nordicsemi.android.toolbox.profile.manager.ThroughputManager + +object ThroughputRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): StateFlow = + _dataMap.getOrPut(deviceId) { MutableStateFlow(ThroughputServiceData()) } + + fun updateThroughput(deviceId: String, throughputMetrics: ThroughputMetrics) { + _dataMap[deviceId]?.update { + it.copy(throughputData = throughputMetrics) + } + } + + suspend fun sendDataToDK( + deviceId: String, + writeDataType: ThroughputInputType, + ) { + val maxWriteValueLength = _dataMap[deviceId]?.value?.maxWriteValueLength ?: 20 + ThroughputManager.writeRequest( + deviceId = deviceId, + maxWriteValueLength = maxWriteValueLength, + inputType = writeDataType, + ) + } + + fun updateWriteStatus(deviceId: String, status: WritingStatus) { + _dataMap[deviceId]?.update { it.copy(writingStatus = status) } + } + + fun updateMaxWriteValueLength(deviceId: String, mtuSize: Int?) { + _dataMap[deviceId]?.update { it.copy(maxWriteValueLength = mtuSize) } + } + + fun clearData(deviceId: String) { + _dataMap.remove(deviceId) + } + +} \ No newline at end of file diff --git a/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/UartRepository.kt b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/UartRepository.kt new file mode 100644 index 00000000..d235fbd0 --- /dev/null +++ b/profile_manager/src/main/java/no/nordicsemi/android/toolbox/profile/manager/repository/UartRepository.kt @@ -0,0 +1,180 @@ +package no.nordicsemi.android.toolbox.profile.manager.repository + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import no.nordicsemi.android.toolbox.profile.data.UARTRecord +import no.nordicsemi.android.toolbox.profile.data.UARTRecordType +import no.nordicsemi.android.toolbox.profile.data.UARTServiceData +import no.nordicsemi.android.toolbox.profile.data.uart.MacroEol +import no.nordicsemi.android.toolbox.profile.data.uart.UARTConfiguration +import no.nordicsemi.android.toolbox.profile.data.uart.UARTMacro +import no.nordicsemi.android.toolbox.profile.data.uart.parseWithNewLineChar +import no.nordicsemi.android.toolbox.profile.data.uart.toMacroEolUnicodeCharDisplay +import no.nordicsemi.android.toolbox.profile.manager.UARTManager + +object UartRepository { + private val _dataMap = mutableMapOf>() + + fun getData(deviceId: String): Flow { + return _dataMap.getOrPut(deviceId) { MutableStateFlow(UARTServiceData()) } + } + + fun updateMaxWriteLength(deviceId: String, maxWriteLength: Int) { + _dataMap[deviceId]?.update { + it.copy(maxWriteLength = maxWriteLength) + } + } + + fun onNewMessageReceived(deviceId: String, message: String) { + _dataMap[deviceId]?.update { + it.copy(messages = it.messages + UARTRecord(message, UARTRecordType.OUTPUT)) + } + } + + private fun getMaxWriteLength(deviceId: String): Int { + return _dataMap[deviceId]?.value?.maxWriteLength ?: 20 + } + + fun onNewMessageSent(deviceId: String, message: String) { + _dataMap[deviceId]?.update { + it.copy(messages = it.messages + UARTRecord(message, UARTRecordType.INPUT)) + } + } + + suspend fun sendText(deviceId: String, text: String, newLineChar: MacroEol) { + if (_dataMap.containsKey(deviceId)) { + UARTManager.sendText( + deviceId, + text.parseWithNewLineChar(newLineChar), + getMaxWriteLength(deviceId) + ) + } + } + + suspend fun runMacro(deviceId: String, macro: UARTMacro) { + if (macro.command == null) return + // Send the command to the device and update the command message. + if (_dataMap.containsKey(deviceId)) { + UARTManager.sendText( + deviceId, + macro.command!!.parseWithNewLineChar(macro.newLineChar), + getMaxWriteLength(deviceId), + macro.command!!.toMacroEolUnicodeCharDisplay(macro.newLineChar) + ) + } + } + + fun clear(deviceId: String) { + _dataMap.remove(deviceId) + } + + fun clearOutputItems(deviceId: String) { + _dataMap[deviceId]?.update { + it.copy(messages = emptyList()) + } + } + + fun deleteConfiguration(deviceId: String, configuration: UARTConfiguration) { + _dataMap[deviceId]?.update { + it.copy(uartViewState = it.uartViewState.copy(configurations = it.uartViewState.configurations - configuration)) + } + } + + fun addConfiguration(address: String, configuration: UARTConfiguration) { + // Add the new configuration to the list + _dataMap[address]?.update { + val newConfig = configuration.copy(id = it.uartViewState.configurations.size + 1) + it.copy(uartViewState = it.uartViewState.copy(configurations = it.uartViewState.configurations + newConfig)) + } + } + + fun updateSelectedConfigurationName(address: String, configurationName: String) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(selectedConfigurationName = configurationName)) + } + } + + fun loadPreviousConfigurations(address: String, configuration: List) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(configurations = configuration)) + } + } + + fun removeSelectedConfiguration(address: String) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(selectedConfigurationName = null)) + } + } + + fun onEditConfiguration(address: String) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(isConfigurationEdited = !it.uartViewState.isConfigurationEdited)) + } + } + + fun onEditMacro(address: String, editPosition: Int?) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(editedPosition = editPosition)) + } + } + + fun addOrEditMacro(address: String, macro: UARTMacro): UARTConfiguration? { + var newConfig: UARTConfiguration? = null + _dataMap[address]?.update { + it.uartViewState.selectedConfiguration?.let { selectedConfiguration -> + val macros = selectedConfiguration.macros.toMutableList().apply { + set(it.uartViewState.editedPosition!!, macro) + } + newConfig = selectedConfiguration.copy(macros = macros) + // Save the new configuration and edited position. + val newConfiguration = it.uartViewState.configurations.map { config -> + if (config.id == selectedConfiguration.id) { + newConfig!! + } else { + config + } + } + it.copy( + uartViewState = it.uartViewState.copy( + configurations = newConfiguration, + editedPosition = null + ) + ) + }!! + } + return newConfig + } + + fun onEditFinished(address: String) { + _dataMap[address]?.update { + it.copy(uartViewState = it.uartViewState.copy(editedPosition = null)) + } + } + + fun onDeleteMacro(address: String) { + _dataMap[address]?.update { + it.uartViewState.selectedConfiguration?.let { selectedConfiguration -> + val macros = selectedConfiguration.macros.toMutableList().apply { + set(it.uartViewState.editedPosition!!, null) + } + val newConfig = selectedConfiguration.copy(macros = macros) + + // Save the new configuration and edited position. + val newConfiguration = it.uartViewState.configurations.map { config -> + if (config.id == selectedConfiguration.id) { + newConfig + } else { + config + } + } + it.copy( + uartViewState = it.uartViewState.copy( + configurations = newConfiguration, + editedPosition = null + ) + ) + }!! + } + } +} \ No newline at end of file diff --git a/profile_prx/build.gradle.kts b/profile_prx/build.gradle.kts deleted file mode 100644 index 20af0167..00000000 --- a/profile_prx/build.gradle.kts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.prx" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.server) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_prx/module-rules.pro b/profile_prx/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_prx/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_prx/src/main/AndroidManifest.xml b/profile_prx/src/main/AndroidManifest.xml deleted file mode 100644 index d2807698..00000000 --- a/profile_prx/src/main/AndroidManifest.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt deleted file mode 100644 index 7787532f..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/data/PRXServiceData.kt +++ /dev/null @@ -1,25 +0,0 @@ -package no.nordicsemi.android.prx.data - -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel - -data class PRXServiceData( - val localAlarmLevel: AlarmLevel = AlarmLevel.NONE, - val linkLossAlarmLevel: AlarmLevel = AlarmLevel.HIGH, - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val connectionStatus: BleGattConnectionStatus? = null, - val isRemoteAlarm: Boolean = false, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } - - val isLinkLossDisconnected = connectionStatus?.isLinkLoss ?: false -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt deleted file mode 100644 index 4df1f0ac..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXRepository.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.prx.data.PRXServiceData -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PRXRepository @Inject internal constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(PRXServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - private val _remoteAlarmLevel = simpleSharedFlow() - internal val remoteAlarmLevel = _remoteAlarmLevel.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "PRX", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(PRXService::class.java, device) - } - - fun onConnectionStateChanged(connection: GattConnectionStateWithStatus) { - _data.value = _data.value.copy(connectionState = connection) - } - - fun setLocalAlarmLevel(alarmLevel: AlarmLevel) { - _data.value = _data.value.copy(localAlarmLevel = alarmLevel) - } - - fun setLinkLossAlarmLevel(alarmLevel: AlarmLevel) { - _data.value = _data.value.copy(linkLossAlarmLevel = alarmLevel) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun setRemoteAlarmLevel(alarmLevel: AlarmLevel) { - _remoteAlarmLevel.tryEmit(alarmLevel) - } - - fun onRemoteAlarmLevelSet(alarmLevel: AlarmLevel) { - _data.value = _data.value.copy(isRemoteAlarm = alarmLevel != AlarmLevel.NONE) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun disconnect() { - _remoteAlarmLevel.tryEmit(AlarmLevel.NONE) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = PRXServiceData() - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt deleted file mode 100644 index e7c2723e..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/repository/PRXService.kt +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectOptions -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission -import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty -import no.nordicsemi.android.kotlin.ble.core.data.BleWriteType -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevelParser -import no.nordicsemi.android.kotlin.ble.profile.prx.AlertLevelInputParser -import no.nordicsemi.android.kotlin.ble.server.main.ServerBleGatt -import no.nordicsemi.android.kotlin.ble.server.main.ServerConnectionEvent -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristicConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceType -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBluetoothGattConnection -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import no.nordicsemi.android.utils.tryOrLog -import java.util.* -import javax.inject.Inject - -val PRX_SERVICE_UUID = UUID.fromString("00001802-0000-1000-8000-00805f9b34fb") -private val LINK_LOSS_SERVICE_UUID = UUID.fromString("00001803-0000-1000-8000-00805f9b34fb") - -private val ALERT_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A06-0000-1000-8000-00805f9b34fb") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class PRXService : NotificationService() { - - @Inject - lateinit var repository: PRXRepository - - private var client: ClientBleGatt? = null - private var server: ServerBleGatt? = null - - private var alertLevelCharacteristic: ClientBleGattCharacteristic? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startServer(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startServer(device: ServerDevice) = lifecycleScope.launch { - val alertLevelCharacteristic = ServerBleGattCharacteristicConfig( - uuid = ALERT_LEVEL_CHARACTERISTIC_UUID, - properties = listOf(BleGattProperty.PROPERTY_WRITE_NO_RESPONSE), - permissions = listOf(BleGattPermission.PERMISSION_WRITE) - ) - val prxServiceConfig = ServerBleGattServiceConfig( - uuid = PRX_SERVICE_UUID, - type = ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - characteristicConfigs = listOf(alertLevelCharacteristic) - ) - - val linkLossCharacteristic = ServerBleGattCharacteristicConfig( - uuid = ALERT_LEVEL_CHARACTERISTIC_UUID, - properties = listOf(BleGattProperty.PROPERTY_WRITE, BleGattProperty.PROPERTY_READ), - permissions = listOf(BleGattPermission.PERMISSION_WRITE, BleGattPermission.PERMISSION_READ), - initialValue = AlertLevelInputParser.parse(AlarmLevel.HIGH) - ) - - val linkLossServiceConfig = ServerBleGattServiceConfig( - uuid = LINK_LOSS_SERVICE_UUID, - type =ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - characteristicConfigs = listOf(linkLossCharacteristic) - ) - - val server = ServerBleGatt.create(this@PRXService, lifecycleScope, prxServiceConfig, linkLossServiceConfig) - this@PRXService.server = server - - //Order is important. We don't want to connect before services have been added to the server. - startGattClient(device) - - server.connectionEvents - .mapNotNull { (it as? ServerConnectionEvent.DeviceConnected)?.connection } - .onEach { setUpServerConnection(it) } - .launchIn(lifecycleScope) - } - - private fun setUpServerConnection(connection: ServerBluetoothGattConnection) { - val prxService = connection.services.findService(PRX_SERVICE_UUID)!! - val linkLossService = connection.services.findService(LINK_LOSS_SERVICE_UUID)!! - - val prxCharacteristic = prxService.findCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)!! - val linkLossCharacteristic = linkLossService.findCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)!! - - prxCharacteristic.value - .mapNotNull { AlarmLevelParser.parse(it) } - .onEach { repository.setLocalAlarmLevel(it) } - .launchIn(lifecycleScope) - - linkLossCharacteristic.value - .mapNotNull { AlarmLevelParser.parse(it) } - .onEach { repository.setLinkLossAlarmLevel(it) } - .launchIn(lifecycleScope) - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect( - this@PRXService, - device, - lifecycleScope, - options = BleGattConnectOptions(autoConnect = true) - ) - this@PRXService.client = client - - client.waitForBonding() - - client.connectionStateWithStatus - .filterNotNull() - .onEach { repository.onConnectionStateChanged(it) } - .onEach { stopIfDisconnected(it.state, it.status) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - - repository.remoteAlarmLevel - .onEach { writeAlertLevel(it) } - .launchIn(lifecycleScope) - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val prxService = services.findService(PRX_SERVICE_UUID)!! - alertLevelCharacteristic = prxService.findCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)!! - val linkLossService = services.findService(LINK_LOSS_SERVICE_UUID)!! - val linkLossCharacteristic = linkLossService.findCharacteristic(ALERT_LEVEL_CHARACTERISTIC_UUID)!! - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - - tryOrLog { - linkLossCharacteristic.write(AlertLevelInputParser.parse(AlarmLevel.HIGH)) - } - } - - private suspend fun writeAlertLevel(alarmLevel: AlarmLevel) { - try { - alertLevelCharacteristic?.run { - write(AlertLevelInputParser.parse(alarmLevel), BleWriteType.NO_RESPONSE) - repository.onRemoteAlarmLevelSet(alarmLevel) - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - private fun stopIfDisconnected(connectionState: GattConnectionState, connectionStatus: BleGattConnectionStatus) { - if (connectionState == GattConnectionState.STATE_DISCONNECTED && !connectionStatus.isLinkLoss) { - server?.stopServer() - repository.disconnect() - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - server?.stopServer() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt deleted file mode 100644 index 1f2a13f8..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXContentView.kt +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.prx.R -import no.nordicsemi.android.prx.data.PRXServiceData -import no.nordicsemi.android.ui.view.BatteryLevelView -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun ContentView(state: PRXServiceData, onEvent: (PRXScreenViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - SettingsSection(state, onEvent) - - Spacer(modifier = Modifier.height(16.dp)) - - RecordsSection(state) - - Spacer(modifier = Modifier.height(16.dp)) - - state.batteryLevel?.let { - BatteryLevelView(it) - - Spacer(modifier = Modifier.height(16.dp)) - } - - Button( - onClick = { onEvent(DisconnectEvent) } - ) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} - -@Composable -private fun SettingsSection(state: PRXServiceData, onEvent: (PRXScreenViewEvent) -> Unit) { - ScreenSection { - SectionTitle(icon = Icons.Default.Settings, title = stringResource(R.string.prx_settings)) - - Spacer(modifier = Modifier.height(16.dp)) - - if (state.isRemoteAlarm) { - TurnAlarmOffButton(onEvent) - } else { - TurnAlarmOnButton(onEvent) - } - } -} - -@Composable -private fun TurnAlarmOnButton(onEvent: (PRXScreenViewEvent) -> Unit) { - Button( - onClick = { onEvent(TurnOnAlert) } - ) { - Text(text = stringResource(id = R.string.prx_find_me)) - } -} - -@Composable -private fun TurnAlarmOffButton(onEvent: (PRXScreenViewEvent) -> Unit) { - Button( - onClick = { onEvent(TurnOffAlert) } - ) { - Text(text = stringResource(id = R.string.prx_silent_me)) - } -} - -@Composable -private fun RecordsSection(state: PRXServiceData) { - ScreenSection { - SectionTitle(resId = R.drawable.ic_records, title = stringResource(id = R.string.prx_records)) - - Spacer(modifier = Modifier.height(16.dp)) - - Column { - KeyValueField( - stringResource(id = R.string.prx_is_remote_alarm), - state.isRemoteAlarm.toDisplayString() - ) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField( - stringResource(id = R.string.prx_local_alarm_level), - state.localAlarmLevel.toDisplayString().uppercase() - ) - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXLinkLossView.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXLinkLossView.kt deleted file mode 100644 index 741e7dde..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXLinkLossView.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.view - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.HighlightOff -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.prx.R -import no.nordicsemi.android.ui.view.ScreenSection - -@Composable -fun DeviceOutOfRangeView(navigateUp: () -> Unit) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - ScreenSection { - Icon( - imageVector = Icons.Default.HighlightOff, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSecondary, - modifier = Modifier - .background( - color = MaterialTheme.colorScheme.secondary, - shape = CircleShape - ) - .padding(8.dp) - ) - - Spacer(modifier = Modifier.size(16.dp)) - - Text( - text = stringResource(id = R.string.prx_device_out_of_range), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(modifier = Modifier.size(16.dp)) - - Text( - text = stringResource(id = R.string.prx_device_out_of_range_reason), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium - ) - } - - Spacer(modifier = Modifier.size(16.dp)) - - Button(onClick = { navigateUp() }) { - Text(text = stringResource(id = R.string.disconnect)) - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXMapper.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXMapper.kt deleted file mode 100644 index 4f9bf5d2..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXMapper.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.view - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel -import no.nordicsemi.android.prx.R - -@Composable -internal fun Boolean.toDisplayString(): String { - return when (this) { - true -> stringResource(id = R.string.prx_alarm_on) - false -> stringResource(id = R.string.prx_alarm_off) - }.uppercase() -} - -@Composable -internal fun AlarmLevel.toDisplayString(): String { - return when (this) { - AlarmLevel.NONE -> stringResource(id = R.string.prx_alarm_level_none) - AlarmLevel.MEDIUM -> stringResource(id = R.string.prx_alarm_level_medium) - AlarmLevel.HIGH -> stringResource(id = R.string.prx_alarm_level_height) - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt deleted file mode 100644 index 3f511756..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreen.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.prx.R -import no.nordicsemi.android.prx.viewmodel.PRXViewModel -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun PRXScreen() { - val viewModel: PRXViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUpEvent) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.prx_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> ContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt deleted file mode 100644 index 532da310..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/view/PRXScreenViewEvent.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.view - -internal sealed class PRXScreenViewEvent - -internal object NavigateUpEvent : PRXScreenViewEvent() - -internal object TurnOnAlert : PRXScreenViewEvent() - -internal object TurnOffAlert : PRXScreenViewEvent() - -internal object DisconnectEvent : PRXScreenViewEvent() - -internal object OpenLoggerEvent : PRXScreenViewEvent() diff --git a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt b/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt deleted file mode 100644 index 3d88ffb3..00000000 --- a/profile_prx/src/main/java/no/nordicsemi/android/prx/viewmodel/PRXViewModel.kt +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.prx.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.profile.prx.AlarmLevel -import no.nordicsemi.android.prx.repository.AlarmHandler -import no.nordicsemi.android.prx.repository.PRXRepository -import no.nordicsemi.android.prx.repository.PRX_SERVICE_UUID -import no.nordicsemi.android.prx.view.DisconnectEvent -import no.nordicsemi.android.prx.view.NavigateUpEvent -import no.nordicsemi.android.prx.view.OpenLoggerEvent -import no.nordicsemi.android.prx.view.PRXScreenViewEvent -import no.nordicsemi.android.prx.view.TurnOffAlert -import no.nordicsemi.android.prx.view.TurnOnAlert -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class PRXViewModel @Inject constructor( - private val repository: PRXRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics, - private val alarmHandler: AlarmHandler -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.isLinkLossDisconnected) { - alarmHandler.playAlarm(it.linkLossAlarmLevel) - } - - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.PRX)) - } - }.launchIn(viewModelScope) - - repository.data - .map { it.localAlarmLevel } - .distinctUntilChanged() - .onEach { alarmHandler.playAlarm(it) } - .launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(PRX_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> repository.launch(result.value) - } - } - - fun onEvent(event: PRXScreenViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - TurnOffAlert -> repository.setRemoteAlarmLevel(AlarmLevel.NONE) - TurnOnAlert -> repository.setRemoteAlarmLevel(AlarmLevel.HIGH) - NavigateUpEvent -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() - } - } - - private fun disconnect() { - alarmHandler.pauseAlarm() - navigationManager.navigateUp() - repository.disconnect() - } - - override fun onCleared() { - super.onCleared() - alarmHandler.pauseAlarm() - repository.setOnScreen(false) - } -} diff --git a/profile_prx/src/main/res/values/strings.xml b/profile_prx/src/main/res/values/strings.xml deleted file mode 100644 index e2dfe443..00000000 --- a/profile_prx/src/main/res/values/strings.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Proximity - - Silent me - Find me - - Data - Settings - - on - off - - none - medium - height - - Remote alarm - Local alarm level - - Device out of range - Device is out of range and it has disconnected. It should reconnect automatically after device is in range again. - diff --git a/profile_rscs/build.gradle.kts b/profile_rscs/build.gradle.kts deleted file mode 100644 index 25607f2d..00000000 --- a/profile_rscs/build.gradle.kts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) -} - -android { - namespace = "no.nordicsemi.android.rscs" -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) -} diff --git a/profile_rscs/module-rules.pro b/profile_rscs/module-rules.pro deleted file mode 100644 index 83f673f9..00000000 --- a/profile_rscs/module-rules.pro +++ /dev/null @@ -1,17 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/profile_rscs/src/main/AndroidManifest.xml b/profile_rscs/src/main/AndroidManifest.xml deleted file mode 100644 index 06c80901..00000000 --- a/profile_rscs/src/main/AndroidManifest.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceData.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceData.kt deleted file mode 100644 index 6251a857..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/data/RSCSServiceData.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.data - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.rscs.data.RSCSData -import no.nordicsemi.android.rscs.R - -internal data class RSCSServiceData( - val data: RSCSData = RSCSData(), - val batteryLevel: Int? = null, - val connectionState: GattConnectionStateWithStatus? = null, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } - - @Composable - fun displayActivity(): String { - return if (data.running) { - stringResource(id = R.string.rscs_running) - } else { - stringResource(id = R.string.rscs_walking) - } - } - - @Composable - fun displayPace(): String { - return stringResource(id = R.string.rscs_speed, data.instantaneousSpeed) - } - - @Composable - fun displayCadence(): String { - return stringResource(id = R.string.rscs_rpm, data.instantaneousCadence) - } - - @Composable - fun displayNumberOfSteps(): String? { - if (data.totalDistance == null || data.strideLength == null) { - return null - } - val numberOfSteps = data.totalDistance!! / data.strideLength!!.toLong() - return stringResource(id = R.string.rscs_steps, numberOfSteps) - } -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt deleted file mode 100644 index 5993114e..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSRepository.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.rscs.data.RSCSData -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.rscs.data.RSCSServiceData -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RSCSRepository @Inject constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val stringConst: StringConst -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(RSCSServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - private fun initLogger(device: ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "RSCS", device.address) - .also { Timber.plant(it) } - } - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(RSCSService::class.java, device) - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onRSCSDataChanged(data: RSCSData) { - _data.value = _data.value.copy(data = data) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = RSCSServiceData() - } -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt deleted file mode 100644 index a7f9a45a..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/repository/RSCSService.kt +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.kotlin.ble.profile.rscs.RSCSDataParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.* -import javax.inject.Inject - -val RSCS_SERVICE_UUID: UUID = UUID.fromString("00001814-0000-1000-8000-00805F9B34FB") -private val RSC_MEASUREMENT_CHARACTERISTIC_UUID = UUID.fromString("00002A53-0000-1000-8000-00805F9B34FB") - -private val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -private val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class RSCSService : NotificationService() { - - @Inject - lateinit var repository: RSCSRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@RSCSService, device, lifecycleScope) - this@RSCSService.client = client - - client.connectionStateWithStatus - .onEach { repository.onConnectionStateChanged(it) } - .filterNotNull() - .onEach { stopIfDisconnected(it) } - .launchIn(lifecycleScope) - - if (!client.isConnected) { - return@launch - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val rscsService = services.findService(RSCS_SERVICE_UUID)!! - val rscsMeasurementCharacteristic = rscsService.findCharacteristic(RSC_MEASUREMENT_CHARACTERISTIC_UUID)!! - - rscsMeasurementCharacteristic.getNotifications() - .mapNotNull { RSCSDataParser.parse(it) } - .onEach { repository.onRSCSDataChanged(it) } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun stopIfDisconnected(connectionState: GattConnectionStateWithStatus) { - if (connectionState.state == GattConnectionState.STATE_DISCONNECTED) { - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt deleted file mode 100644 index 486561c3..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCSScreen.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.rscs.R -import no.nordicsemi.android.rscs.viewmodel.RSCSViewModel -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun RSCSScreen() { - val viewModel: RSCSViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUpEvent) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.deviceName, - connectionState = state.connectionState, - title = R.string.rscs_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLoggerEvent) } - ) - } - ) { - Column( - modifier = Modifier - .padding(it) - .verticalScroll(rememberScrollState()) - .padding(16.dp) - ) { - when (state.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> DeviceConnectingView { NavigateUpButton(navigateUp) } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> DeviceDisconnectedView(state.disconnectStatus) { - NavigateUpButton(navigateUp) - } - GattConnectionState.STATE_CONNECTED -> RSCSContentView(state) { viewModel.onEvent(it) } - } - } - } -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt deleted file mode 100644 index fffc53a1..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/RSCScreenViewEvent.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.view - -internal sealed class RSCScreenViewEvent - -internal object NavigateUpEvent : RSCScreenViewEvent() - -internal object DisconnectEvent : RSCScreenViewEvent() - -internal object OpenLoggerEvent : RSCScreenViewEvent() diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt deleted file mode 100644 index 54360961..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/view/SensorsReadingView.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.view - -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.rscs.R -import no.nordicsemi.android.rscs.data.RSCSServiceData -import no.nordicsemi.android.ui.view.KeyValueField -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun SensorsReadingView(state: RSCSServiceData) { - ScreenSection { - SectionTitle(resId = R.drawable.ic_records, title = "Records") - - Spacer(modifier = Modifier.height(16.dp)) - - KeyValueField(stringResource(id = R.string.rscs_activity), state.displayActivity()) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.rscs_pace), state.displayPace()) - Spacer(modifier = Modifier.height(4.dp)) - KeyValueField(stringResource(id = R.string.rscs_cadence), state.displayCadence()) - Spacer(modifier = Modifier.height(4.dp)) - state.displayNumberOfSteps()?.let { - KeyValueField(stringResource(id = R.string.rscs_number_of_steps), it) - } - } -} - -@Preview -@Composable -private fun Preview() { - SensorsReadingView(RSCSServiceData()) -} diff --git a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt b/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt deleted file mode 100644 index 74309784..00000000 --- a/profile_rscs/src/main/java/no/nordicsemi/android/rscs/viewmodel/RSCSViewModel.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.rscs.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.rscs.repository.RSCSRepository -import no.nordicsemi.android.rscs.repository.RSCS_SERVICE_UUID -import no.nordicsemi.android.rscs.view.DisconnectEvent -import no.nordicsemi.android.rscs.view.NavigateUpEvent -import no.nordicsemi.android.rscs.view.OpenLoggerEvent -import no.nordicsemi.android.rscs.view.RSCScreenViewEvent -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import javax.inject.Inject - -@HiltViewModel -internal class RSCSViewModel @Inject constructor( - private val repository: RSCSRepository, - private val navigationManager: Navigator, - private val analytics: AppAnalytics -) : ViewModel() { - - val state = repository.data - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.RSCS)) - } - }.launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(RSCS_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - private fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - fun onEvent(event: RSCScreenViewEvent) { - when (event) { - DisconnectEvent -> disconnect() - NavigateUpEvent -> navigationManager.navigateUp() - OpenLoggerEvent -> repository.openLogger() - } - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_rscs/src/main/res/values/strings.xml b/profile_rscs/src/main/res/values/strings.xml deleted file mode 100644 index eb061fc4..00000000 --- a/profile_rscs/src/main/res/values/strings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - Running speed & cadence - - Activity - Pace - Cadence - Number of steps - - Walking - Running - %.1f min/km - %d RPM - Number of Steps %d - diff --git a/profile_uart/build.gradle.kts b/profile_uart/build.gradle.kts deleted file mode 100644 index 1d88c4d6..00000000 --- a/profile_uart/build.gradle.kts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -plugins { - alias(libs.plugins.nordic.feature) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.ksp) -} - -android { - namespace = "no.nordicsemi.android.uart" - - testOptions { - unitTests.isIncludeAndroidResources = true - } -} - -dependencies { - implementation(project(":lib_analytics")) - implementation(project(":lib_service")) - implementation(project(":lib_scanner")) - implementation(project(":lib_ui")) - implementation(project(":lib_utils")) - - implementation(libs.nordic.core) - implementation(libs.nordic.theme) - implementation(libs.nordic.ui) - implementation(libs.nordic.navigation) - implementation(libs.nordic.logger) - - implementation(libs.nordic.blek.client) - implementation(libs.nordic.blek.profile) - implementation(libs.nordic.blek.core) - implementation(libs.nordic.blek.server) - implementation(libs.nordic.blek.advertiser) - implementation(libs.nordic.blek.uiscanner) - - implementation(libs.room.runtime) - implementation(libs.room.ktx) - ksp(libs.room.compiler) - - implementation(libs.androidx.dataStore.core) - implementation(libs.androidx.dataStore.preferences) - implementation(libs.androidx.compose.material.iconsExtended) - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.lifecycle.service) - - implementation(libs.androidx.hilt.navigation.compose) - - - // Timber & SLF4J - implementation (libs.slf4j.timber) - implementation(libs.nordic.log.timber) - - testImplementation(libs.hilt.android.testing) - kaptTest(libs.hilt.compiler) - testImplementation(libs.androidx.test.rules) - - testImplementation(libs.junit4) - testImplementation(libs.test.mockk) - testImplementation(libs.androidx.test.ext) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.slf4j.simple) - testImplementation(libs.test.robolectric) - testImplementation(libs.kotlin.junit) - - implementation("org.simpleframework:simple-xml:2.7.1") { - exclude(group = "stax", module = "stax-api") - exclude(group = "xpp3", module = "xpp3") - } -} diff --git a/profile_uart/module-rules.pro b/profile_uart/module-rules.pro deleted file mode 100644 index 4bc45e71..00000000 --- a/profile_uart/module-rules.pro +++ /dev/null @@ -1,20 +0,0 @@ -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in C:/Users/alno/AppData/Local/Android/sdk/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the proguardFiles -# directive in build.gradle.kts. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - --keep class no.nordicsemi.android.uart.db.XmlConfiguration{ *; } --keep class no.nordicsemi.android.uart.db.XmlMacro{ *; } diff --git a/profile_uart/src/main/AndroidManifest.xml b/profile_uart/src/main/AndroidManifest.xml deleted file mode 100644 index 5f9114e8..00000000 --- a/profile_uart/src/main/AndroidManifest.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt deleted file mode 100644 index 68b70208..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/DaoHiltModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package no.nordicsemi.android.uart - -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import no.nordicsemi.android.uart.db.ConfigurationsDao -import no.nordicsemi.android.uart.db.ConfigurationsDatabase -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class DaoHiltModule { - - @Provides - @Singleton - internal fun provideDao(db: ConfigurationsDatabase): ConfigurationsDao { - return db.dao() - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/UartServer.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/UartServer.kt deleted file mode 100644 index d4075f62..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/UartServer.kt +++ /dev/null @@ -1,143 +0,0 @@ -package no.nordicsemi.android.uart - -import android.annotation.SuppressLint -import android.content.Context -import android.os.ParcelUuid -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.advertiser.BleAdvertiser -import no.nordicsemi.android.kotlin.ble.core.MockServerDevice -import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertisingConfig -import no.nordicsemi.android.kotlin.ble.core.advertiser.BleAdvertisingData -import no.nordicsemi.android.kotlin.ble.core.data.BleGattPermission -import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty -import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray -import no.nordicsemi.android.kotlin.ble.server.main.ServerBleGatt -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattCharacteristicConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceConfig -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBleGattServiceType -import no.nordicsemi.android.kotlin.ble.server.main.service.ServerBluetoothGattConnection -import no.nordicsemi.android.uart.repository.BATTERY_LEVEL_CHARACTERISTIC_UUID -import no.nordicsemi.android.uart.repository.BATTERY_SERVICE_UUID -import no.nordicsemi.android.uart.repository.UART_RX_CHARACTERISTIC_UUID -import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID -import no.nordicsemi.android.uart.repository.UART_TX_CHARACTERISTIC_UUID -import javax.inject.Inject -import javax.inject.Singleton - -private const val STANDARD_DELAY = 1000L - -@SuppressLint("MissingPermission") -@Singleton -class UartServer @Inject constructor( - private val scope: CoroutineScope, -) { - - private lateinit var server: ServerBleGatt - - private lateinit var rxCharacteristic: ServerBleGattCharacteristic - private lateinit var txCharacteristic: ServerBleGattCharacteristic - private lateinit var batteryLevelCharacteristic: ServerBleGattCharacteristic - - fun start( - context: Context, - device: MockServerDevice = MockServerDevice( - name = "Mock UART Server", - address = "66:55:44:33:22:11" - ), - ) = scope.launch { - val rxCharacteristic = ServerBleGattCharacteristicConfig( - UART_RX_CHARACTERISTIC_UUID, - listOf(BleGattProperty.PROPERTY_NOTIFY, BleGattProperty.PROPERTY_WRITE), - listOf() - ) - - val txCharacteristic = ServerBleGattCharacteristicConfig( - UART_TX_CHARACTERISTIC_UUID, - listOf(BleGattProperty.PROPERTY_INDICATE, BleGattProperty.PROPERTY_WRITE), - listOf(BleGattPermission.PERMISSION_WRITE) - ) - - val uartService = ServerBleGattServiceConfig( - UART_SERVICE_UUID, - ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - listOf(rxCharacteristic, txCharacteristic) - ) - - val batteryLevelCharacteristic = ServerBleGattCharacteristicConfig( - BATTERY_LEVEL_CHARACTERISTIC_UUID, - listOf(BleGattProperty.PROPERTY_READ, BleGattProperty.PROPERTY_NOTIFY), - listOf(BleGattPermission.PERMISSION_READ) - ) - - val batteryService = ServerBleGattServiceConfig( - BATTERY_SERVICE_UUID, - ServerBleGattServiceType.SERVICE_TYPE_PRIMARY, - listOf(batteryLevelCharacteristic) - ) - - server = ServerBleGatt.create( - context = context, - scope = scope, - config = arrayOf(uartService, batteryService), - mock = device - ) - - val advertiser = BleAdvertiser.create(context) - advertiser.advertise( - config = BleAdvertisingConfig( - advertiseData = BleAdvertisingData( - serviceUuid = ParcelUuid(UART_SERVICE_UUID) - ) - ), mock = device - ).launchIn(scope) - - launch { - server.connections - .mapNotNull { it.values.firstOrNull() } - .collect { setUpConnection(it) } - } - } - - internal fun stopServer() { - server.stopServer() - } - - private fun setUpConnection(connection: ServerBluetoothGattConnection) { - val glsService = connection.services.findService(UART_SERVICE_UUID)!! - rxCharacteristic = glsService.findCharacteristic(UART_RX_CHARACTERISTIC_UUID)!! - txCharacteristic = glsService.findCharacteristic(UART_TX_CHARACTERISTIC_UUID)!! - - rxCharacteristic.value.onEach { - send(txCharacteristic, it) - }.launchIn(scope) - - val batteryService = connection.services.findService(BATTERY_SERVICE_UUID)!! - batteryLevelCharacteristic = batteryService - .findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID)!! - - startBatteryService() - } - - private fun startBatteryService() { - scope.launch { - repeat(100) { - send(batteryLevelCharacteristic, DataByteArray.from(0x61)) - delay(STANDARD_DELAY) - send(batteryLevelCharacteristic, DataByteArray.from(0x60)) - delay(STANDARD_DELAY) - send(batteryLevelCharacteristic, DataByteArray.from(0x5F)) - delay(STANDARD_DELAY) - } - } - } - - private suspend fun send(characteristics: ServerBleGattCharacteristic, data: DataByteArray) { - characteristics.setValueAndNotifyClient(data) - } -} \ No newline at end of file diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/ConfigurationDataSource.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/ConfigurationDataSource.kt deleted file mode 100644 index 3b0e4ddf..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/ConfigurationDataSource.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.map -import javax.inject.Inject -import javax.inject.Singleton - -private const val FILE = "UART_CONFIGURATION" -private const val LAST_CONFIGURATION_KEY = "LAST_CONFIGURATION" - -@Singleton -internal class ConfigurationDataSource @Inject constructor( - @ApplicationContext - private val context: Context -) { - - private val Context.dataStore: DataStore by preferencesDataStore(name = FILE) - - private val LAST_CONFIGURATION = stringPreferencesKey(LAST_CONFIGURATION_KEY) - - val lastConfigurationName = context.dataStore.data.map { - it[LAST_CONFIGURATION] - } - - suspend fun saveConfigurationName(name: String) { - context.dataStore.edit { - it[LAST_CONFIGURATION] = name - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroEol.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroEol.kt deleted file mode 100644 index a468a2c5..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroEol.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -enum class MacroEol(val index: Int) { - LF(0), - CR(1), - CR_LF(2); -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroIcon.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroIcon.kt deleted file mode 100644 index b8d4eded..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/MacroIcon.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -enum class MacroIcon(public val index: Int) { - LEFT(0), - UP(1), - RIGHT(2), - DOWN(3), - SETTINGS(4), - REW(5), - PLAY(6), - PAUSE(7), - STOP(8), - FWD(9), - INFO(10), - NUMBER_1(11), - NUMBER_2(12), - NUMBER_3(13), - NUMBER_4(14), - NUMBER_5(15), - NUMBER_6(16), - NUMBER_7(17), - NUMBER_8(18), - NUMBER_9(19); - - companion object { - fun create(index: Int): MacroIcon { - return entries.firstOrNull { it.index == index } - ?: throw IllegalArgumentException("Cannot create MacroIcon for index: $index") - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTConfiguration.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTConfiguration.kt deleted file mode 100644 index 3cbde998..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTConfiguration.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -private const val MACROS_SIZES = 9 - -data class UARTConfiguration( - val id: Int?, - val name: String, - val macros: List = List(9) { null } -) { - - init { - if (macros.size < 9) { - throw IllegalArgumentException("Macros should always have 9 positions.") - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTMacro.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTMacro.kt deleted file mode 100644 index 94a718bd..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTMacro.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -data class UARTMacro(val icon: MacroIcon, val command: String?, val newLineChar: MacroEol) diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTParser.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTParser.kt deleted file mode 100644 index ee10739f..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTParser.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -fun String.parseWithNewLineChar(newLineChar: MacroEol): String { - return when (newLineChar) { - MacroEol.LF -> this - MacroEol.CR_LF -> this.replace("\n", "\r\n") - MacroEol.CR -> this.replace("\n", "\r") - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTPersistentDataSource.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTPersistentDataSource.kt deleted file mode 100644 index b23f83bf..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTPersistentDataSource.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.uart.db.CommentVisitor -import no.nordicsemi.android.uart.db.Configuration -import no.nordicsemi.android.uart.db.ConfigurationsDao -import no.nordicsemi.android.uart.db.XmlConfiguration -import no.nordicsemi.android.uart.db.XmlMacro -import org.simpleframework.xml.Serializer -import org.simpleframework.xml.core.Persister -import org.simpleframework.xml.strategy.Strategy -import org.simpleframework.xml.strategy.VisitorStrategy -import org.simpleframework.xml.stream.Format -import org.simpleframework.xml.stream.HyphenStyle -import java.io.StringWriter -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class UARTPersistentDataSource @Inject constructor( - private val configurationsDao: ConfigurationsDao, -) { - - fun getConfigurations(): Flow> = configurationsDao.load().map { - it.mapNotNull { it.toDomain() } - } - - private fun Configuration.toDomain(): UARTConfiguration? { - return try { - val xml: String = xml - val format = Format(HyphenStyle()) - val serializer: Serializer = Persister(format) - val configuration = serializer.read(XmlConfiguration::class.java, xml) - - UARTConfiguration( - _id, - configuration.name ?: "Unknown", - createMacro(configuration.commands) - ) - } catch (t: Throwable) { - t.printStackTrace() - null - } - } - - private fun createMacro(macros: Array): List { - return macros.map { - if (it == null) { - null - } else { - val icon = MacroIcon.create(it.iconIndex) - UARTMacro(icon, it.command, it.eol) - } - } - } - - suspend fun saveConfiguration(configuration: UARTConfiguration) { - val format = Format(HyphenStyle()) - val strategy: Strategy = VisitorStrategy(CommentVisitor()) - val serializer: Serializer = Persister(strategy, format) - val writer = StringWriter() - serializer.write(configuration.toXmlConfiguration(), writer) - val xml = writer.toString() - - configurationsDao.insert(Configuration(configuration.id, configuration.name, xml, 0)) - } - - suspend fun deleteConfiguration(configuration: UARTConfiguration) { - configurationsDao.delete(configuration.name) - } - - private fun UARTConfiguration.toXmlConfiguration(): XmlConfiguration { - val xmlConfiguration = XmlConfiguration() - xmlConfiguration.name = name - val commands = macros.map { macro -> - macro?.let { - XmlMacro().apply { - setEol(it.newLineChar.index) - command = it.command - iconIndex = it.icon.index - } - } - }.toTypedArray() - xmlConfiguration.commands = commands - return xmlConfiguration - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt deleted file mode 100644 index ee7b0ac8..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/data/UARTServiceData.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.data - -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus - -internal data class UARTServiceData( - val messages: List = emptyList(), - val connectionState: GattConnectionStateWithStatus? = null, - val batteryLevel: Int? = null, - val deviceName: String? = null, - val missingServices: Boolean = false -) { - - val disconnectStatus = if (missingServices) { - BleGattConnectionStatus.NOT_SUPPORTED - } else { - connectionState?.status ?: BleGattConnectionStatus.UNKNOWN - } - - val displayMessages = messages -} - -internal data class UARTRecord( - val text: String, - val type: UARTRecordType, - val timestamp: Long = System.currentTimeMillis() -) - -enum class UARTRecordType { - INPUT, OUTPUT -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/CommentVisitor.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/CommentVisitor.kt deleted file mode 100644 index cf829057..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/CommentVisitor.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db - -import no.nordicsemi.android.uart.data.MacroIcon -import org.simpleframework.xml.strategy.Type -import org.simpleframework.xml.strategy.Visitor -import org.simpleframework.xml.stream.InputNode -import org.simpleframework.xml.stream.NodeMap -import org.simpleframework.xml.stream.OutputNode - -/** - * The comment visitor will add comments to the XML during saving. - */ -internal class CommentVisitor : Visitor { - override fun read(type: Type, node: NodeMap) { - // do nothing - } - - override fun write(type: Type, node: NodeMap) { - if (type.type == Array::class.java) { - val element = node.node - val builder = - StringBuilder("A configuration must have 9 commands, one for each button.\n Possible icons are:") - for (icon in MacroIcon.entries) builder.append("\n - ") - .append(icon.toString()) - element.comment = builder.toString() - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/Configuration.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/Configuration.kt deleted file mode 100644 index 6eecbf57..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/Configuration.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "configurations") -internal data class Configuration( - @PrimaryKey(autoGenerate = true) - @ColumnInfo(name = "_id") val _id: Int?, - @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "xml") val xml: String, - @ColumnInfo(name = "deleted", defaultValue = "0") val deleted: Int -) diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDao.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDao.kt deleted file mode 100644 index 3f788df8..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDao.kt +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query -import kotlinx.coroutines.flow.Flow - -@Dao -internal interface ConfigurationsDao { - - @Query("SELECT * FROM configurations") - fun load(): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(configuration: Configuration) - - @Query("DELETE FROM configurations WHERE name = :name") - suspend fun delete(name: String) -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDatabase.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDatabase.kt deleted file mode 100644 index e781838b..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/ConfigurationsDatabase.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db - -import androidx.room.Database -import androidx.room.RoomDatabase - -@Database(entities = [Configuration::class], version = 2) -internal abstract class ConfigurationsDatabase : RoomDatabase() { - abstract fun dao(): ConfigurationsDao -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/InitMigration.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/InitMigration.kt deleted file mode 100644 index 5bc8bdf7..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/InitMigration.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db - -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase - -val MIGRATION_1_2 = object : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - // Empty implementation, because the schema isn't changing. - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlConfiguration.java b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlConfiguration.java deleted file mode 100644 index 1691252e..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlConfiguration.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.ElementArray; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.core.PersistenceException; -import org.simpleframework.xml.core.Validate; - -@Root -public class XmlConfiguration { - public static final int COMMANDS_COUNT = 9; - - @Attribute(required = false, empty = "Unnamed") - private String name; - - @ElementArray - private XmlMacro[] commands = new XmlMacro[COMMANDS_COUNT]; - - /** - * Returns the field name - * - * @return optional name - */ - public String getName() { - return name; - } - - /** - * Sets the name to specified value - * @param name the new name - */ - public void setName(final String name) { - this.name = name; - } - - /** - * Returns the array of commands. There is always 9 of them. - * @return the commands array - */ - public XmlMacro[] getCommands() { - return commands; - } - - public void setCommands(XmlMacro[] commands) { - this.commands = commands; - } - - @Validate - private void validate() throws PersistenceException{ - if (commands == null || commands.length != COMMANDS_COUNT) - throw new PersistenceException("There must be always " + COMMANDS_COUNT + " commands in a configuration."); - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlMacro.java b/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlMacro.java deleted file mode 100644 index 726b9b34..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/db/XmlMacro.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.db; - -import org.simpleframework.xml.Attribute; -import org.simpleframework.xml.Root; -import org.simpleframework.xml.Text; - -import no.nordicsemi.android.uart.data.MacroEol; -import no.nordicsemi.android.uart.data.MacroIcon; - -@Root -public class XmlMacro { - - @Text(required = false) - private String command; - - @Attribute(required = false) - private boolean active = false; - - @Attribute(required = false) - private MacroEol eol = MacroEol.LF; - - @Attribute(required = false) - private MacroIcon icon = MacroIcon.LEFT; - - /** - * Sets the command. - * @param command the command that will be sent to UART device - */ - public void setCommand(final String command) { - this.command = command; - } - - /** - * Sets whether the command is active. - * @param active true to make it active - */ - public void setActive(final boolean active) { - this.active = active; - } - - /** - * Sets the new line type. - * @param eol end of line terminator - */ - public void setEol(final int eol) { - this.eol = MacroEol.values()[eol]; - } - - /** - * Sets the icon index. - * @param index index of the icon. - */ - public void setIconIndex(final int index) { - this.icon = MacroIcon.values()[index]; - } - - /** - * Returns the command that will be sent to UART device. - * @return the command - */ - public String getCommand() { - return command; - } - - /** - * Returns whether the icon is active. - * @return true if it's active - */ - public boolean isActive() { - return active; - } - - /** - * Returns the new line type. - * @return end of line terminator - */ - public MacroEol getEol() { - return eol; - } - - /** - * Returns the icon index. - * @return the icon index - */ - public int getIconIndex() { - return icon.getIndex(); - } - /** - * Returns the EOL index. - * @return the EOL index - */ - public int getEolIndex() { - return eol.getIndex(); - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt deleted file mode 100644 index b93534e5..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTRepository.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.repository - -import android.content.Context -import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import no.nordicsemi.android.common.core.simpleSharedFlow -import no.nordicsemi.android.common.logger.LoggerLauncher -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.log.LogSession -import no.nordicsemi.android.log.timber.nRFLoggerTree -import no.nordicsemi.android.service.DisconnectAndStopEvent -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.uart.data.ConfigurationDataSource -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.UARTMacro -import no.nordicsemi.android.uart.data.UARTRecord -import no.nordicsemi.android.uart.data.UARTRecordType -import no.nordicsemi.android.uart.data.UARTServiceData -import no.nordicsemi.android.uart.data.parseWithNewLineChar -import no.nordicsemi.android.ui.view.StringConst -import timber.log.Timber -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UARTRepository @Inject internal constructor( - @ApplicationContext - private val context: Context, - private val serviceManager: ServiceManager, - private val configurationDataSource: ConfigurationDataSource, - private val stringConst: StringConst, -) { - private var logger: nRFLoggerTree? = null - - private val _data = MutableStateFlow(UARTServiceData()) - internal val data = _data.asStateFlow() - - private val _stopEvent = simpleSharedFlow() - internal val stopEvent = _stopEvent.asSharedFlow() - - private val _command = simpleSharedFlow() - internal val command = _command.asSharedFlow() - - val isRunning = data.map { it.connectionState?.state == GattConnectionState.STATE_CONNECTED } - - val lastConfigurationName = configurationDataSource.lastConfigurationName - - private var isOnScreen = false - private var isServiceRunning = false - - fun setOnScreen(isOnScreen: Boolean) { - this.isOnScreen = isOnScreen - - if (shouldClean()) clean() - } - - fun setServiceRunning(serviceRunning: Boolean) { - this.isServiceRunning = serviceRunning - - if (shouldClean()) clean() - } - - private fun shouldClean() = !isOnScreen && !isServiceRunning - - fun launch(device: ServerDevice) { - initLogger(device) - _data.value = _data.value.copy(deviceName = device.name) - serviceManager.startService(UARTService::class.java, device) - } - - private fun initLogger(device : ServerDevice) { - logger?.let { Timber.uproot(it) } - logger = nRFLoggerTree(context, stringConst.APP_NAME, "UART", device.name ?: "Unknown") - .also { Timber.plant(it) } - } - - fun onConnectionStateChanged(connectionState: GattConnectionStateWithStatus?) { - _data.value = _data.value.copy(connectionState = connectionState) - } - - fun onBatteryLevelChanged(batteryLevel: Int) { - _data.value = _data.value.copy(batteryLevel = batteryLevel) - } - - fun onNewMessageReceived(value: String) { - _data.value = _data.value.copy(messages = _data.value.messages + UARTRecord(value, UARTRecordType.OUTPUT)) - } - - fun onNewMessageSent(value: String) { - _data.value = _data.value.copy(messages = _data.value.messages + UARTRecord(value, UARTRecordType.INPUT)) - } - - fun sendText(text: String, newLineChar: MacroEol) { - _command.tryEmit(text.parseWithNewLineChar(newLineChar)) - } - - fun runMacro(macro: UARTMacro) { - if (macro.command == null) { - return - } - _command.tryEmit(macro.command.parseWithNewLineChar(macro.newLineChar)) - } - - fun clearItems() { - _data.value = _data.value.copy(messages = emptyList()) - } - - fun openLogger() { - LoggerLauncher.launch(context, logger?.session as? LogSession) - } - - fun log(priority: Int, message: String) { - logger?.log(priority, message) - } - - fun onMissingServices() { - _data.value = _data.value.copy(missingServices = true) - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - suspend fun saveConfigurationName(name: String) { - configurationDataSource.saveConfigurationName(name) - } - - fun disconnect() { - _stopEvent.tryEmit(DisconnectAndStopEvent()) - } - - private fun clean() { - logger = null - _data.value = UARTServiceData() - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt deleted file mode 100644 index 8639302b..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/repository/UARTService.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.repository - -import android.annotation.SuppressLint -import android.content.Intent -import androidx.core.content.IntentCompat -import androidx.lifecycle.lifecycleScope -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.kotlin.ble.client.main.callback.ClientBleGatt -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattCharacteristic -import no.nordicsemi.android.kotlin.ble.client.main.service.ClientBleGattServices -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.BleGattProperty -import no.nordicsemi.android.kotlin.ble.core.data.BleWriteType -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.Mtu -import no.nordicsemi.android.kotlin.ble.core.data.util.DataByteArray -import no.nordicsemi.android.kotlin.ble.profile.battery.BatteryLevelParser -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.NotificationService -import java.util.UUID -import javax.inject.Inject - -val UART_SERVICE_UUID: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") -internal val UART_RX_CHARACTERISTIC_UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") -internal val UART_TX_CHARACTERISTIC_UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") - -internal val BATTERY_SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805f9b34fb") -internal val BATTERY_LEVEL_CHARACTERISTIC_UUID = UUID.fromString("00002A19-0000-1000-8000-00805f9b34fb") - -@SuppressLint("MissingPermission") -@AndroidEntryPoint -internal class UARTService : NotificationService() { - - @Inject - lateinit var repository: UARTRepository - - private var client: ClientBleGatt? = null - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - repository.setServiceRunning(true) - - val device = IntentCompat.getParcelableExtra(intent!!, DEVICE_DATA, ServerDevice::class.java)!! - - startGattClient(device) - - repository.stopEvent - .onEach { disconnect() } - .launchIn(lifecycleScope) - - return START_REDELIVER_INTENT - } - - private fun startGattClient(device: ServerDevice) = lifecycleScope.launch { - val client = ClientBleGatt.connect(this@UARTService, device, lifecycleScope) - this@UARTService.client = client - - if (!client.isConnected) { - return@launch - } - - try { - client.requestMtu(Mtu.max) - } catch (e: Exception) { - e.printStackTrace() - } - - try { - val services = client.discoverServices() - configureGatt(services) - } catch (e: Exception) { - repository.onMissingServices() - } - - client.connectionStateWithStatus - .filterNotNull() - .onEach { repository.onConnectionStateChanged(it) } - .onEach { stopIfDisconnected(it.state, it.status) } - .filterNotNull() - .launchIn(lifecycleScope) - } - - private suspend fun configureGatt(services: ClientBleGattServices) { - val uartService = services.findService(UART_SERVICE_UUID)!! - val rxCharacteristic = uartService.findCharacteristic(UART_RX_CHARACTERISTIC_UUID)!! - val txCharacteristic = uartService.findCharacteristic(UART_TX_CHARACTERISTIC_UUID)!! - - txCharacteristic.getNotifications() - .map { String(it.value) } - .onEach { repository.onNewMessageReceived(it) } - .onEach { repository.log(10, "Received: $it") } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - repository.command - .onEach { rxCharacteristic.splitWrite(DataByteArray.from(it), getWriteType(rxCharacteristic)) } - .onEach { repository.onNewMessageSent(it) } - .onEach { repository.log(10, "Sent: $it") } - .catch { it.printStackTrace() } - .launchIn(lifecycleScope) - - // Battery service is optional - services.findService(BATTERY_SERVICE_UUID) - ?.findCharacteristic(BATTERY_LEVEL_CHARACTERISTIC_UUID) - ?.getNotifications() - ?.mapNotNull { BatteryLevelParser.parse(it) } - ?.onEach { repository.onBatteryLevelChanged(it) } - ?.catch { it.printStackTrace() } - ?.launchIn(lifecycleScope) - } - - private fun getWriteType(characteristic: ClientBleGattCharacteristic): BleWriteType { - return if (characteristic.properties.contains(BleGattProperty.PROPERTY_WRITE)) { - BleWriteType.DEFAULT - } else { - BleWriteType.NO_RESPONSE - } - } - - private fun stopIfDisconnected(connectionState: GattConnectionState, connectionStatus: BleGattConnectionStatus) { - if (connectionState == GattConnectionState.STATE_DISCONNECTED && !connectionStatus.isLinkLoss) { - repository.disconnect() - stopSelf() - } - } - - private fun disconnect() { - client?.disconnect() - } - - override fun onDestroy() { - super.onDestroy() - repository.setServiceRunning(false) - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/InputSection.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/InputSection.kt deleted file mode 100644 index 72f281b2..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/InputSection.kt +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch -import no.nordicsemi.android.common.ui.view.RadioButtonGroup -import no.nordicsemi.android.common.ui.view.RadioButtonItem -import no.nordicsemi.android.common.ui.view.RadioGroupViewEntity -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle -import no.nordicsemi.android.utils.EMPTY - -@Composable -internal fun InputSection(onEvent: (UARTViewEvent) -> Unit) { - val text = rememberSaveable { mutableStateOf(String.EMPTY) } - val hint = stringResource(id = R.string.uart_input_hint) - val checkedItem = rememberSaveable { mutableStateOf(MacroEol.entries[0]) } - - Row(verticalAlignment = Alignment.CenterVertically) { - Box(modifier = Modifier.weight(1f)) { - - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 65.dp) - .verticalScroll(scrollState), - value = text.value, - label = { Text(hint) }, - onValueChange = { newValue: String -> - text.value = newValue - scope.launch { - scrollState.scrollTo(Int.MAX_VALUE) - } - } - ) - } - - Spacer(modifier = Modifier.size(16.dp)) - - Button( - onClick = { - onEvent(OnRunInput(text.value, checkedItem.value)) - text.value = String.EMPTY - }, - modifier = Modifier.padding(top = 6.dp) - ) { - Text(text = stringResource(id = R.string.uart_send)) - } - } -} - -@Composable -internal fun EditInputSection(onEvent: (UARTViewEvent) -> Unit) { - val checkedItem = rememberSaveable { mutableStateOf(MacroEol.entries[0]) } - - val items = MacroEol.entries.map { - RadioButtonItem(it.toDisplayString(), it == checkedItem.value) - } - val viewEntity = RadioGroupViewEntity(items) - - ScreenSection { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - SectionTitle( - resId = R.drawable.ic_input, - title = stringResource(R.string.uart_input), - menu = { - IconButton(onClick = { onEvent(MacroInputSwitchClick) }) { - Icon( - painterResource(id = R.drawable.ic_macro), - contentDescription = stringResource(id = R.string.uart_input_macro), - ) - } - } - ) - - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(id = R.string.uart_macro_dialog_eol), - style = MaterialTheme.typography.labelLarge - ) - - RadioButtonGroup(viewEntity) { - val i = items.indexOf(it) - checkedItem.value = MacroEol.values()[i] - } - } - - Spacer(modifier = Modifier.size(16.dp)) - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/MacroSection.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/MacroSection.kt deleted file mode 100644 index 477d0e50..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/MacroSection.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.ui.view.ScreenSection -import no.nordicsemi.android.ui.view.SectionTitle - -@Composable -internal fun MacroSection(viewState: UARTViewState, onEvent: (UARTViewEvent) -> Unit) { - val showAddDialog = rememberSaveable { mutableStateOf(false) } - val showDeleteDialog = rememberSaveable { mutableStateOf(false) } - - if (showAddDialog.value) { - UARTAddConfigurationDialog(onEvent) { showAddDialog.value = false } - } - - if (showDeleteDialog.value) { - DeleteConfigurationDialog(onEvent) { showDeleteDialog.value = false } - } - - if (viewState.showEditDialog) { - UARTAddMacroDialog(viewState.selectedMacro) { onEvent(it) } - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(16.dp) - .heightIn(min = 400.dp) - ) { - ScreenSection { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - SectionTitle( - resId = R.drawable.ic_macro, - title = stringResource(R.string.uart_macros), - menu = { - viewState.selectedConfiguration?.let { - if (!viewState.isConfigurationEdited) { - IconButton(onClick = { onEvent(OnEditConfiguration) }) { - Icon( - Icons.Default.Edit, - stringResource(id = R.string.uart_configuration_edit) - ) - } - } else { - IconButton(onClick = { onEvent(OnEditConfiguration) }) { - Icon( - painterResource(id = R.drawable.ic_pencil_off), - stringResource(id = R.string.uart_configuration_edit) - ) - } - } - IconButton(onClick = { showDeleteDialog.value = true }) { - Icon( - Icons.Default.Delete, - stringResource(id = R.string.uart_configuration_delete) - ) - } - } - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Row { - Box(modifier = Modifier.weight(1f)) { - UARTConfigurationPicker(viewState, onEvent) - } - - Spacer(modifier = Modifier.size(16.dp)) - - Button(onClick = { showAddDialog.value = true }) { - Text(stringResource(id = R.string.uart_configuration_add)) - } - } - - viewState.selectedConfiguration?.let { - Spacer(modifier = Modifier.height(16.dp)) - - UARTMacroView(it, viewState.isConfigurationEdited, onEvent) - } - } - } - } -} - -@Composable -private fun DeleteConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) { - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(id = R.string.uart_delete_dialog_title), - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - Text(text = stringResource(id = R.string.uart_delete_dialog_info)) - }, - confirmButton = { - TextButton(onClick = { - onDismiss() - onEvent(OnDeleteConfiguration) - }) { - Text(text = stringResource(id = R.string.uart_delete_dialog_confirm)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(text = stringResource(id = R.string.uart_delete_dialog_cancel)) - } - } - ) -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/OutputSection.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/OutputSection.kt deleted file mode 100644 index 42145413..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/OutputSection.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.UARTRecord -import no.nordicsemi.android.uart.data.UARTRecordType -import no.nordicsemi.android.ui.view.SectionTitle -import java.text.SimpleDateFormat -import java.util.Locale - -@Composable -internal fun OutputSection(records: List, onEvent: (UARTViewEvent) -> Unit) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize() - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - SectionTitle( - resId = R.drawable.ic_output, - title = stringResource(R.string.uart_output), - modifier = Modifier, - menu = { Menu(onEvent) } - ) - } - - Spacer(modifier = Modifier.size(16.dp)) - - val scrollState = rememberLazyListState() - - LazyColumn( - modifier = Modifier.fillMaxWidth(), - state = scrollState - ) { - if (records.isEmpty()) { - item { Text(text = stringResource(id = R.string.uart_output_placeholder)) } - } else { - records.forEach { - item { - when (it.type) { - UARTRecordType.INPUT -> MessageItemInput(record = it) - UARTRecordType.OUTPUT -> MessageItemOutput(record = it) - } - - Spacer(modifier = Modifier.height(16.dp)) - } - } - } - } - - LaunchedEffect(records) { - if (scrollState.isScrolledToTheEnd() || records.isEmpty()) { - return@LaunchedEffect - } - launch { - scrollState.scrollToItem(records.lastIndex) - } - } - } -} - -fun LazyListState.isScrolledToTheEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 - -@Composable -private fun MessageItemInput(record: UARTRecord) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.End - ) { - Text( - text = record.timeToString(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(4.dp)) - Column( - modifier = Modifier - .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomStart = 10.dp)) - .background(MaterialTheme.colorScheme.secondary) - .padding(8.dp), - horizontalAlignment = Alignment.End - ) { - Text( - text = record.text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondary - ) - } - } -} - -@Composable -private fun MessageItemOutput(record: UARTRecord) { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start - ) { - Text( - text = record.timeToString(), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurface, - ) - Spacer(modifier = Modifier.height(4.dp)) - Column( - modifier = Modifier - .clip(RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp, bottomEnd = 10.dp)) - .background(MaterialTheme.colorScheme.primary) - .padding(8.dp) - ) { - Text( - text = record.text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onPrimary - ) - } - } -} - -@Composable -private fun Menu(onEvent: (UARTViewEvent) -> Unit) { - Row { - IconButton(onClick = { onEvent(ClearOutputItems) }) { - Icon( - Icons.Default.Delete, - contentDescription = stringResource(id = R.string.uart_clear_items), - ) - } - } -} - -private val datFormatter = SimpleDateFormat("dd MMMM yyyy, HH:mm:ss", Locale.ENGLISH) - -private fun UARTRecord.timeToString(): String { - return datFormatter.format(timestamp) -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddConfigurationDialog.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddConfigurationDialog.kt deleted file mode 100644 index 08fea8e0..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddConfigurationDialog.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.utils.EMPTY - -@Composable -internal fun UARTAddConfigurationDialog(onEvent: (UARTViewEvent) -> Unit, onDismiss: () -> Unit) { - val name = rememberSaveable { mutableStateOf(String.EMPTY) } - val isError = rememberSaveable { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = { onDismiss() }, - title = { Text(stringResource(id = R.string.uart_configuration_dialog_title)) }, - text = { NameInput(name, isError) }, - confirmButton = { - TextButton(onClick = { - if (isNameValid(name.value)) { - onDismiss() - onEvent(OnAddConfiguration(name.value)) - } else { - isError.value = true - } - }) { - Text(stringResource(id = R.string.uart_macro_dialog_confirm)) - } - }, - dismissButton = { - TextButton(onClick = { onDismiss() }) { - Text(stringResource(id = R.string.uart_macro_dialog_dismiss)) - } - } - ) -} - -@Composable -private fun NameInput( - name: MutableState, - isError: MutableState -) { - Column { - - OutlinedTextField( - value = name.value, - label = { Text(stringResource(id = R.string.uart_configuration_hint)) }, - singleLine = true, - onValueChange = { - isError.value = false - name.value = it - } - ) - - val errorText = if (isError.value) { - stringResource(id = R.string.uart_name_empty) - } else { - String.EMPTY - } - - Text( - text = errorText, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.error - ) - } -} - -private fun isNameValid(name: String): Boolean { - return name.isNotBlank() -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt deleted file mode 100644 index 17291d25..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTAddMacroDialog.kt +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.GridItemSpan -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.common.ui.view.RadioButtonGroup -import no.nordicsemi.android.common.ui.view.RadioButtonItem -import no.nordicsemi.android.common.ui.view.RadioGroupViewEntity -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.MacroIcon -import no.nordicsemi.android.uart.data.UARTMacro -import no.nordicsemi.android.utils.EMPTY - -private const val GRID_SIZE = 5 - -@Composable -internal fun UARTAddMacroDialog(macro: UARTMacro?, onEvent: (UARTViewEvent) -> Unit) { - val newLineChar = rememberSaveable { mutableStateOf(macro?.newLineChar ?: MacroEol.LF) } - val command = rememberSaveable { mutableStateOf(macro?.command ?: String.EMPTY) } - val selectedIcon = rememberSaveable { mutableStateOf(macro?.icon ?: MacroIcon.entries.toTypedArray()[0]) } - - AlertDialog( - onDismissRequest = { onEvent(OnEditFinish) }, - dismissButton = { - TextButton(onClick = { onEvent(OnDeleteMacro) }) { - Text(stringResource(id = R.string.uart_macro_dialog_delete)) - } - }, - confirmButton = { - TextButton(onClick = { - onEvent(OnCreateMacro(UARTMacro(selectedIcon.value, command.value, newLineChar.value))) - }) { - Text(stringResource(id = R.string.uart_macro_dialog_confirm)) - } - }, - title = { - Text( - text = stringResource(id = R.string.uart_macro_dialog_title), - style = MaterialTheme.typography.headlineSmall - ) - }, - text = { - LazyVerticalGrid( - columns = GridCells.Fixed(GRID_SIZE), - modifier = Modifier.wrapContentHeight() - ) { - item(span = { GridItemSpan(GRID_SIZE) }) { - Column { - NewLineCharSection(newLineChar.value) { newLineChar.value = it } - - Spacer(modifier = Modifier.size(16.dp)) - } - } - - item(span = { GridItemSpan(GRID_SIZE) }) { - CommandInput(command) - } - - items(20) { item -> - val icon = MacroIcon.create(item) - val background = if (selectedIcon.value == icon) { - MaterialTheme.colorScheme.primaryContainer - } else { - Color.Transparent - } - - Image( - painter = painterResource(id = icon.toResId()), - contentDescription = stringResource(id = R.string.uart_macro_icon), - colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer), - modifier = Modifier - .size(40.dp) - .clip(RoundedCornerShape(10.dp)) - .clickable { selectedIcon.value = icon } - .background(background) - ) - } - } - } - ) -} - -@Composable -private fun CommandInput(command: MutableState) { - Column { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth(), - value = command.value, - label = { Text(stringResource(id = R.string.uart_macro_dialog_command)) }, - onValueChange = { - command.value = it - } - ) - - Spacer(modifier = Modifier.size(16.dp)) - } -} - -@Composable -private fun NewLineCharSection(checkedItem: MacroEol, onItemClick: (MacroEol) -> Unit) { - val items = MacroEol.entries.map { - RadioButtonItem(it.toDisplayString(), it == checkedItem) - } - val viewEntity = RadioGroupViewEntity(items) - - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = stringResource(id = R.string.uart_macro_dialog_eol), - style = MaterialTheme.typography.labelLarge - ) - - RadioButtonGroup(viewEntity) { - val i = items.indexOf(it) - onItemClick(MacroEol.values()[i]) - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTConfigurationPicker.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTConfigurationPicker.kt deleted file mode 100644 index e4f3bfc1..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTConfigurationPicker.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.ui.view.dialog.FlowCanceled -import no.nordicsemi.android.ui.view.dialog.ItemSelectedResult -import no.nordicsemi.android.ui.view.dialog.StringListDialog -import no.nordicsemi.android.ui.view.dialog.StringListDialogConfig -import no.nordicsemi.android.ui.view.dialog.StringListDialogResult -import no.nordicsemi.android.ui.view.dialog.toAnnotatedString - -@Composable -internal fun UARTConfigurationPicker(state: UARTViewState, onEvent: (UARTViewEvent) -> Unit) { - val showDialog = rememberSaveable { mutableStateOf(false) } - - UARTConfigurationButton(state.selectedConfiguration) { - showDialog.value = true - } - - if (showDialog.value) { - SelectWheelSizeDialog(state) { - when (it) { - FlowCanceled -> showDialog.value = false - is ItemSelectedResult -> { - onEvent(OnConfigurationSelected(state.configurations[it.index])) - showDialog.value = false - } - } - } - } -} - -@Composable -internal fun SelectWheelSizeDialog(state: UARTViewState, onEvent: (StringListDialogResult) -> Unit) { - val wheelEntries = state.configurations.map { it.name } - - StringListDialog(createConfig(wheelEntries) { - onEvent(it) - }) -} - -@Composable -private fun createConfig(entries: List, onResult: (StringListDialogResult) -> Unit): StringListDialogConfig { - return StringListDialogConfig( - title = stringResource(id = R.string.uart_configuration_picker_dialog).toAnnotatedString(), - items = entries, - onResult = onResult, - leftIcon = R.drawable.ic_uart_settings - ) -} - -@Composable -internal fun UARTConfigurationButton(configuration: UARTConfiguration?, onClick: () -> Unit) { - OutlinedButton(onClick = { onClick() }) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ){ - Column { - Text( - text = stringResource(id = R.string.uart_configuration_picker_hint), - style = MaterialTheme.typography.labelSmall - ) - val text = configuration?.name ?: stringResource(id = R.string.uart_configuration_picker_not_selected) - Text(text = text, style = MaterialTheme.typography.bodyMedium) - } - - Icon(Icons.Default.ArrowDropDown, contentDescription = "") - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt deleted file mode 100644 index c918480c..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTContentView.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import no.nordicsemi.android.uart.data.UARTServiceData -import no.nordicsemi.android.ui.view.ScreenSection - -@Composable -internal fun UARTContentView( - state: UARTServiceData, - onEvent: (UARTViewEvent) -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .padding(16.dp) - .fillMaxSize() - ) { - - Box(modifier = Modifier.weight(1f)) { - ScreenSection { - Column(modifier = Modifier.fillMaxWidth()) { - OutputSection(state.displayMessages, onEvent) - } - } - } - - Spacer(modifier = Modifier.size(16.dp)) - - InputSection(onEvent = onEvent) - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMapper.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMapper.kt deleted file mode 100644 index 5b61fdf8..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTMapper.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.MacroIcon - -@Composable -fun MacroEol.toDisplayString(): String { - return when (this) { - MacroEol.LF -> stringResource(id = R.string.uart_macro_dialog_lf) - MacroEol.CR_LF -> stringResource(id = R.string.uart_macro_dialog_cr_lf) - MacroEol.CR -> stringResource(id = R.string.uart_macro_dialog_cr) - } -} - -@DrawableRes -fun MacroIcon.toResId(): Int { - return when (this) { - MacroIcon.LEFT -> R.drawable.ic_uart_left - MacroIcon.UP -> R.drawable.ic_uart_up - MacroIcon.RIGHT -> R.drawable.ic_uart_right - MacroIcon.DOWN -> R.drawable.ic_uart_down - MacroIcon.SETTINGS -> R.drawable.ic_uart_settings - MacroIcon.REW -> R.drawable.ic_uart_rewind - MacroIcon.PLAY -> R.drawable.ic_uart_play - MacroIcon.PAUSE -> R.drawable.ic_uart_pause - MacroIcon.STOP -> R.drawable.ic_uart_stop - MacroIcon.FWD -> R.drawable.ic_uart_forward - MacroIcon.INFO -> R.drawable.ic_uart_about - MacroIcon.NUMBER_1 -> R.drawable.ic_uart_1 - MacroIcon.NUMBER_2 -> R.drawable.ic_uart_2 - MacroIcon.NUMBER_3 -> R.drawable.ic_uart_3 - MacroIcon.NUMBER_4 -> R.drawable.ic_uart_4 - MacroIcon.NUMBER_5 -> R.drawable.ic_uart_5 - MacroIcon.NUMBER_6 -> R.drawable.ic_uart_6 - MacroIcon.NUMBER_7 -> R.drawable.ic_uart_7 - MacroIcon.NUMBER_8 -> R.drawable.ic_uart_8 - MacroIcon.NUMBER_9 -> R.drawable.ic_uart_9 - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt deleted file mode 100644 index cdbefacb..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTScreen.kt +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.hilt.navigation.compose.hiltViewModel -import no.nordicsemi.android.common.ui.view.PagerView -import no.nordicsemi.android.common.ui.view.PagerViewEntity -import no.nordicsemi.android.common.ui.view.PagerViewItem -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceConnectingView -import no.nordicsemi.android.kotlin.ble.ui.scanner.view.DeviceDisconnectedView -import no.nordicsemi.android.uart.R -import no.nordicsemi.android.uart.viewmodel.UARTViewModel -import no.nordicsemi.android.ui.view.NavigateUpButton -import no.nordicsemi.android.ui.view.ProfileAppBar - -@Composable -fun UARTScreen() { - val viewModel: UARTViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - - val navigateUp = { viewModel.onEvent(NavigateUp) } - - Scaffold( - topBar = { - ProfileAppBar( - deviceName = state.uartManagerState.deviceName, - connectionState = state.uartManagerState.connectionState, - title = R.string.uart_title, - navigateUp = navigateUp, - disconnect = { viewModel.onEvent(DisconnectEvent) }, - openLogger = { viewModel.onEvent(OpenLogger) } - ) - } - ) { - Column( - modifier = Modifier.padding(it) - ) { - when (state.uartManagerState.connectionState?.state) { - null, - GattConnectionState.STATE_CONNECTING -> PaddingBox { DeviceConnectingView { NavigateUpButton(navigateUp) } } - GattConnectionState.STATE_DISCONNECTED, - GattConnectionState.STATE_DISCONNECTING -> PaddingBox { - DeviceDisconnectedView(state.uartManagerState.disconnectStatus) { NavigateUpButton(navigateUp) } - } - GattConnectionState.STATE_CONNECTED -> SuccessScreen() - } - } - } -} - -@Composable -private fun PaddingBox(content: @Composable () -> Unit) { - Box(modifier = Modifier.padding(16.dp)) { - content() - } -} - -@Composable -private fun SuccessScreen() { - val input = stringResource(id = R.string.uart_input) - val macros = stringResource(id = R.string.uart_macros) - val viewEntity = remember { - PagerViewEntity( - listOf( - PagerViewItem(input) { KeyboardView() }, - PagerViewItem(macros) { MacroView() } - ) - ) - } - PagerView( - viewEntity = viewEntity, - modifier = Modifier.fillMaxSize(), - itemSpacing = 16.dp, - coroutineScope = rememberCoroutineScope(), - scrollable = false - ) -} - -@Composable -private fun KeyboardView() { - val viewModel: UARTViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - UARTContentView(state.uartManagerState) { viewModel.onEvent(it) } -} - -@Composable -private fun MacroView() { - val viewModel: UARTViewModel = hiltViewModel() - val state = viewModel.state.collectAsState().value - MacroSection(state) { viewModel.onEvent(it) } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt deleted file mode 100644 index b3f1cc70..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTState.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.uart.data.UARTMacro -import no.nordicsemi.android.uart.data.UARTServiceData - -internal data class UARTViewState( - val editedPosition: Int? = null, - val selectedConfigurationName: String? = null, - val isConfigurationEdited: Boolean = false, - val configurations: List = emptyList(), - val uartManagerState: UARTServiceData = UARTServiceData(), - val isInputVisible: Boolean = true -) { - val showEditDialog: Boolean = editedPosition != null - - val selectedConfiguration: UARTConfiguration? = configurations.find { selectedConfigurationName == it.name } - - val selectedMacro: UARTMacro? = selectedConfiguration?.let { configuration -> - editedPosition?.let { - configuration.macros.getOrNull(it) - } - } -} diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViewEvent.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViewEvent.kt deleted file mode 100644 index 59d3ec63..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/view/UARTViewEvent.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.view - -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.uart.data.UARTMacro - -internal sealed class UARTViewEvent - -internal data class OnEditMacro(val position: Int) : UARTViewEvent() -internal data class OnCreateMacro(val macro: UARTMacro) : UARTViewEvent() -internal data object OnDeleteMacro : UARTViewEvent() -internal data object OnEditFinish : UARTViewEvent() - -internal data class OnConfigurationSelected(val configuration: UARTConfiguration) : UARTViewEvent() -internal data class OnAddConfiguration(val name: String) : UARTViewEvent() -internal data object OnEditConfiguration : UARTViewEvent() -internal data object OnDeleteConfiguration : UARTViewEvent() -internal data class OnRunMacro(val macro: UARTMacro) : UARTViewEvent() -internal data class OnRunInput(val text: String, val newLineChar: MacroEol) : UARTViewEvent() - -internal data object ClearOutputItems : UARTViewEvent() -internal data object DisconnectEvent : UARTViewEvent() - -internal data object NavigateUp : UARTViewEvent() -internal data object OpenLogger : UARTViewEvent() - -internal data object MacroInputSwitchClick : UARTViewEvent() diff --git a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt b/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt deleted file mode 100644 index ed21abf1..00000000 --- a/profile_uart/src/main/java/no/nordicsemi/android/uart/viewmodel/UARTViewModel.kt +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Copyright (c) 2022, Nordic Semiconductor - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, are - * permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, this list of - * conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright notice, this list - * of conditions and the following disclaimer in the documentation and/or other materials - * provided with the distribution. - * - * 3. Neither the name of the copyright holder nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A - * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING - * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, - * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package no.nordicsemi.android.uart.viewmodel - -import android.os.ParcelUuid -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.analytics.Profile -import no.nordicsemi.android.analytics.ProfileConnectedEvent -import no.nordicsemi.android.analytics.UARTChangeConfiguration -import no.nordicsemi.android.analytics.UARTCreateConfiguration -import no.nordicsemi.android.analytics.UARTMode -import no.nordicsemi.android.analytics.UARTSendAnalyticsEvent -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.toolbox.scanner.ScannerDestinationId -import no.nordicsemi.android.uart.data.MacroEol -import no.nordicsemi.android.uart.data.UARTConfiguration -import no.nordicsemi.android.uart.data.UARTMacro -import no.nordicsemi.android.uart.data.UARTPersistentDataSource -import no.nordicsemi.android.uart.repository.UARTRepository -import no.nordicsemi.android.uart.repository.UART_SERVICE_UUID -import no.nordicsemi.android.uart.view.ClearOutputItems -import no.nordicsemi.android.uart.view.DisconnectEvent -import no.nordicsemi.android.uart.view.MacroInputSwitchClick -import no.nordicsemi.android.uart.view.NavigateUp -import no.nordicsemi.android.uart.view.OnAddConfiguration -import no.nordicsemi.android.uart.view.OnConfigurationSelected -import no.nordicsemi.android.uart.view.OnCreateMacro -import no.nordicsemi.android.uart.view.OnDeleteConfiguration -import no.nordicsemi.android.uart.view.OnDeleteMacro -import no.nordicsemi.android.uart.view.OnEditConfiguration -import no.nordicsemi.android.uart.view.OnEditFinish -import no.nordicsemi.android.uart.view.OnEditMacro -import no.nordicsemi.android.uart.view.OnRunInput -import no.nordicsemi.android.uart.view.OnRunMacro -import no.nordicsemi.android.uart.view.OpenLogger -import no.nordicsemi.android.uart.view.UARTViewEvent -import no.nordicsemi.android.uart.view.UARTViewState -import javax.inject.Inject - -@HiltViewModel -internal class UARTViewModel @Inject constructor( - private val repository: UARTRepository, - private val navigationManager: Navigator, - private val dataSource: UARTPersistentDataSource, - private val analytics: AppAnalytics, -) : ViewModel() { - - private val _state = MutableStateFlow(UARTViewState()) - val state = _state.asStateFlow() - - init { - repository.setOnScreen(true) - - viewModelScope.launch { - if (repository.isRunning.firstOrNull() == false) { - requestBluetoothDevice() - } - } - - repository.data.onEach { - _state.value = _state.value.copy(uartManagerState = it) - - if (it.connectionState?.state == GattConnectionState.STATE_CONNECTED) { - analytics.logEvent(ProfileConnectedEvent(Profile.UART)) - } - }.launchIn(viewModelScope) - - dataSource.getConfigurations().onEach { - _state.value = _state.value.copy(configurations = it) - }.launchIn(viewModelScope) - - repository.lastConfigurationName.onEach { - it?.let { - _state.value = _state.value.copy(selectedConfigurationName = it) - } - }.launchIn(viewModelScope) - } - - private fun requestBluetoothDevice() { - navigationManager.navigateTo(ScannerDestinationId, ParcelUuid(UART_SERVICE_UUID)) - - navigationManager.resultFrom(ScannerDestinationId) - .onEach { handleResult(it) } - .launchIn(viewModelScope) - } - - internal fun handleResult(result: NavigationResult) { - when (result) { - is NavigationResult.Cancelled -> navigationManager.navigateUp() - is NavigationResult.Success -> onDeviceSelected(result.value) - } - } - - private fun onDeviceSelected(device: ServerDevice) { - repository.launch(device) - } - - fun onEvent(event: UARTViewEvent) { - when (event) { - is OnCreateMacro -> addNewMacro(event.macro) - OnDeleteMacro -> deleteMacro() - DisconnectEvent -> disconnect() - is OnRunMacro -> runMacro(event.macro) - NavigateUp -> navigationManager.navigateUp() - is OnEditMacro -> onEditMacro(event) - OnEditFinish -> onEditFinish() - is OnConfigurationSelected -> onConfigurationSelected(event) - is OnAddConfiguration -> onAddConfiguration(event) - OnDeleteConfiguration -> deleteConfiguration() - OnEditConfiguration -> onEditConfiguration() - ClearOutputItems -> repository.clearItems() - OpenLogger -> repository.openLogger() - is OnRunInput -> sendText(event.text, event.newLineChar) - MacroInputSwitchClick -> onMacroInputSwitch() - } - } - - private fun runMacro(macro: UARTMacro) { - repository.runMacro(macro) - analytics.logEvent(UARTSendAnalyticsEvent(UARTMode.MACRO)) - } - - private fun sendText(text: String, newLineChar: MacroEol) { - repository.sendText(text, newLineChar) - analytics.logEvent(UARTSendAnalyticsEvent(UARTMode.TEXT)) - } - - private fun onMacroInputSwitch() { - _state.value = _state.value.copy(isInputVisible = !state.value.isInputVisible) - } - - private fun onEditConfiguration() { - val isEdited = _state.value.isConfigurationEdited - _state.value = _state.value.copy(isConfigurationEdited = !isEdited) - } - - private fun onAddConfiguration(event: OnAddConfiguration) { - viewModelScope.launch(Dispatchers.IO) { - dataSource.saveConfiguration(UARTConfiguration(null, event.name)) - _state.value = _state.value.copy(selectedConfigurationName = event.name) - } - saveLastConfigurationName(event.name) - analytics.logEvent(UARTCreateConfiguration()) - } - - private fun onEditMacro(event: OnEditMacro) { - _state.value = _state.value.copy(editedPosition = event.position) - } - - private fun onEditFinish() { - _state.value = _state.value.copy(editedPosition = null) - } - - private fun onConfigurationSelected(event: OnConfigurationSelected) { - saveLastConfigurationName(event.configuration.name) - analytics.logEvent(UARTChangeConfiguration()) - } - - private fun saveLastConfigurationName(name: String) { - viewModelScope.launch { - repository.saveConfigurationName(name) - } - } - - private fun addNewMacro(macro: UARTMacro) { - viewModelScope.launch(Dispatchers.IO) { - _state.value.selectedConfiguration?.let { - val macros = it.macros.toMutableList().apply { - set(_state.value.editedPosition!!, macro) - } - val newConf = it.copy(macros = macros) - dataSource.saveConfiguration(newConf) - _state.value = _state.value.copy(editedPosition = null) - } - } - } - - private fun deleteConfiguration() { - viewModelScope.launch(Dispatchers.IO) { - _state.value.selectedConfiguration?.let { - dataSource.deleteConfiguration(it) - } - } - } - - private fun deleteMacro() { - viewModelScope.launch(Dispatchers.IO) { - _state.value.selectedConfiguration?.let { - val macros = it.macros.toMutableList().apply { - set(_state.value.editedPosition!!, null) - } - val newConf = it.copy(macros = macros) - dataSource.saveConfiguration(newConf) - _state.value = _state.value.copy(editedPosition = null) - } - } - } - - private fun disconnect() { - repository.disconnect() - navigationManager.navigateUp() - } - - override fun onCleared() { - super.onCleared() - repository.setOnScreen(false) - } -} diff --git a/profile_uart/src/main/res/values/strings.xml b/profile_uart/src/main/res/values/strings.xml deleted file mode 100644 index 61c5f9be..00000000 --- a/profile_uart/src/main/res/values/strings.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - UART - - Please define a macro to send command to the device. - - Add - Delete selected configuration. - Edit selected configuration. - - Send - Input - Text to send - Macros - Output - Select configuration - Not selected. - Select configuration - - Command: %s - - Run macro - Delete macro - - Add macro - Here will be displayed read value from GATT characteristic. - - Add configuration - Configuration - Add macro - Alias - Command - Confirm - Dismiss - Delete - - EOL: - EOL: %s - LF - CR + LF - CR - - The incoming messages will be displayed here. - Provided command cannot be empty. - - Name - Provided name cannot be empty. - - Icon representing defined command. - - Delete configuration? - Are you sure that you want to delete this configuration? Your data will be irretrievably lost. - Confirm - Cancel - - Click to switch between text input and macro input. - Clear items. - Click to constantly scroll view to the latest available log. - --> %s - <-- %s - - Settings - Go to settings screen. - diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt deleted file mode 100644 index 8799220f..00000000 --- a/profile_uart/src/test/java/no/nordicsemi/android/gls/NordicLoggerFactoryTestModule.kt +++ /dev/null @@ -1,40 +0,0 @@ -package no.nordicsemi.android.gls - -import android.content.Context -import dagger.Module -import dagger.Provides -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import no.nordicsemi.android.common.logger.BleLoggerAndLauncher -import no.nordicsemi.android.ui.view.NordicLoggerFactory -import no.nordicsemi.android.ui.view.NordicLoggerFactoryHiltModule - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [NordicLoggerFactoryHiltModule::class] -) -class NordicLoggerFactoryTestModule { - - @Provides - fun createLogger(): NordicLoggerFactory { - return object : NordicLoggerFactory { - override fun createNordicLogger( - context: Context, - profile: String?, - key: String, - name: String?, - ): BleLoggerAndLauncher { - return object : BleLoggerAndLauncher { - override fun launch() { - - } - - override fun log(priority: Int, log: String) { - println(log) - } - } - } - } - } -} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt deleted file mode 100644 index 70b59201..00000000 --- a/profile_uart/src/test/java/no/nordicsemi/android/gls/ServiceManagerTestModule.kt +++ /dev/null @@ -1,57 +0,0 @@ -package no.nordicsemi.android.gls - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import dagger.Module -import dagger.Provides -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import no.nordicsemi.android.kotlin.ble.core.MockServerDevice -import no.nordicsemi.android.kotlin.ble.core.ServerDevice -import no.nordicsemi.android.service.DEVICE_DATA -import no.nordicsemi.android.service.ServiceManager -import no.nordicsemi.android.service.ServiceManagerHiltModule -import no.nordicsemi.android.uart.repository.UARTService -import org.robolectric.Robolectric -import org.robolectric.android.controller.ServiceController -import javax.inject.Singleton - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [ServiceManagerHiltModule::class] -) -class ServiceManagerTestModule { - - private val componentName = ComponentName("org.robolectric", UARTService::class.java.name) - - @Provides - internal fun provideDevice(): MockServerDevice { - return MockServerDevice( - name = "GLS Server", - address = "55:44:33:22:11" - ) - } - - @Provides - internal fun provideServiceController( - @ApplicationContext context: Context, - device: MockServerDevice - ): ServiceController { - return Robolectric.buildService(UARTService::class.java, Intent(context, UARTService::class.java).apply { - putExtra(DEVICE_DATA, device) - }) - } - - @Provides - @Singleton - internal fun provideServiceManager(controller: ServiceController): ServiceManager { - return object : ServiceManager { - override fun startService(service: Class, device: ServerDevice) { - controller.create().startCommand(3, 4).get() - } - } - } -} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt deleted file mode 100644 index 3dab6188..00000000 --- a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestDbHiltModule.kt +++ /dev/null @@ -1,29 +0,0 @@ -package no.nordicsemi.android.gls - -import android.content.Context -import androidx.room.Room -import dagger.Module -import dagger.Provides -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn -import no.nordicsemi.android.uart.DbHiltModule -import no.nordicsemi.android.uart.db.ConfigurationsDatabase -import no.nordicsemi.android.uart.db.MIGRATION_1_2 -import javax.inject.Singleton - -@Module -@TestInstallIn( - components = [SingletonComponent::class], - replaces = [DbHiltModule::class] -) -class TestDbHiltModule { - @Provides - @Singleton - internal fun provideDB(@ApplicationContext context: Context): ConfigurationsDatabase { - return Room.inMemoryDatabaseBuilder( - context, - ConfigurationsDatabase::class.java - ).addMigrations(MIGRATION_1_2).build() - } -} \ No newline at end of file diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt deleted file mode 100644 index 36e316cb..00000000 --- a/profile_uart/src/test/java/no/nordicsemi/android/gls/TestHiltModule.kt +++ /dev/null @@ -1,13 +0,0 @@ -package no.nordicsemi.android.gls - -import dagger.Module -import dagger.hilt.components.SingletonComponent -import dagger.hilt.testing.TestInstallIn - -//@Module -//@TestInstallIn( -// components = [SingletonComponent::class], -// replaces = [AnalyticsModule::class] -//) -//class TestHiltModule { -//} diff --git a/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt b/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt deleted file mode 100644 index c6e36800..00000000 --- a/profile_uart/src/test/java/no/nordicsemi/android/gls/UARTViewModelTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -package no.nordicsemi.android.gls - -import android.content.Context -import androidx.test.rule.ServiceTestRule -import dagger.hilt.android.testing.BindValue -import dagger.hilt.android.testing.HiltAndroidRule -import dagger.hilt.android.testing.HiltAndroidTest -import dagger.hilt.android.testing.HiltTestApplication -import dagger.hilt.android.testing.UninstallModules -import io.mockk.every -import io.mockk.impl.annotations.MockK -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.junit4.MockKRule -import io.mockk.mockk -import io.mockk.mockkObject -import io.mockk.mockkStatic -import io.mockk.spyk -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.advanceUntilIdle -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import no.nordicsemi.android.analytics.AppAnalytics -import no.nordicsemi.android.common.core.ApplicationScope -import no.nordicsemi.android.common.logger.BleLoggerAndLauncher -import no.nordicsemi.android.common.logger.DefaultBleLogger -import no.nordicsemi.android.common.navigation.NavigationResult -import no.nordicsemi.android.common.navigation.Navigator -import no.nordicsemi.android.common.navigation.di.NavigationModule -import no.nordicsemi.android.kotlin.ble.core.MockServerDevice -import no.nordicsemi.android.kotlin.ble.core.data.BleGattConnectionStatus -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionState -import no.nordicsemi.android.kotlin.ble.core.data.GattConnectionStateWithStatus -import no.nordicsemi.android.uart.UartServer -import no.nordicsemi.android.uart.data.UARTPersistentDataSource -import no.nordicsemi.android.uart.repository.UARTRepository -import no.nordicsemi.android.uart.view.DisconnectEvent -import no.nordicsemi.android.uart.viewmodel.UARTViewModel -import no.nordicsemi.android.ui.view.NordicLoggerFactory -import no.nordicsemi.android.ui.view.StringConst -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import javax.inject.Inject - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@HiltAndroidTest -@Config(application = HiltTestApplication::class) -@UninstallModules(NavigationModule::class) -@RunWith(RobolectricTestRunner::class) -internal class UARTViewModelTest { - - @get:Rule - val mockkRule = MockKRule(this) - - @get:Rule - val serviceRule = ServiceTestRule() - - @get:Rule - var hiltRule = HiltAndroidRule(this) - - @BindValue - @JvmField - val analyticsService: Navigator = mockk(relaxed = true) - - @RelaxedMockK - lateinit var analytics: AppAnalytics - - @MockK - lateinit var stringConst: StringConst - - @RelaxedMockK - lateinit var context: Context - - @RelaxedMockK - lateinit var logger: BleLoggerAndLauncher - - @Inject - lateinit var repository: UARTRepository - - @Inject - lateinit var dataSource: UARTPersistentDataSource - - lateinit var viewModel: UARTViewModel - - lateinit var uartServer: UartServer - - @Inject - lateinit var device: MockServerDevice - - @Before - fun setUp() { - hiltRule.inject() - Dispatchers.setMain(UnconfinedTestDispatcher()) - } - - @After - fun release() { - Dispatchers.resetMain() - } - - @Before - fun before() { - viewModel = UARTViewModel(repository, mockk(relaxed = true), dataSource, mockk(relaxed = true), object : - NordicLoggerFactory { - override fun createNordicLogger( - context: Context, - profile: String?, - key: String, - name: String?, - ): BleLoggerAndLauncher { - return logger - } - - }) - runBlocking { - mockkStatic("no.nordicsemi.android.common.core.ApplicationScopeKt") - every { ApplicationScope } returns CoroutineScope(UnconfinedTestDispatcher()) - every { stringConst.APP_NAME } returns "Test" - - uartServer = UartServer(CoroutineScope(UnconfinedTestDispatcher())) - uartServer.start(spyk(), device) - } - } - - @Before - fun prepareLogger() { - mockkObject(DefaultBleLogger.Companion) - every { DefaultBleLogger.create(any(), any(), any(), any()) } returns mockk() - } - - @Test - fun `when connected should return state connected`() = runTest { - val connectedState = GattConnectionStateWithStatus( - GattConnectionState.STATE_CONNECTED, - BleGattConnectionStatus.SUCCESS - ) - viewModel.handleResult(NavigationResult.Success(device)) - - advanceUntilIdle() - - assertEquals(connectedState, viewModel.state.value.uartManagerState.connectionState) - } - -// @Test -// fun `when disconnected should return state connected`() = runTest { -// val disconnectedState = GattConnectionStateWithStatus( -// GattConnectionState.STATE_DISCONNECTED, -// BleGattConnectionStatus.SUCCESS -// ) -// viewModel.handleResult(NavigationResult.Success(device)) -// viewModel.onEvent(DisconnectEvent) -// -// advanceUntilIdle() -// -// assertEquals(disconnectedState, viewModel.state.value.uartManagerState.connectionState) -// } -// -// @Test -// fun `when request last record then change status and get 1 record`() = runTest { -// viewModel.handleResult(NavigationResult.Success(device)) -// advanceUntilIdle() //Needed because of delay() in waitForBonding() -// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) -// -// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.LAST)) -// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) -// -// glsServer.continueWithResponse() //continue server breakpoint -// -// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) -// assertEquals(1, viewModel.state.value.glsServiceData.records.size) -// -// val parsedResponse = GlucoseMeasurementParser.parse(glsServer.OLDEST_RECORD) -// assertEquals(parsedResponse, viewModel.state.value.glsServiceData.records.keys.first()) -// } -// -// @Test -// fun `when request all record then change status and get 5 records`() = runTest { -// viewModel.handleResult(NavigationResult.Success(device)) -// advanceUntilIdle() //Needed because of delay() in waitForBonding() -// assertEquals(RequestStatus.IDLE, viewModel.state.value.glsServiceData.requestStatus) -// -// viewModel.onEvent(OnWorkingModeSelected(WorkingMode.ALL)) -// assertEquals(RequestStatus.PENDING, viewModel.state.value.glsServiceData.requestStatus) -// -// glsServer.continueWithResponse() //continue server breakpoint -// advanceUntilIdle() //We have to use because of delay() in sendAll() -// -// assertEquals(RequestStatus.SUCCESS, viewModel.state.value.glsServiceData.requestStatus) -// assertEquals(5, viewModel.state.value.glsServiceData.records.size) -// -// val expectedRecords = glsServer.records.map { GlucoseMeasurementParser.parse(it) } -// assertContentEquals(expectedRecords, viewModel.state.value.glsServiceData.records.keys) -// } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 76db6455..326376cd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -50,7 +50,7 @@ dependencyResolutionManagement { } versionCatalogs { create("libs") { - from("no.nordicsemi.android.gradle:version-catalog:2.4") + from("no.nordicsemi.android.gradle:version-catalog:2.9") } } } @@ -58,22 +58,16 @@ dependencyResolutionManagement { rootProject.name = "Android-nRF-Toolbox" include(":app") - -include(":profile_bps") -include(":profile_cgms") -include(":profile_csc") -include(":profile_gls") -include(":profile_hrs") -include(":profile_hts") -include(":profile_prx") -include(":profile_rscs") -include(":profile_uart") - include(":lib_analytics") -include(":lib_scanner") +include(":profile-parsers") include(":lib_service") +include(":lib_storage") include(":lib_ui") include(":lib_utils") +include(":profile") +include(":profile_data") +include(":profile_manager") +include(":permissions-ranging") //if (file("../Android-Common-Libraries").exists()) { // includeBuild("../Android-Common-Libraries") @@ -81,4 +75,4 @@ include(":lib_utils") // //if (file("../Kotlin-BLE-Library").exists()) { // includeBuild("../Kotlin-BLE-Library") -//} +//} \ No newline at end of file